mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-02-17 20:50:48 +01:00
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(":core")
|
||||||
|
|
||||||
|
implementation project(":chat-engine")
|
||||||
|
implementation project(":matrix-chat-engine")
|
||||||
|
|
||||||
implementation Dependencies.google.androidxComposeUi
|
implementation Dependencies.google.androidxComposeUi
|
||||||
implementation Dependencies.mavenCentral.ktorAndroid
|
implementation Dependencies.mavenCentral.ktorAndroid
|
||||||
implementation Dependencies.mavenCentral.sqldelightAndroid
|
implementation Dependencies.mavenCentral.sqldelightAndroid
|
||||||
|
@ -17,43 +17,29 @@ import app.dapk.st.core.extensions.ErrorTracker
|
|||||||
import app.dapk.st.core.extensions.unsafeLazy
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
import app.dapk.st.directory.DirectoryModule
|
import app.dapk.st.directory.DirectoryModule
|
||||||
import app.dapk.st.domain.StoreModule
|
import app.dapk.st.domain.StoreModule
|
||||||
|
import app.dapk.st.engine.MatrixEngine
|
||||||
import app.dapk.st.firebase.messaging.MessagingModule
|
import app.dapk.st.firebase.messaging.MessagingModule
|
||||||
import app.dapk.st.home.HomeModule
|
import app.dapk.st.home.HomeModule
|
||||||
import app.dapk.st.home.MainActivity
|
import app.dapk.st.home.MainActivity
|
||||||
import app.dapk.st.imageloader.ImageLoaderModule
|
import app.dapk.st.imageloader.ImageLoaderModule
|
||||||
import app.dapk.st.login.LoginModule
|
import app.dapk.st.login.LoginModule
|
||||||
import app.dapk.st.matrix.MatrixClient
|
|
||||||
import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator
|
import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator
|
||||||
import app.dapk.st.matrix.auth.authService
|
import app.dapk.st.matrix.common.EventId
|
||||||
import app.dapk.st.matrix.auth.installAuthService
|
import app.dapk.st.matrix.common.JsonString
|
||||||
import app.dapk.st.matrix.common.*
|
import app.dapk.st.matrix.common.MatrixLogger
|
||||||
import app.dapk.st.matrix.crypto.RoomMembersProvider
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.crypto.Verification
|
|
||||||
import app.dapk.st.matrix.crypto.cryptoService
|
|
||||||
import app.dapk.st.matrix.crypto.installCryptoService
|
|
||||||
import app.dapk.st.matrix.device.deviceService
|
|
||||||
import app.dapk.st.matrix.device.installEncryptionService
|
|
||||||
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
|
|
||||||
import app.dapk.st.matrix.message.*
|
|
||||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||||
import app.dapk.st.matrix.push.installPushService
|
|
||||||
import app.dapk.st.matrix.push.pushService
|
|
||||||
import app.dapk.st.matrix.room.*
|
|
||||||
import app.dapk.st.matrix.sync.*
|
|
||||||
import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent
|
|
||||||
import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
|
|
||||||
import app.dapk.st.messenger.MessengerActivity
|
import app.dapk.st.messenger.MessengerActivity
|
||||||
import app.dapk.st.messenger.MessengerModule
|
import app.dapk.st.messenger.MessengerModule
|
||||||
import app.dapk.st.messenger.gallery.ImageGalleryModule
|
import app.dapk.st.messenger.gallery.ImageGalleryModule
|
||||||
import app.dapk.st.navigator.IntentFactory
|
import app.dapk.st.navigator.IntentFactory
|
||||||
import app.dapk.st.navigator.MessageAttachment
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
import app.dapk.st.notifications.MatrixPushHandler
|
|
||||||
import app.dapk.st.notifications.NotificationsModule
|
import app.dapk.st.notifications.NotificationsModule
|
||||||
import app.dapk.st.olm.DeviceKeyFactory
|
|
||||||
import app.dapk.st.olm.OlmPersistenceWrapper
|
import app.dapk.st.olm.OlmPersistenceWrapper
|
||||||
import app.dapk.st.olm.OlmWrapper
|
|
||||||
import app.dapk.st.profile.ProfileModule
|
import app.dapk.st.profile.ProfileModule
|
||||||
|
import app.dapk.st.push.PushHandler
|
||||||
import app.dapk.st.push.PushModule
|
import app.dapk.st.push.PushModule
|
||||||
|
import app.dapk.st.push.PushTokenPayload
|
||||||
import app.dapk.st.push.messaging.MessagingServiceAdapter
|
import app.dapk.st.push.messaging.MessagingServiceAdapter
|
||||||
import app.dapk.st.settings.SettingsModule
|
import app.dapk.st.settings.SettingsModule
|
||||||
import app.dapk.st.share.ShareEntryModule
|
import app.dapk.st.share.ShareEntryModule
|
||||||
@ -62,8 +48,8 @@ import app.dapk.st.work.TaskRunnerModule
|
|||||||
import app.dapk.st.work.WorkModule
|
import app.dapk.st.work.WorkModule
|
||||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.time.Clock
|
|
||||||
|
|
||||||
internal class AppModule(context: Application, logger: MatrixLogger) {
|
internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||||
|
|
||||||
@ -77,9 +63,8 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
|||||||
|
|
||||||
private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db")
|
private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db")
|
||||||
private val database = DapkDb(driver)
|
private val database = DapkDb(driver)
|
||||||
private val clock = Clock.systemUTC()
|
|
||||||
val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
|
val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
|
||||||
val base64 = AndroidBase64()
|
private val base64 = AndroidBase64()
|
||||||
|
|
||||||
val storeModule = unsafeLazy {
|
val storeModule = unsafeLazy {
|
||||||
StoreModule(
|
StoreModule(
|
||||||
@ -95,9 +80,10 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
|||||||
private val imageLoaderModule = ImageLoaderModule(context)
|
private val imageLoaderModule = ImageLoaderModule(context)
|
||||||
|
|
||||||
private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) }
|
private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) }
|
||||||
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta)
|
private val chatEngineModule =
|
||||||
|
ChatEngineModule(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta)
|
||||||
|
|
||||||
val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, workModule, storeModule, context, coroutineDispatchers)
|
val domainModules = DomainModules(chatEngineModule, trackingModule.errorTracker, context, coroutineDispatchers)
|
||||||
|
|
||||||
val coreAndroidModule = CoreAndroidModule(
|
val coreAndroidModule = CoreAndroidModule(
|
||||||
intentFactory = object : IntentFactory {
|
intentFactory = object : IntentFactory {
|
||||||
@ -131,76 +117,57 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
|||||||
|
|
||||||
val featureModules = FeatureModules(
|
val featureModules = FeatureModules(
|
||||||
storeModule,
|
storeModule,
|
||||||
matrixModules,
|
chatEngineModule,
|
||||||
domainModules,
|
domainModules,
|
||||||
trackingModule,
|
trackingModule,
|
||||||
coreAndroidModule,
|
coreAndroidModule,
|
||||||
imageLoaderModule,
|
imageLoaderModule,
|
||||||
imageContentReader,
|
|
||||||
context,
|
context,
|
||||||
buildMeta,
|
buildMeta,
|
||||||
deviceMeta,
|
deviceMeta,
|
||||||
coroutineDispatchers,
|
coroutineDispatchers,
|
||||||
clock,
|
|
||||||
base64,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class FeatureModules internal constructor(
|
internal class FeatureModules internal constructor(
|
||||||
private val storeModule: Lazy<StoreModule>,
|
private val storeModule: Lazy<StoreModule>,
|
||||||
private val matrixModules: MatrixModules,
|
private val chatEngineModule: ChatEngineModule,
|
||||||
private val domainModules: DomainModules,
|
private val domainModules: DomainModules,
|
||||||
private val trackingModule: TrackingModule,
|
private val trackingModule: TrackingModule,
|
||||||
private val coreAndroidModule: CoreAndroidModule,
|
private val coreAndroidModule: CoreAndroidModule,
|
||||||
imageLoaderModule: ImageLoaderModule,
|
imageLoaderModule: ImageLoaderModule,
|
||||||
imageContentReader: ImageContentReader,
|
|
||||||
context: Context,
|
context: Context,
|
||||||
buildMeta: BuildMeta,
|
buildMeta: BuildMeta,
|
||||||
deviceMeta: DeviceMeta,
|
deviceMeta: DeviceMeta,
|
||||||
coroutineDispatchers: CoroutineDispatchers,
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
clock: Clock,
|
|
||||||
base64: Base64,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val directoryModule by unsafeLazy {
|
val directoryModule by unsafeLazy {
|
||||||
DirectoryModule(
|
DirectoryModule(
|
||||||
syncService = matrixModules.sync,
|
|
||||||
messageService = matrixModules.message,
|
|
||||||
context = context,
|
context = context,
|
||||||
credentialsStore = storeModule.value.credentialsStore(),
|
chatEngine = chatEngineModule.engine,
|
||||||
roomStore = storeModule.value.roomStore(),
|
|
||||||
roomService = matrixModules.room,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val loginModule by unsafeLazy {
|
val loginModule by unsafeLazy {
|
||||||
LoginModule(
|
LoginModule(
|
||||||
matrixModules.auth,
|
chatEngineModule.engine,
|
||||||
domainModules.pushModule,
|
domainModules.pushModule,
|
||||||
matrixModules.profile,
|
|
||||||
trackingModule.errorTracker
|
trackingModule.errorTracker
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val messengerModule by unsafeLazy {
|
val messengerModule by unsafeLazy {
|
||||||
MessengerModule(
|
MessengerModule(
|
||||||
matrixModules.sync,
|
chatEngineModule.engine,
|
||||||
matrixModules.message,
|
|
||||||
matrixModules.room,
|
|
||||||
storeModule.value.credentialsStore(),
|
|
||||||
storeModule.value.roomStore(),
|
|
||||||
clock,
|
|
||||||
context,
|
context,
|
||||||
base64,
|
|
||||||
imageContentReader,
|
|
||||||
storeModule.value.messageStore(),
|
storeModule.value.messageStore(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, matrixModules.sync, buildMeta) }
|
val homeModule by unsafeLazy { HomeModule(chatEngineModule.engine, storeModule.value, buildMeta) }
|
||||||
val settingsModule by unsafeLazy {
|
val settingsModule by unsafeLazy {
|
||||||
SettingsModule(
|
SettingsModule(
|
||||||
|
chatEngineModule.engine,
|
||||||
storeModule.value,
|
storeModule.value,
|
||||||
pushModule,
|
pushModule,
|
||||||
matrixModules.crypto,
|
|
||||||
matrixModules.sync,
|
|
||||||
context.contentResolver,
|
context.contentResolver,
|
||||||
buildMeta,
|
buildMeta,
|
||||||
deviceMeta,
|
deviceMeta,
|
||||||
@ -210,12 +177,11 @@ internal class FeatureModules internal constructor(
|
|||||||
storeModule.value.messageStore(),
|
storeModule.value.messageStore(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room, trackingModule.errorTracker) }
|
val profileModule by unsafeLazy { ProfileModule(chatEngineModule.engine, trackingModule.errorTracker) }
|
||||||
val notificationsModule by unsafeLazy {
|
val notificationsModule by unsafeLazy {
|
||||||
NotificationsModule(
|
NotificationsModule(
|
||||||
|
chatEngineModule.engine,
|
||||||
imageLoaderModule.iconLoader(),
|
imageLoaderModule.iconLoader(),
|
||||||
storeModule.value.roomStore(),
|
|
||||||
storeModule.value.overviewStore(),
|
|
||||||
context,
|
context,
|
||||||
intentFactory = coreAndroidModule.intentFactory(),
|
intentFactory = coreAndroidModule.intentFactory(),
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
@ -224,7 +190,7 @@ internal class FeatureModules internal constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val shareEntryModule by unsafeLazy {
|
val shareEntryModule by unsafeLazy {
|
||||||
ShareEntryModule(matrixModules.sync, matrixModules.room)
|
ShareEntryModule(chatEngineModule.engine)
|
||||||
}
|
}
|
||||||
|
|
||||||
val imageGalleryModule by unsafeLazy {
|
val imageGalleryModule by unsafeLazy {
|
||||||
@ -241,7 +207,7 @@ internal class FeatureModules internal constructor(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MatrixModules(
|
internal class ChatEngineModule(
|
||||||
private val storeModule: Lazy<StoreModule>,
|
private val storeModule: Lazy<StoreModule>,
|
||||||
private val trackingModule: TrackingModule,
|
private val trackingModule: TrackingModule,
|
||||||
private val workModule: WorkModule,
|
private val workModule: WorkModule,
|
||||||
@ -252,232 +218,48 @@ internal class MatrixModules(
|
|||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val matrix by unsafeLazy {
|
val engine by unsafeLazy {
|
||||||
val store = storeModule.value
|
val store = storeModule.value
|
||||||
val credentialsStore = store.credentialsStore()
|
MatrixEngine.Factory().create(
|
||||||
MatrixClient(
|
base64,
|
||||||
KtorMatrixHttpClientFactory(
|
buildMeta,
|
||||||
credentialsStore,
|
logger,
|
||||||
includeLogging = buildMeta.isDebug,
|
SmallTalkDeviceNameGenerator(),
|
||||||
),
|
coroutineDispatchers,
|
||||||
logger
|
trackingModule.errorTracker,
|
||||||
).also {
|
imageContentReader,
|
||||||
it.install {
|
BackgroundWorkAdapter(workModule.workScheduler()),
|
||||||
installAuthService(credentialsStore, SmallTalkDeviceNameGenerator())
|
store.memberStore(),
|
||||||
installEncryptionService(store.knownDevicesStore())
|
store.roomStore(),
|
||||||
|
store.profileStore(),
|
||||||
val olmAccountStore = OlmPersistenceWrapper(store.olmStore(), base64)
|
store.syncStore(),
|
||||||
val singletonFlows = SingletonFlows(coroutineDispatchers)
|
store.overviewStore(),
|
||||||
val olm = OlmWrapper(
|
store.filterStore(),
|
||||||
olmStore = olmAccountStore,
|
store.localEchoStore,
|
||||||
singletonFlows = singletonFlows,
|
store.credentialsStore(),
|
||||||
jsonCanonicalizer = JsonCanonicalizer(),
|
store.knownDevicesStore(),
|
||||||
deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()),
|
OlmPersistenceWrapper(store.olmStore(), base64),
|
||||||
errorTracker = trackingModule.errorTracker,
|
)
|
||||||
logger = logger,
|
|
||||||
clock = Clock.systemUTC(),
|
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
|
||||||
)
|
|
||||||
installCryptoService(
|
|
||||||
credentialsStore,
|
|
||||||
olm,
|
|
||||||
roomMembersProvider = { services ->
|
|
||||||
RoomMembersProvider {
|
|
||||||
services.roomService().joinedMembers(it).map { it.userId }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
base64 = base64,
|
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
|
||||||
)
|
|
||||||
installMessageService(
|
|
||||||
store.localEchoStore,
|
|
||||||
BackgroundWorkAdapter(workModule.workScheduler()),
|
|
||||||
imageContentReader,
|
|
||||||
messageEncrypter = {
|
|
||||||
val cryptoService = it.cryptoService()
|
|
||||||
MessageEncrypter { message ->
|
|
||||||
val result = cryptoService.encrypt(
|
|
||||||
roomId = message.roomId,
|
|
||||||
credentials = credentialsStore.credentials()!!,
|
|
||||||
messageJson = message.contents,
|
|
||||||
)
|
|
||||||
|
|
||||||
MessageEncrypter.EncryptedMessagePayload(
|
|
||||||
result.algorithmName,
|
|
||||||
result.senderKey,
|
|
||||||
result.cipherText,
|
|
||||||
result.sessionId,
|
|
||||||
result.deviceId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mediaEncrypter = {
|
|
||||||
val cryptoService = it.cryptoService()
|
|
||||||
MediaEncrypter { input ->
|
|
||||||
val result = cryptoService.encrypt(input)
|
|
||||||
MediaEncrypter.Result(
|
|
||||||
uri = result.uri,
|
|
||||||
contentLength = result.contentLength,
|
|
||||||
algorithm = result.algorithm,
|
|
||||||
ext = result.ext,
|
|
||||||
keyOperations = result.keyOperations,
|
|
||||||
kty = result.kty,
|
|
||||||
k = result.k,
|
|
||||||
iv = result.iv,
|
|
||||||
hashes = result.hashes,
|
|
||||||
v = result.v,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
val overviewStore = store.overviewStore()
|
|
||||||
installRoomService(
|
|
||||||
storeModule.value.memberStore(),
|
|
||||||
roomMessenger = {
|
|
||||||
val messageService = it.messageService()
|
|
||||||
object : RoomMessenger {
|
|
||||||
override suspend fun enableEncryption(roomId: RoomId) {
|
|
||||||
messageService.sendEventMessage(
|
|
||||||
roomId, MessageService.EventMessage.Encryption(
|
|
||||||
algorithm = AlgorithmName("m.megolm.v1.aes-sha2")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
roomInviteRemover = {
|
|
||||||
overviewStore.removeInvites(listOf(it))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
installProfileService(storeModule.value.profileStore(), singletonFlows, credentialsStore)
|
|
||||||
|
|
||||||
installSyncService(
|
|
||||||
credentialsStore,
|
|
||||||
overviewStore,
|
|
||||||
store.roomStore(),
|
|
||||||
store.syncStore(),
|
|
||||||
store.filterStore(),
|
|
||||||
deviceNotifier = { services ->
|
|
||||||
val encryption = services.deviceService()
|
|
||||||
val crypto = services.cryptoService()
|
|
||||||
DeviceNotifier { userIds, syncToken ->
|
|
||||||
encryption.updateStaleDevices(userIds)
|
|
||||||
crypto.updateOlmSession(userIds, syncToken)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
messageDecrypter = { serviceProvider ->
|
|
||||||
val cryptoService = serviceProvider.cryptoService()
|
|
||||||
MessageDecrypter {
|
|
||||||
cryptoService.decrypt(it)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keySharer = { serviceProvider ->
|
|
||||||
val cryptoService = serviceProvider.cryptoService()
|
|
||||||
KeySharer { sharedRoomKeys ->
|
|
||||||
cryptoService.importRoomKeys(sharedRoomKeys)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
verificationHandler = { services ->
|
|
||||||
val cryptoService = services.cryptoService()
|
|
||||||
VerificationHandler { apiEvent ->
|
|
||||||
logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
|
|
||||||
cryptoService.onVerificationEvent(
|
|
||||||
when (apiEvent) {
|
|
||||||
is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested(
|
|
||||||
apiEvent.sender,
|
|
||||||
apiEvent.content.fromDevice,
|
|
||||||
apiEvent.content.transactionId,
|
|
||||||
apiEvent.content.methods,
|
|
||||||
apiEvent.content.timestampPosix,
|
|
||||||
)
|
|
||||||
|
|
||||||
is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready(
|
|
||||||
apiEvent.sender,
|
|
||||||
apiEvent.content.fromDevice,
|
|
||||||
apiEvent.content.transactionId,
|
|
||||||
apiEvent.content.methods,
|
|
||||||
)
|
|
||||||
|
|
||||||
is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started(
|
|
||||||
apiEvent.sender,
|
|
||||||
apiEvent.content.fromDevice,
|
|
||||||
apiEvent.content.method,
|
|
||||||
apiEvent.content.protocols,
|
|
||||||
apiEvent.content.hashes,
|
|
||||||
apiEvent.content.codes,
|
|
||||||
apiEvent.content.short,
|
|
||||||
apiEvent.content.transactionId,
|
|
||||||
)
|
|
||||||
|
|
||||||
is ApiToDeviceEvent.VerificationCancel -> TODO()
|
|
||||||
is ApiToDeviceEvent.VerificationAccept -> TODO()
|
|
||||||
is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key(
|
|
||||||
apiEvent.sender,
|
|
||||||
apiEvent.content.transactionId,
|
|
||||||
apiEvent.content.key
|
|
||||||
)
|
|
||||||
|
|
||||||
is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac(
|
|
||||||
apiEvent.sender,
|
|
||||||
apiEvent.content.transactionId,
|
|
||||||
apiEvent.content.keys,
|
|
||||||
apiEvent.content.mac,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
oneTimeKeyProducer = { services ->
|
|
||||||
val cryptoService = services.cryptoService()
|
|
||||||
MaybeCreateMoreKeys {
|
|
||||||
cryptoService.maybeCreateMoreKeys(it)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
roomMembersService = { services ->
|
|
||||||
val roomService = services.roomService()
|
|
||||||
object : RoomMembersService {
|
|
||||||
override suspend fun find(roomId: RoomId, userIds: List<UserId>) = roomService.findMembers(roomId, userIds)
|
|
||||||
override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId)
|
|
||||||
override suspend fun insert(roomId: RoomId, members: List<RoomMember>) = roomService.insertMembers(roomId, members)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
errorTracker = trackingModule.errorTracker,
|
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
|
||||||
)
|
|
||||||
|
|
||||||
installPushService(credentialsStore)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val auth by unsafeLazy { matrix.authService() }
|
|
||||||
val push by unsafeLazy { matrix.pushService() }
|
|
||||||
val sync by unsafeLazy { matrix.syncService() }
|
|
||||||
val message by unsafeLazy { matrix.messageService() }
|
|
||||||
val room by unsafeLazy { matrix.roomService() }
|
|
||||||
val profile by unsafeLazy { matrix.profileService() }
|
|
||||||
val crypto by unsafeLazy { matrix.cryptoService() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DomainModules(
|
internal class DomainModules(
|
||||||
private val matrixModules: MatrixModules,
|
private val chatEngineModule: ChatEngineModule,
|
||||||
private val errorTracker: ErrorTracker,
|
private val errorTracker: ErrorTracker,
|
||||||
private val workModule: WorkModule,
|
|
||||||
private val storeModule: Lazy<StoreModule>,
|
|
||||||
private val context: Application,
|
private val context: Application,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val pushHandler by unsafeLazy {
|
private val pushHandler by unsafeLazy {
|
||||||
val store = storeModule.value
|
val enginePushHandler = chatEngineModule.engine.pushHandler()
|
||||||
MatrixPushHandler(
|
object : PushHandler {
|
||||||
workScheduler = workModule.workScheduler(),
|
override fun onNewToken(payload: PushTokenPayload) {
|
||||||
credentialsStore = store.credentialsStore(),
|
enginePushHandler.onNewToken(JsonString(Json.encodeToString(PushTokenPayload.serializer(), payload)))
|
||||||
matrixModules.sync,
|
}
|
||||||
store.roomStore(),
|
|
||||||
)
|
override fun onMessageReceived(eventId: EventId?, roomId: RoomId?) = enginePushHandler.onMessageReceived(eventId, roomId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val messaging by unsafeLazy { MessagingModule(MessagingServiceAdapter(pushHandler), context) }
|
val messaging by unsafeLazy { MessagingModule(MessagingServiceAdapter(pushHandler), context) }
|
||||||
@ -492,7 +274,9 @@ internal class DomainModules(
|
|||||||
messaging.messaging,
|
messaging.messaging,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) }
|
val taskRunnerModule by unsafeLazy {
|
||||||
|
TaskRunnerModule(TaskRunnerAdapter(chatEngineModule.engine, AppTaskRunner(chatEngineModule.engine)))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
package app.dapk.st.graph
|
package app.dapk.st.graph
|
||||||
|
|
||||||
import app.dapk.st.matrix.push.PushService
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.push.PushTokenPayload
|
import app.dapk.st.push.PushTokenPayload
|
||||||
import app.dapk.st.work.TaskRunner
|
import app.dapk.st.work.TaskRunner
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.*
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class AppTaskRunner(
|
class AppTaskRunner(
|
||||||
private val pushService: PushService,
|
private val chatEngine: ChatEngine,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun run(workTask: TaskRunner.RunnableWorkTask): TaskRunner.TaskResult {
|
suspend fun run(workTask: TaskRunner.RunnableWorkTask): TaskRunner.TaskResult {
|
||||||
@ -15,7 +15,7 @@ class AppTaskRunner(
|
|||||||
"push_token" -> {
|
"push_token" -> {
|
||||||
runCatching {
|
runCatching {
|
||||||
val payload = Json.decodeFromString(PushTokenPayload.serializer(), workTask.task.jsonPayload)
|
val payload = Json.decodeFromString(PushTokenPayload.serializer(), workTask.task.jsonPayload)
|
||||||
pushService.registerPush(payload.token, payload.gatewayUrl)
|
chatEngine.registerPushToken(payload.token, payload.gatewayUrl)
|
||||||
}.fold(
|
}.fold(
|
||||||
onSuccess = { TaskRunner.TaskResult.Success(workTask.source) },
|
onSuccess = { TaskRunner.TaskResult.Success(workTask.source) },
|
||||||
onFailure = {
|
onFailure = {
|
||||||
|
@ -9,7 +9,7 @@ class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : Backgrou
|
|||||||
WorkScheduler.WorkTask(
|
WorkScheduler.WorkTask(
|
||||||
jobId = 1,
|
jobId = 1,
|
||||||
type = task.type,
|
type = task.type,
|
||||||
jsonPayload = task.jsonPayload,
|
jsonPayload = task.jsonPayload.value,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
package app.dapk.st.graph
|
package app.dapk.st.graph
|
||||||
|
|
||||||
import app.dapk.st.matrix.MatrixTaskRunner
|
import app.dapk.st.engine.ChatEngine
|
||||||
|
import app.dapk.st.engine.ChatEngineTask
|
||||||
import app.dapk.st.work.TaskRunner
|
import app.dapk.st.work.TaskRunner
|
||||||
|
|
||||||
class TaskRunnerAdapter(
|
class TaskRunnerAdapter(
|
||||||
private val matrixTaskRunner: suspend (MatrixTaskRunner.MatrixTask) -> MatrixTaskRunner.TaskResult,
|
private val chatEngine: ChatEngine,
|
||||||
private val appTaskRunner: AppTaskRunner,
|
private val appTaskRunner: AppTaskRunner,
|
||||||
) : TaskRunner {
|
) : TaskRunner {
|
||||||
|
|
||||||
@ -12,11 +13,12 @@ class TaskRunnerAdapter(
|
|||||||
return tasks.map {
|
return tasks.map {
|
||||||
when {
|
when {
|
||||||
it.task.type.startsWith("matrix") -> {
|
it.task.type.startsWith("matrix") -> {
|
||||||
when (val result = matrixTaskRunner(MatrixTaskRunner.MatrixTask(it.task.type, it.task.jsonPayload))) {
|
when (val result = chatEngine.runTask(ChatEngineTask(it.task.type, it.task.jsonPayload))) {
|
||||||
is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry)
|
is app.dapk.st.engine.TaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry)
|
||||||
MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source)
|
app.dapk.st.engine.TaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> appTaskRunner.run(it)
|
else -> appTaskRunner.run(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
chat-engine/build.gradle
Normal file
13
chat-engine/build.gradle
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
plugins {
|
||||||
|
id 'kotlin'
|
||||||
|
id 'java-test-fixtures'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||||
|
api project(":matrix:common")
|
||||||
|
|
||||||
|
kotlinFixtures(it)
|
||||||
|
testFixturesImplementation(testFixtures(project(":matrix:common")))
|
||||||
|
testFixturesImplementation(testFixtures(project(":core")))
|
||||||
|
}
|
82
chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt
Normal file
82
chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package app.dapk.st.engine
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.common.EventId
|
||||||
|
import app.dapk.st.matrix.common.JsonString
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
interface ChatEngine : TaskRunner {
|
||||||
|
|
||||||
|
fun directory(): Flow<DirectoryState>
|
||||||
|
fun invites(): Flow<InviteState>
|
||||||
|
fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState>
|
||||||
|
|
||||||
|
fun notificationsMessages(): Flow<UnreadNotifications>
|
||||||
|
fun notificationsInvites(): Flow<InviteNotification>
|
||||||
|
|
||||||
|
suspend fun login(request: LoginRequest): LoginResult
|
||||||
|
|
||||||
|
suspend fun me(forceRefresh: Boolean): Me
|
||||||
|
|
||||||
|
suspend fun InputStream.importRoomKeys(password: String): Flow<ImportResult>
|
||||||
|
|
||||||
|
suspend fun send(message: SendMessage, room: RoomOverview)
|
||||||
|
|
||||||
|
suspend fun registerPushToken(token: String, gatewayUrl: String)
|
||||||
|
|
||||||
|
suspend fun joinRoom(roomId: RoomId)
|
||||||
|
|
||||||
|
suspend fun rejectJoinRoom(roomId: RoomId)
|
||||||
|
|
||||||
|
suspend fun findMembersSummary(roomId: RoomId): List<RoomMember>
|
||||||
|
|
||||||
|
fun mediaDecrypter(): MediaDecrypter
|
||||||
|
|
||||||
|
fun pushHandler(): PushHandler
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskRunner {
|
||||||
|
|
||||||
|
suspend fun runTask(task: ChatEngineTask): TaskResult
|
||||||
|
|
||||||
|
sealed interface TaskResult {
|
||||||
|
object Success : TaskResult
|
||||||
|
data class Failure(val canRetry: Boolean) : TaskResult
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class ChatEngineTask(val type: String, val jsonPayload: String)
|
||||||
|
|
||||||
|
interface MediaDecrypter {
|
||||||
|
|
||||||
|
fun decrypt(input: InputStream, k: String, iv: String): Collector
|
||||||
|
|
||||||
|
fun interface Collector {
|
||||||
|
fun collect(partial: (ByteArray) -> Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PushHandler {
|
||||||
|
fun onNewToken(payload: JsonString)
|
||||||
|
fun onMessageReceived(eventId: EventId?, roomId: RoomId?)
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias UnreadNotifications = Pair<Map<RoomOverview, List<RoomEvent>>, NotificationDiff>
|
||||||
|
|
||||||
|
data class NotificationDiff(
|
||||||
|
val unchanged: Map<RoomId, List<EventId>>,
|
||||||
|
val changedOrNew: Map<RoomId, List<EventId>>,
|
||||||
|
val removed: Map<RoomId, List<EventId>>,
|
||||||
|
val newRooms: Set<RoomId>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InviteNotification(
|
||||||
|
val content: String,
|
||||||
|
val roomId: RoomId
|
||||||
|
)
|
221
chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt
Normal file
221
chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
package app.dapk.st.engine
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.common.*
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
typealias DirectoryState = List<DirectoryItem>
|
||||||
|
typealias OverviewState = List<RoomOverview>
|
||||||
|
typealias InviteState = List<RoomInvite>
|
||||||
|
|
||||||
|
data class DirectoryItem(
|
||||||
|
val overview: RoomOverview,
|
||||||
|
val unreadCount: UnreadCount,
|
||||||
|
val typing: Typing?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RoomOverview(
|
||||||
|
val roomId: RoomId,
|
||||||
|
val roomCreationUtc: Long,
|
||||||
|
val roomName: String?,
|
||||||
|
val roomAvatarUrl: AvatarUrl?,
|
||||||
|
val lastMessage: LastMessage?,
|
||||||
|
val isGroup: Boolean,
|
||||||
|
val readMarker: EventId?,
|
||||||
|
val isEncrypted: Boolean,
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class LastMessage(
|
||||||
|
val content: String,
|
||||||
|
val utcTimestamp: Long,
|
||||||
|
val author: RoomMember,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RoomInvite(
|
||||||
|
val from: RoomMember,
|
||||||
|
val roomId: RoomId,
|
||||||
|
val inviteMeta: InviteMeta,
|
||||||
|
) {
|
||||||
|
sealed class InviteMeta {
|
||||||
|
object DirectMessage : InviteMeta()
|
||||||
|
data class Room(val roomName: String? = null) : InviteMeta()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class UnreadCount(val value: Int)
|
||||||
|
|
||||||
|
data class Typing(val roomId: RoomId, val members: List<RoomMember>)
|
||||||
|
|
||||||
|
data class LoginRequest(val userName: String, val password: String, val serverUrl: String?)
|
||||||
|
|
||||||
|
sealed interface LoginResult {
|
||||||
|
data class Success(val userCredentials: UserCredentials) : LoginResult
|
||||||
|
object MissingWellKnown : LoginResult
|
||||||
|
data class Error(val cause: Throwable) : LoginResult
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Me(
|
||||||
|
val userId: UserId,
|
||||||
|
val displayName: String?,
|
||||||
|
val avatarUrl: AvatarUrl?,
|
||||||
|
val homeServerUrl: HomeServerUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface ImportResult {
|
||||||
|
data class Success(val roomIds: Set<RoomId>, val totalImportedKeysCount: Long) : ImportResult
|
||||||
|
data class Error(val cause: Type) : ImportResult {
|
||||||
|
|
||||||
|
sealed interface Type {
|
||||||
|
data class Unknown(val cause: Throwable) : Type
|
||||||
|
object NoKeysFound : Type
|
||||||
|
object UnexpectedDecryptionOutput : Type
|
||||||
|
object UnableToOpenFile : Type
|
||||||
|
object InvalidFile : Type
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Update(val importedKeysCount: Long) : ImportResult
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MessengerState(
|
||||||
|
val self: UserId,
|
||||||
|
val roomState: RoomState,
|
||||||
|
val typing: Typing?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RoomState(
|
||||||
|
val roomOverview: RoomOverview,
|
||||||
|
val events: List<RoomEvent>,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val DEFAULT_ZONE = ZoneId.systemDefault()
|
||||||
|
internal val MESSAGE_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm")
|
||||||
|
|
||||||
|
sealed class RoomEvent {
|
||||||
|
|
||||||
|
abstract val eventId: EventId
|
||||||
|
abstract val utcTimestamp: Long
|
||||||
|
abstract val author: RoomMember
|
||||||
|
abstract val meta: MessageMeta
|
||||||
|
|
||||||
|
data class Message(
|
||||||
|
override val eventId: EventId,
|
||||||
|
override val utcTimestamp: Long,
|
||||||
|
val content: String,
|
||||||
|
override val author: RoomMember,
|
||||||
|
override val meta: MessageMeta,
|
||||||
|
val edited: Boolean = false,
|
||||||
|
val redacted: Boolean = false,
|
||||||
|
) : RoomEvent() {
|
||||||
|
|
||||||
|
val time: String by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||||
|
val instant = Instant.ofEpochMilli(utcTimestamp)
|
||||||
|
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Reply(
|
||||||
|
val message: RoomEvent,
|
||||||
|
val replyingTo: RoomEvent,
|
||||||
|
) : RoomEvent() {
|
||||||
|
|
||||||
|
override val eventId: EventId = message.eventId
|
||||||
|
override val utcTimestamp: Long = message.utcTimestamp
|
||||||
|
override val author: RoomMember = message.author
|
||||||
|
override val meta: MessageMeta = message.meta
|
||||||
|
|
||||||
|
val replyingToSelf = replyingTo.author == message.author
|
||||||
|
|
||||||
|
val time: String by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||||
|
val instant = Instant.ofEpochMilli(utcTimestamp)
|
||||||
|
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Image(
|
||||||
|
override val eventId: EventId,
|
||||||
|
override val utcTimestamp: Long,
|
||||||
|
val imageMeta: ImageMeta,
|
||||||
|
override val author: RoomMember,
|
||||||
|
override val meta: MessageMeta,
|
||||||
|
val edited: Boolean = false,
|
||||||
|
) : RoomEvent() {
|
||||||
|
|
||||||
|
val time: String by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||||
|
val instant = Instant.ofEpochMilli(utcTimestamp)
|
||||||
|
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ImageMeta(
|
||||||
|
val width: Int?,
|
||||||
|
val height: Int?,
|
||||||
|
val url: String,
|
||||||
|
val keys: Keys?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class Keys(
|
||||||
|
val k: String,
|
||||||
|
val iv: String,
|
||||||
|
val v: String,
|
||||||
|
val hashes: Map<String, String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class MessageMeta {
|
||||||
|
|
||||||
|
object FromServer : MessageMeta()
|
||||||
|
|
||||||
|
data class LocalEcho(
|
||||||
|
val echoId: String,
|
||||||
|
val state: State
|
||||||
|
) : MessageMeta() {
|
||||||
|
|
||||||
|
sealed class State {
|
||||||
|
object Sending : State()
|
||||||
|
|
||||||
|
object Sent : State()
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val message: String,
|
||||||
|
val type: Type,
|
||||||
|
) : State() {
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface SendMessage {
|
||||||
|
|
||||||
|
data class TextMessage(
|
||||||
|
val content: String,
|
||||||
|
val reply: Reply? = null,
|
||||||
|
) : SendMessage {
|
||||||
|
|
||||||
|
data class Reply(
|
||||||
|
val author: RoomMember,
|
||||||
|
val originalMessage: String,
|
||||||
|
val eventId: EventId,
|
||||||
|
val timestampUtc: Long,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ImageMessage(
|
||||||
|
val uri: String,
|
||||||
|
) : SendMessage
|
||||||
|
|
||||||
|
}
|
24
chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt
Normal file
24
chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package fake
|
||||||
|
|
||||||
|
import app.dapk.st.engine.ChatEngine
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import test.delegateEmit
|
||||||
|
import test.delegateReturn
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class FakeChatEngine : ChatEngine by mockk() {
|
||||||
|
|
||||||
|
fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn()
|
||||||
|
|
||||||
|
fun givenDirectory() = every { directory() }.delegateReturn()
|
||||||
|
|
||||||
|
fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn()
|
||||||
|
|
||||||
|
fun givenNotificationsInvites() = every { notificationsInvites() }.delegateEmit()
|
||||||
|
|
||||||
|
fun givenNotificationsMessages() = every { notificationsMessages() }.delegateEmit()
|
||||||
|
|
||||||
|
}
|
66
chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt
Normal file
66
chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package fixture
|
||||||
|
|
||||||
|
import app.dapk.st.engine.*
|
||||||
|
import app.dapk.st.matrix.common.*
|
||||||
|
|
||||||
|
fun aMessengerState(
|
||||||
|
self: UserId = aUserId(),
|
||||||
|
roomState: RoomState,
|
||||||
|
typing: Typing? = null
|
||||||
|
) = MessengerState(self, roomState, typing)
|
||||||
|
|
||||||
|
fun aRoomOverview(
|
||||||
|
roomId: RoomId = aRoomId(),
|
||||||
|
roomCreationUtc: Long = 0L,
|
||||||
|
roomName: String? = null,
|
||||||
|
roomAvatarUrl: AvatarUrl? = null,
|
||||||
|
lastMessage: RoomOverview.LastMessage? = null,
|
||||||
|
isGroup: Boolean = false,
|
||||||
|
readMarker: EventId? = null,
|
||||||
|
isEncrypted: Boolean = false,
|
||||||
|
) = RoomOverview(roomId, roomCreationUtc, roomName, roomAvatarUrl, lastMessage, isGroup, readMarker, isEncrypted)
|
||||||
|
|
||||||
|
fun anEncryptedRoomMessageEvent(
|
||||||
|
eventId: EventId = anEventId(),
|
||||||
|
utcTimestamp: Long = 0L,
|
||||||
|
content: String = "encrypted-content",
|
||||||
|
author: RoomMember = aRoomMember(),
|
||||||
|
meta: MessageMeta = MessageMeta.FromServer,
|
||||||
|
edited: Boolean = false,
|
||||||
|
redacted: Boolean = false,
|
||||||
|
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited, redacted)
|
||||||
|
|
||||||
|
fun aRoomImageMessageEvent(
|
||||||
|
eventId: EventId = anEventId(),
|
||||||
|
utcTimestamp: Long = 0L,
|
||||||
|
content: RoomEvent.Image.ImageMeta = anImageMeta(),
|
||||||
|
author: RoomMember = aRoomMember(),
|
||||||
|
meta: MessageMeta = MessageMeta.FromServer,
|
||||||
|
edited: Boolean = false,
|
||||||
|
) = RoomEvent.Image(eventId, utcTimestamp, content, author, meta, edited)
|
||||||
|
|
||||||
|
fun aRoomReplyMessageEvent(
|
||||||
|
message: RoomEvent = aRoomMessageEvent(),
|
||||||
|
replyingTo: RoomEvent = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")),
|
||||||
|
) = RoomEvent.Reply(message, replyingTo)
|
||||||
|
|
||||||
|
fun aRoomMessageEvent(
|
||||||
|
eventId: EventId = anEventId(),
|
||||||
|
utcTimestamp: Long = 0L,
|
||||||
|
content: String = "message-content",
|
||||||
|
author: RoomMember = aRoomMember(),
|
||||||
|
meta: MessageMeta = MessageMeta.FromServer,
|
||||||
|
edited: Boolean = false,
|
||||||
|
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited)
|
||||||
|
|
||||||
|
fun anImageMeta(
|
||||||
|
width: Int? = 100,
|
||||||
|
height: Int? = 100,
|
||||||
|
url: String = "https://a-url.com",
|
||||||
|
keys: RoomEvent.Image.ImageMeta.Keys? = null
|
||||||
|
) = RoomEvent.Image.ImageMeta(width, height, url, keys)
|
||||||
|
|
||||||
|
fun aRoomState(
|
||||||
|
roomOverview: RoomOverview = aRoomOverview(),
|
||||||
|
events: List<RoomEvent> = listOf(aRoomMessageEvent()),
|
||||||
|
) = RoomState(roomOverview, events)
|
@ -2,7 +2,7 @@ package fixture
|
|||||||
|
|
||||||
import app.dapk.st.matrix.common.EventId
|
import app.dapk.st.matrix.common.EventId
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.notifications.NotificationDiff
|
import app.dapk.st.engine.NotificationDiff
|
||||||
|
|
||||||
object NotificationDiffFixtures {
|
object NotificationDiffFixtures {
|
||||||
|
|
@ -1,6 +1,8 @@
|
|||||||
package app.dapk.st.core.extensions
|
package app.dapk.st.core.extensions
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
|
|
||||||
suspend fun <T> Flow<T>.firstOrNull(count: Int, predicate: suspend (T) -> Boolean): T? {
|
suspend fun <T> Flow<T>.firstOrNull(count: Int, predicate: suspend (T) -> Boolean): T? {
|
||||||
var counter = 0
|
var counter = 0
|
||||||
@ -18,5 +20,3 @@ suspend fun <T> Flow<T>.firstOrNull(count: Int, predicate: suspend (T) -> Boolea
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Flow<T>.startAndIgnoreEmissions(): Flow<Boolean> = this.map { false }.onStart { emit(true) }.filter { it }
|
|
@ -1,3 +1,5 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
@ -5,7 +5,6 @@ dependencies {
|
|||||||
implementation project(':core')
|
implementation project(':core')
|
||||||
implementation project(':domains:android:core')
|
implementation project(':domains:android:core')
|
||||||
implementation project(':domains:store')
|
implementation project(':domains:store')
|
||||||
implementation project(':matrix:services:push')
|
|
||||||
|
|
||||||
firebase(it, "messaging")
|
firebase(it, "messaging")
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.amshove.kluent.internal.assertEquals
|
import org.amshove.kluent.internal.assertEquals
|
||||||
import test.ExpectTestScope
|
import test.ExpectTestScope
|
||||||
|
import test.FlowTestObserver
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
internal class ViewModelTestScopeImpl(
|
internal class ViewModelTestScopeImpl(
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
applyAndroidComposeLibraryModule(project)
|
applyAndroidComposeLibraryModule(project)
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":matrix:services:sync")
|
implementation project(":chat-engine")
|
||||||
implementation project(":matrix:services:message")
|
|
||||||
implementation project(":matrix:services:room")
|
|
||||||
implementation project(":domains:android:compose-core")
|
implementation project(":domains:android:compose-core")
|
||||||
implementation project(":domains:android:viewmodel")
|
implementation project(":domains:android:viewmodel")
|
||||||
implementation project(":features:messenger")
|
implementation project(":features:messenger")
|
||||||
@ -13,11 +11,10 @@ dependencies {
|
|||||||
|
|
||||||
kotlinTest(it)
|
kotlinTest(it)
|
||||||
|
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
|
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:services:message"))
|
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||||
androidImportFixturesWorkaround(project, project(":core"))
|
androidImportFixturesWorkaround(project, project(":core"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||||
}
|
}
|
@ -38,9 +38,10 @@ import app.dapk.st.design.components.Toolbar
|
|||||||
import app.dapk.st.directory.DirectoryEvent.OpenDownloadUrl
|
import app.dapk.st.directory.DirectoryEvent.OpenDownloadUrl
|
||||||
import app.dapk.st.directory.DirectoryScreenState.Content
|
import app.dapk.st.directory.DirectoryScreenState.Content
|
||||||
import app.dapk.st.directory.DirectoryScreenState.EmptyLoading
|
import app.dapk.st.directory.DirectoryScreenState.EmptyLoading
|
||||||
|
import app.dapk.st.engine.DirectoryItem
|
||||||
|
import app.dapk.st.engine.RoomOverview
|
||||||
|
import app.dapk.st.engine.Typing
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
import app.dapk.st.messenger.MessengerActivity
|
import app.dapk.st.messenger.MessengerActivity
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
@ -147,7 +148,7 @@ private fun Content(listState: LazyListState, state: Content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DirectoryItem(room: RoomFoo, onClick: (RoomId) -> Unit, clock: Clock) {
|
private fun DirectoryItem(room: DirectoryItem, onClick: (RoomId) -> Unit, clock: Clock) {
|
||||||
val overview = room.overview
|
val overview = room.overview
|
||||||
val roomName = overview.roomName ?: "Empty room"
|
val roomName = overview.roomName ?: "Empty room"
|
||||||
val hasUnread = room.unreadCount.value > 0
|
val hasUnread = room.unreadCount.value > 0
|
||||||
@ -233,7 +234,7 @@ private fun DirectoryItem(room: RoomFoo, onClick: (RoomId) -> Unit, clock: Clock
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun body(overview: RoomOverview, secondaryText: Color, typing: SyncService.SyncEvent.Typing?) {
|
private fun body(overview: RoomOverview, secondaryText: Color, typing: Typing?) {
|
||||||
val bodySize = 14.sp
|
val bodySize = 14.sp
|
||||||
|
|
||||||
when {
|
when {
|
||||||
|
@ -2,31 +2,17 @@ package app.dapk.st.directory
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.message.MessageService
|
|
||||||
import app.dapk.st.matrix.room.RoomService
|
|
||||||
import app.dapk.st.matrix.sync.RoomStore
|
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
|
|
||||||
class DirectoryModule(
|
class DirectoryModule(
|
||||||
private val syncService: SyncService,
|
|
||||||
private val messageService: MessageService,
|
|
||||||
private val roomService: RoomService,
|
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val credentialsStore: CredentialsStore,
|
private val chatEngine: ChatEngine,
|
||||||
private val roomStore: RoomStore,
|
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun directoryViewModel(): DirectoryViewModel {
|
fun directoryViewModel(): DirectoryViewModel {
|
||||||
return DirectoryViewModel(
|
return DirectoryViewModel(
|
||||||
ShortcutHandler(context),
|
ShortcutHandler(context),
|
||||||
DirectoryUseCase(
|
chatEngine,
|
||||||
syncService,
|
|
||||||
messageService,
|
|
||||||
roomService,
|
|
||||||
credentialsStore,
|
|
||||||
roomStore,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package app.dapk.st.directory
|
package app.dapk.st.directory
|
||||||
|
|
||||||
|
import app.dapk.st.engine.DirectoryState
|
||||||
|
|
||||||
sealed interface DirectoryScreenState {
|
sealed interface DirectoryScreenState {
|
||||||
|
|
||||||
object EmptyLoading : DirectoryScreenState
|
object EmptyLoading : DirectoryScreenState
|
||||||
|
@ -2,6 +2,7 @@ package app.dapk.st.directory
|
|||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.dapk.st.directory.DirectoryScreenState.*
|
import app.dapk.st.directory.DirectoryScreenState.*
|
||||||
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
import app.dapk.st.viewmodel.DapkViewModel
|
||||||
import app.dapk.st.viewmodel.MutableStateFactory
|
import app.dapk.st.viewmodel.MutableStateFactory
|
||||||
import app.dapk.st.viewmodel.defaultStateFactory
|
import app.dapk.st.viewmodel.defaultStateFactory
|
||||||
@ -12,7 +13,7 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
class DirectoryViewModel(
|
class DirectoryViewModel(
|
||||||
private val shortcutHandler: ShortcutHandler,
|
private val shortcutHandler: ShortcutHandler,
|
||||||
private val directoryUseCase: DirectoryUseCase,
|
private val chatEngine: ChatEngine,
|
||||||
factory: MutableStateFactory<DirectoryScreenState> = defaultStateFactory(),
|
factory: MutableStateFactory<DirectoryScreenState> = defaultStateFactory(),
|
||||||
) : DapkViewModel<DirectoryScreenState, DirectoryEvent>(
|
) : DapkViewModel<DirectoryScreenState, DirectoryEvent>(
|
||||||
initialState = EmptyLoading,
|
initialState = EmptyLoading,
|
||||||
@ -23,7 +24,7 @@ class DirectoryViewModel(
|
|||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
syncJob = viewModelScope.launch {
|
syncJob = viewModelScope.launch {
|
||||||
directoryUseCase.state().onEach {
|
chatEngine.directory().onEach {
|
||||||
shortcutHandler.onDirectoryUpdate(it.map { it.overview })
|
shortcutHandler.onDirectoryUpdate(it.map { it.overview })
|
||||||
state = when (it.isEmpty()) {
|
state = when (it.isEmpty()) {
|
||||||
true -> Empty
|
true -> Empty
|
||||||
|
@ -5,8 +5,8 @@ import android.content.pm.ShortcutInfo
|
|||||||
import androidx.core.app.Person
|
import androidx.core.app.Person
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
|
import app.dapk.st.engine.RoomOverview
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import app.dapk.st.messenger.MessengerActivity
|
import app.dapk.st.messenger.MessengerActivity
|
||||||
|
|
||||||
class ShortcutHandler(private val context: Context) {
|
class ShortcutHandler(private val context: Context) {
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
package app.dapk.st.directory
|
package app.dapk.st.directory
|
||||||
|
|
||||||
import ViewModelTest
|
import ViewModelTest
|
||||||
|
import app.dapk.st.engine.DirectoryItem
|
||||||
|
import app.dapk.st.engine.UnreadCount
|
||||||
|
import fake.FakeChatEngine
|
||||||
import fixture.aRoomOverview
|
import fixture.aRoomOverview
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import test.delegateReturn
|
|
||||||
|
|
||||||
private val AN_OVERVIEW = aRoomOverview()
|
private val AN_OVERVIEW = aRoomOverview()
|
||||||
private val AN_OVERVIEW_STATE = RoomFoo(AN_OVERVIEW, UnreadCount(1), null)
|
private val AN_OVERVIEW_STATE = DirectoryItem(AN_OVERVIEW, UnreadCount(1), null)
|
||||||
|
|
||||||
class DirectoryViewModelTest {
|
class DirectoryViewModelTest {
|
||||||
|
|
||||||
private val runViewModelTest = ViewModelTest()
|
private val runViewModelTest = ViewModelTest()
|
||||||
private val fakeDirectoryUseCase = FakeDirectoryUseCase()
|
|
||||||
private val fakeShortcutHandler = FakeShortcutHandler()
|
private val fakeShortcutHandler = FakeShortcutHandler()
|
||||||
|
private val fakeChatEngine = FakeChatEngine()
|
||||||
|
|
||||||
private val viewModel = DirectoryViewModel(
|
private val viewModel = DirectoryViewModel(
|
||||||
fakeShortcutHandler.instance,
|
fakeShortcutHandler.instance,
|
||||||
fakeDirectoryUseCase.instance,
|
fakeChatEngine,
|
||||||
runViewModelTest.testMutableStateFactory(),
|
runViewModelTest.testMutableStateFactory(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ class DirectoryViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `when starting, then updates shortcuts and emits room state`() = runViewModelTest {
|
fun `when starting, then updates shortcuts and emits room state`() = runViewModelTest {
|
||||||
fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(listOf(AN_OVERVIEW)) }
|
fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(listOf(AN_OVERVIEW)) }
|
||||||
fakeDirectoryUseCase.given().returns(flowOf(listOf(AN_OVERVIEW_STATE)))
|
fakeChatEngine.givenDirectory().returns(flowOf(listOf(AN_OVERVIEW_STATE)))
|
||||||
|
|
||||||
viewModel.test().start()
|
viewModel.test().start()
|
||||||
|
|
||||||
@ -44,9 +45,4 @@ class DirectoryViewModelTest {
|
|||||||
|
|
||||||
class FakeShortcutHandler {
|
class FakeShortcutHandler {
|
||||||
val instance = mockk<ShortcutHandler>()
|
val instance = mockk<ShortcutHandler>()
|
||||||
}
|
|
||||||
|
|
||||||
class FakeDirectoryUseCase {
|
|
||||||
val instance = mockk<DirectoryUseCase>()
|
|
||||||
fun given() = every { instance.state() }.delegateReturn()
|
|
||||||
}
|
}
|
@ -1,9 +1,7 @@
|
|||||||
applyAndroidComposeLibraryModule(project)
|
applyAndroidComposeLibraryModule(project)
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":matrix:services:profile")
|
implementation project(":chat-engine")
|
||||||
implementation project(":matrix:services:crypto")
|
|
||||||
implementation project(":matrix:services:sync")
|
|
||||||
implementation project(":features:directory")
|
implementation project(":features:directory")
|
||||||
implementation project(":features:login")
|
implementation project(":features:login")
|
||||||
implementation project(":features:settings")
|
implementation project(":features:settings")
|
||||||
|
@ -4,31 +4,28 @@ import app.dapk.st.core.BuildMeta
|
|||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.directory.DirectoryViewModel
|
import app.dapk.st.directory.DirectoryViewModel
|
||||||
import app.dapk.st.domain.StoreModule
|
import app.dapk.st.domain.StoreModule
|
||||||
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.login.LoginViewModel
|
import app.dapk.st.login.LoginViewModel
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
import app.dapk.st.profile.ProfileViewModel
|
import app.dapk.st.profile.ProfileViewModel
|
||||||
|
|
||||||
class HomeModule(
|
class HomeModule(
|
||||||
|
private val chatEngine: ChatEngine,
|
||||||
private val storeModule: StoreModule,
|
private val storeModule: StoreModule,
|
||||||
private val profileService: ProfileService,
|
|
||||||
private val syncService: SyncService,
|
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun homeViewModel(directory: DirectoryViewModel, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel {
|
fun homeViewModel(directory: DirectoryViewModel, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel {
|
||||||
return HomeViewModel(
|
return HomeViewModel(
|
||||||
|
chatEngine,
|
||||||
storeModule.credentialsStore(),
|
storeModule.credentialsStore(),
|
||||||
directory,
|
directory,
|
||||||
login,
|
login,
|
||||||
profileViewModel,
|
profileViewModel,
|
||||||
profileService,
|
|
||||||
storeModule.cacheCleaner(),
|
storeModule.cacheCleaner(),
|
||||||
BetaVersionUpgradeUseCase(
|
BetaVersionUpgradeUseCase(
|
||||||
storeModule.applicationStore(),
|
storeModule.applicationStore(),
|
||||||
buildMeta,
|
buildMeta,
|
||||||
),
|
),
|
||||||
syncService,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,13 +4,13 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
import app.dapk.st.engine.Me
|
||||||
|
|
||||||
sealed interface HomeScreenState {
|
sealed interface HomeScreenState {
|
||||||
|
|
||||||
object Loading : HomeScreenState
|
object Loading : HomeScreenState
|
||||||
object SignedOut : HomeScreenState
|
object SignedOut : HomeScreenState
|
||||||
data class SignedIn(val page: Page, val me: ProfileService.Me, val invites: Int) : HomeScreenState
|
data class SignedIn(val page: Page, val me: Me, val invites: Int) : HomeScreenState
|
||||||
|
|
||||||
enum class Page(val icon: ImageVector) {
|
enum class Page(val icon: ImageVector) {
|
||||||
Directory(Icons.Filled.Menu),
|
Directory(Icons.Filled.Menu),
|
||||||
|
@ -3,12 +3,11 @@ package app.dapk.st.home
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.dapk.st.directory.DirectoryViewModel
|
import app.dapk.st.directory.DirectoryViewModel
|
||||||
import app.dapk.st.domain.StoreCleaner
|
import app.dapk.st.domain.StoreCleaner
|
||||||
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.home.HomeScreenState.*
|
import app.dapk.st.home.HomeScreenState.*
|
||||||
import app.dapk.st.login.LoginViewModel
|
import app.dapk.st.login.LoginViewModel
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.matrix.common.CredentialsStore
|
||||||
import app.dapk.st.matrix.common.isSignedIn
|
import app.dapk.st.matrix.common.isSignedIn
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
import app.dapk.st.profile.ProfileViewModel
|
import app.dapk.st.profile.ProfileViewModel
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
import app.dapk.st.viewmodel.DapkViewModel
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -20,14 +19,13 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class HomeViewModel(
|
class HomeViewModel(
|
||||||
|
private val chatEngine: ChatEngine,
|
||||||
private val credentialsProvider: CredentialsStore,
|
private val credentialsProvider: CredentialsStore,
|
||||||
private val directoryViewModel: DirectoryViewModel,
|
private val directoryViewModel: DirectoryViewModel,
|
||||||
private val loginViewModel: LoginViewModel,
|
private val loginViewModel: LoginViewModel,
|
||||||
private val profileViewModel: ProfileViewModel,
|
private val profileViewModel: ProfileViewModel,
|
||||||
private val profileService: ProfileService,
|
|
||||||
private val cacheCleaner: StoreCleaner,
|
private val cacheCleaner: StoreCleaner,
|
||||||
private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||||
private val syncService: SyncService,
|
|
||||||
) : DapkViewModel<HomeScreenState, HomeEvent>(
|
) : DapkViewModel<HomeScreenState, HomeEvent>(
|
||||||
initialState = Loading
|
initialState = Loading
|
||||||
) {
|
) {
|
||||||
@ -56,8 +54,8 @@ class HomeViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun initialHomeContent(): SignedIn {
|
private suspend fun initialHomeContent(): SignedIn {
|
||||||
val me = profileService.me(forceRefresh = false)
|
val me = chatEngine.me(forceRefresh = false)
|
||||||
val initialInvites = syncService.invites().first().size
|
val initialInvites = chatEngine.invites().first().size
|
||||||
return SignedIn(Page.Directory, me, invites = initialInvites)
|
return SignedIn(Page.Directory, me, invites = initialInvites)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +68,7 @@ class HomeViewModel(
|
|||||||
|
|
||||||
private fun CoroutineScope.listenForInviteChanges() {
|
private fun CoroutineScope.listenForInviteChanges() {
|
||||||
listenForInvitesJob?.cancel()
|
listenForInvitesJob?.cancel()
|
||||||
listenForInvitesJob = syncService.invites()
|
listenForInvitesJob = chatEngine.invites()
|
||||||
.onEach { invites ->
|
.onEach { invites ->
|
||||||
when (val currentState = state) {
|
when (val currentState = state) {
|
||||||
is SignedIn -> updateState { currentState.copy(invites = invites.size) }
|
is SignedIn -> updateState { currentState.copy(invites = invites.size) }
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
applyAndroidComposeLibraryModule(project)
|
applyAndroidComposeLibraryModule(project)
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(":chat-engine")
|
||||||
implementation project(":domains:android:compose-core")
|
implementation project(":domains:android:compose-core")
|
||||||
implementation project(":domains:android:push")
|
implementation project(":domains:android:push")
|
||||||
implementation project(":domains:android:viewmodel")
|
implementation project(":domains:android:viewmodel")
|
||||||
implementation project(":matrix:services:auth")
|
|
||||||
implementation project(":matrix:services:profile")
|
|
||||||
implementation project(":matrix:services:crypto")
|
|
||||||
implementation project(":design-library")
|
implementation project(":design-library")
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
}
|
}
|
@ -2,18 +2,16 @@ package app.dapk.st.login
|
|||||||
|
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
import app.dapk.st.matrix.auth.AuthService
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
|
||||||
import app.dapk.st.push.PushModule
|
import app.dapk.st.push.PushModule
|
||||||
|
|
||||||
class LoginModule(
|
class LoginModule(
|
||||||
private val authService: AuthService,
|
private val chatEngine: ChatEngine,
|
||||||
private val pushModule: PushModule,
|
private val pushModule: PushModule,
|
||||||
private val profileService: ProfileService,
|
|
||||||
private val errorTracker: ErrorTracker,
|
private val errorTracker: ErrorTracker,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun loginViewModel(): LoginViewModel {
|
fun loginViewModel(): LoginViewModel {
|
||||||
return LoginViewModel(authService, pushModule.pushTokenRegistrar(), profileService, errorTracker)
|
return LoginViewModel(chatEngine, pushModule.pushTokenRegistrar(), errorTracker)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,10 +3,11 @@ package app.dapk.st.login
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
import app.dapk.st.core.logP
|
import app.dapk.st.core.logP
|
||||||
|
import app.dapk.st.engine.ChatEngine
|
||||||
|
import app.dapk.st.engine.LoginRequest
|
||||||
|
import app.dapk.st.engine.LoginResult
|
||||||
import app.dapk.st.login.LoginEvent.LoginComplete
|
import app.dapk.st.login.LoginEvent.LoginComplete
|
||||||
import app.dapk.st.login.LoginScreenState.*
|
import app.dapk.st.login.LoginScreenState.*
|
||||||
import app.dapk.st.matrix.auth.AuthService
|
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
|
||||||
import app.dapk.st.push.PushTokenRegistrar
|
import app.dapk.st.push.PushTokenRegistrar
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
import app.dapk.st.viewmodel.DapkViewModel
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@ -14,9 +15,8 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class LoginViewModel(
|
class LoginViewModel(
|
||||||
private val authService: AuthService,
|
private val chatEngine: ChatEngine,
|
||||||
private val pushTokenRegistrar: PushTokenRegistrar,
|
private val pushTokenRegistrar: PushTokenRegistrar,
|
||||||
private val profileService: ProfileService,
|
|
||||||
private val errorTracker: ErrorTracker,
|
private val errorTracker: ErrorTracker,
|
||||||
) : DapkViewModel<LoginScreenState, LoginEvent>(
|
) : DapkViewModel<LoginScreenState, LoginEvent>(
|
||||||
initialState = Content(showServerUrl = false)
|
initialState = Content(showServerUrl = false)
|
||||||
@ -28,8 +28,8 @@ class LoginViewModel(
|
|||||||
state = Loading
|
state = Loading
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
logP("login") {
|
logP("login") {
|
||||||
when (val result = authService.login(AuthService.LoginRequest(userName, password, serverUrl.takeIfNotEmpty()))) {
|
when (val result = chatEngine.login(LoginRequest(userName, password, serverUrl.takeIfNotEmpty()))) {
|
||||||
is AuthService.LoginResult.Success -> {
|
is LoginResult.Success -> {
|
||||||
runCatching {
|
runCatching {
|
||||||
listOf(
|
listOf(
|
||||||
async { pushTokenRegistrar.registerCurrentToken() },
|
async { pushTokenRegistrar.registerCurrentToken() },
|
||||||
@ -38,11 +38,13 @@ class LoginViewModel(
|
|||||||
}
|
}
|
||||||
_events.tryEmit(LoginComplete)
|
_events.tryEmit(LoginComplete)
|
||||||
}
|
}
|
||||||
is AuthService.LoginResult.Error -> {
|
|
||||||
|
is LoginResult.Error -> {
|
||||||
errorTracker.track(result.cause)
|
errorTracker.track(result.cause)
|
||||||
state = Error(result.cause)
|
state = Error(result.cause)
|
||||||
}
|
}
|
||||||
AuthService.LoginResult.MissingWellKnown -> {
|
|
||||||
|
LoginResult.MissingWellKnown -> {
|
||||||
_events.tryEmit(LoginEvent.WellKnownMissing)
|
_events.tryEmit(LoginEvent.WellKnownMissing)
|
||||||
state = Content(showServerUrl = true)
|
state = Content(showServerUrl = true)
|
||||||
}
|
}
|
||||||
@ -51,7 +53,7 @@ class LoginViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun preloadMe() = profileService.me(forceRefresh = false)
|
private suspend fun preloadMe() = chatEngine.me(forceRefresh = false)
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
val showServerUrl = previousState?.let { it is Content && it.showServerUrl } ?: false
|
val showServerUrl = previousState?.let { it is Content && it.showServerUrl } ?: false
|
||||||
|
@ -2,10 +2,7 @@ applyAndroidComposeLibraryModule(project)
|
|||||||
apply plugin: 'kotlin-parcelize'
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":matrix:services:sync")
|
implementation project(":chat-engine")
|
||||||
implementation project(":matrix:services:message")
|
|
||||||
implementation project(":matrix:services:crypto")
|
|
||||||
implementation project(":matrix:services:room")
|
|
||||||
implementation project(":domains:android:compose-core")
|
implementation project(":domains:android:compose-core")
|
||||||
implementation project(":domains:android:viewmodel")
|
implementation project(":domains:android:viewmodel")
|
||||||
implementation project(":domains:store")
|
implementation project(":domains:store")
|
||||||
@ -16,11 +13,10 @@ dependencies {
|
|||||||
|
|
||||||
kotlinTest(it)
|
kotlinTest(it)
|
||||||
|
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
|
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:services:message"))
|
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||||
androidImportFixturesWorkaround(project, project(":core"))
|
androidImportFixturesWorkaround(project, project(":core"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||||
}
|
}
|
@ -2,10 +2,9 @@ package app.dapk.st.messenger
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import app.dapk.st.core.Base64
|
import app.dapk.st.engine.MediaDecrypter
|
||||||
|
import app.dapk.st.engine.RoomEvent
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.crypto.MediaDecrypter
|
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.decode.DataSource
|
import coil.decode.DataSource
|
||||||
import coil.decode.ImageSource
|
import coil.decode.ImageSource
|
||||||
@ -20,9 +19,11 @@ import okio.BufferedSource
|
|||||||
import okio.Path.Companion.toOkioPath
|
import okio.Path.Companion.toOkioPath
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class DecryptingFetcherFactory(private val context: Context, base64: Base64, private val roomId: RoomId) : Fetcher.Factory<RoomEvent.Image> {
|
class DecryptingFetcherFactory(
|
||||||
|
private val context: Context,
|
||||||
private val mediaDecrypter = MediaDecrypter(base64)
|
private val roomId: RoomId,
|
||||||
|
private val mediaDecrypter: MediaDecrypter,
|
||||||
|
) : Fetcher.Factory<RoomEvent.Image> {
|
||||||
|
|
||||||
override fun create(data: RoomEvent.Image, options: Options, imageLoader: ImageLoader): Fetcher {
|
override fun create(data: RoomEvent.Image, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||||
return DecryptingFetcher(data, context, mediaDecrypter, roomId)
|
return DecryptingFetcher(data, context, mediaDecrypter, roomId)
|
||||||
|
@ -1,49 +1,23 @@
|
|||||||
package app.dapk.st.messenger
|
package app.dapk.st.messenger
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.dapk.st.core.Base64
|
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.message.MessageService
|
|
||||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
|
||||||
import app.dapk.st.matrix.room.RoomService
|
|
||||||
import app.dapk.st.matrix.sync.RoomStore
|
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
import java.time.Clock
|
|
||||||
|
|
||||||
class MessengerModule(
|
class MessengerModule(
|
||||||
private val syncService: SyncService,
|
private val chatEngine: ChatEngine,
|
||||||
private val messageService: MessageService,
|
|
||||||
private val roomService: RoomService,
|
|
||||||
private val credentialsStore: CredentialsStore,
|
|
||||||
private val roomStore: RoomStore,
|
|
||||||
private val clock: Clock,
|
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val base64: Base64,
|
|
||||||
private val imageMetaReader: ImageContentReader,
|
|
||||||
private val messageOptionsStore: MessageOptionsStore,
|
private val messageOptionsStore: MessageOptionsStore,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
internal fun messengerViewModel(): MessengerViewModel {
|
internal fun messengerViewModel(): MessengerViewModel {
|
||||||
return MessengerViewModel(
|
return MessengerViewModel(
|
||||||
messageService,
|
chatEngine,
|
||||||
roomService,
|
|
||||||
roomStore,
|
|
||||||
credentialsStore,
|
|
||||||
timelineUseCase(),
|
|
||||||
LocalIdFactory(),
|
|
||||||
imageMetaReader,
|
|
||||||
messageOptionsStore,
|
messageOptionsStore,
|
||||||
clock
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun timelineUseCase(): TimelineUseCaseImpl {
|
internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, roomId, chatEngine.mediaDecrypter())
|
||||||
val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper()))
|
|
||||||
return TimelineUseCaseImpl(syncService, messageService, roomService, mergeWithLocalEchosUseCase)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, base64, roomId)
|
|
||||||
}
|
}
|
@ -44,12 +44,12 @@ import app.dapk.st.core.StartObserving
|
|||||||
import app.dapk.st.core.components.CenteredLoading
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
import app.dapk.st.core.extensions.takeIfContent
|
import app.dapk.st.core.extensions.takeIfContent
|
||||||
import app.dapk.st.design.components.*
|
import app.dapk.st.design.components.*
|
||||||
|
import app.dapk.st.engine.MessageMeta
|
||||||
|
import app.dapk.st.engine.MessengerState
|
||||||
|
import app.dapk.st.engine.RoomEvent
|
||||||
|
import app.dapk.st.engine.RoomState
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.common.UserId
|
import app.dapk.st.matrix.common.UserId
|
||||||
import app.dapk.st.matrix.sync.MessageMeta
|
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.matrix.sync.RoomEvent.Message
|
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
|
||||||
import app.dapk.st.messenger.gallery.ImageGalleryActivityPayload
|
import app.dapk.st.messenger.gallery.ImageGalleryActivityPayload
|
||||||
import app.dapk.st.navigator.MessageAttachment
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
import app.dapk.st.navigator.Navigator
|
import app.dapk.st.navigator.Navigator
|
||||||
@ -196,7 +196,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions
|
|||||||
AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) {
|
AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
|
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
|
||||||
is Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
|
is RoomEvent.Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
|
||||||
is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>)
|
is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -482,7 +482,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
when (val replyingTo = content.message.replyingTo) {
|
when (val replyingTo = content.message.replyingTo) {
|
||||||
is Message -> {
|
is RoomEvent.Message -> {
|
||||||
Text(
|
Text(
|
||||||
text = replyingTo.content,
|
text = replyingTo.content,
|
||||||
color = content.textColor().copy(alpha = 0.8f),
|
color = content.textColor().copy(alpha = 0.8f),
|
||||||
@ -525,7 +525,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
when (val message = content.message.message) {
|
when (val message = content.message.message) {
|
||||||
is Message -> {
|
is RoomEvent.Message -> {
|
||||||
Text(
|
Text(
|
||||||
text = message.content,
|
text = message.content,
|
||||||
color = content.textColor(),
|
color = content.textColor(),
|
||||||
@ -642,7 +642,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
if (it is Message) {
|
if (it is RoomEvent.Message) {
|
||||||
Box(Modifier.padding(12.dp)) {
|
Box(Modifier.padding(12.dp)) {
|
||||||
Box(Modifier.padding(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) {
|
Box(Modifier.padding(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) {
|
||||||
Icon(
|
Icon(
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
package app.dapk.st.messenger
|
package app.dapk.st.messenger
|
||||||
|
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.engine.MessengerState
|
||||||
|
import app.dapk.st.engine.RoomEvent
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.navigator.MessageAttachment
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
|
|
||||||
data class MessengerScreenState(
|
data class MessengerScreenState(
|
||||||
|
@ -4,35 +4,22 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.core.extensions.takeIfContent
|
import app.dapk.st.core.extensions.takeIfContent
|
||||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.common.EventId
|
import app.dapk.st.engine.RoomEvent
|
||||||
|
import app.dapk.st.engine.SendMessage
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.common.UserId
|
|
||||||
import app.dapk.st.matrix.message.MessageService
|
|
||||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
|
||||||
import app.dapk.st.matrix.room.RoomService
|
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.matrix.sync.RoomStore
|
|
||||||
import app.dapk.st.navigator.MessageAttachment
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
import app.dapk.st.viewmodel.DapkViewModel
|
||||||
import app.dapk.st.viewmodel.MutableStateFactory
|
import app.dapk.st.viewmodel.MutableStateFactory
|
||||||
import app.dapk.st.viewmodel.defaultStateFactory
|
import app.dapk.st.viewmodel.defaultStateFactory
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import java.time.Clock
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
internal class MessengerViewModel(
|
internal class MessengerViewModel(
|
||||||
private val messageService: MessageService,
|
private val chatEngine: ChatEngine,
|
||||||
private val roomService: RoomService,
|
|
||||||
private val roomStore: RoomStore,
|
|
||||||
private val credentialsStore: CredentialsStore,
|
|
||||||
private val observeTimeline: ObserveTimelineUseCase,
|
|
||||||
private val localIdFactory: LocalIdFactory,
|
|
||||||
private val imageContentReader: ImageContentReader,
|
|
||||||
private val messageOptionsStore: MessageOptionsStore,
|
private val messageOptionsStore: MessageOptionsStore,
|
||||||
private val clock: Clock,
|
|
||||||
factory: MutableStateFactory<MessengerScreenState> = defaultStateFactory(),
|
factory: MutableStateFactory<MessengerScreenState> = defaultStateFactory(),
|
||||||
) : DapkViewModel<MessengerScreenState, MessengerEvent>(
|
) : DapkViewModel<MessengerScreenState, MessengerEvent>(
|
||||||
initialState = MessengerScreenState(
|
initialState = MessengerScreenState(
|
||||||
@ -83,31 +70,13 @@ internal class MessengerViewModel(
|
|||||||
|
|
||||||
private fun start(action: MessengerAction.OnMessengerVisible) {
|
private fun start(action: MessengerAction.OnMessengerVisible) {
|
||||||
updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) }
|
updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) }
|
||||||
syncJob = viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
roomStore.markRead(action.roomId)
|
syncJob = chatEngine.messages(action.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled())
|
||||||
|
.onEach { updateState { copy(roomState = Lce.Content(it)) } }
|
||||||
val credentials = credentialsStore.credentials()!!
|
.launchIn(this)
|
||||||
var lastKnownReadEvent: EventId? = null
|
|
||||||
observeTimeline.invoke(action.roomId, credentials.userId).distinctUntilChanged().onEach { state ->
|
|
||||||
state.latestMessageEventFromOthers(self = credentials.userId)?.let {
|
|
||||||
if (lastKnownReadEvent != it) {
|
|
||||||
updateRoomReadStateAsync(latestReadEvent = it, state)
|
|
||||||
lastKnownReadEvent = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateState { copy(roomState = Lce.Content(state)) }
|
|
||||||
}.collect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState): Deferred<Unit> {
|
|
||||||
return async {
|
|
||||||
runCatching {
|
|
||||||
roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = messageOptionsStore.isReadReceiptsDisabled())
|
|
||||||
roomStore.markRead(state.roomState.roomOverview.roomId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendMessage() {
|
private fun sendMessage() {
|
||||||
when (val composerState = state.composerState) {
|
when (val composerState = state.composerState) {
|
||||||
@ -118,27 +87,23 @@ internal class MessengerViewModel(
|
|||||||
state.roomState.takeIfContent()?.let { content ->
|
state.roomState.takeIfContent()?.let { content ->
|
||||||
val roomState = content.roomState
|
val roomState = content.roomState
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
messageService.scheduleMessage(
|
chatEngine.send(
|
||||||
MessageService.Message.TextMessage(
|
message = SendMessage.TextMessage(
|
||||||
MessageService.Message.Content.TextContent(body = copy.value),
|
content = copy.value,
|
||||||
roomId = roomState.roomOverview.roomId,
|
|
||||||
sendEncrypted = roomState.roomOverview.isEncrypted,
|
|
||||||
localId = localIdFactory.create(),
|
|
||||||
timestampUtc = clock.millis(),
|
|
||||||
reply = copy.reply?.let {
|
reply = copy.reply?.let {
|
||||||
MessageService.Message.TextMessage.Reply(
|
SendMessage.TextMessage.Reply(
|
||||||
author = it.author,
|
author = it.author,
|
||||||
originalMessage = when (it) {
|
originalMessage = when (it) {
|
||||||
is RoomEvent.Image -> TODO()
|
is RoomEvent.Image -> TODO()
|
||||||
is RoomEvent.Reply -> TODO()
|
is RoomEvent.Reply -> TODO()
|
||||||
is RoomEvent.Message -> it.content
|
is RoomEvent.Message -> it.content
|
||||||
},
|
},
|
||||||
replyContent = copy.value,
|
|
||||||
eventId = it.eventId,
|
eventId = it.eventId,
|
||||||
timestampUtc = it.utcTimestamp,
|
timestampUtc = it.utcTimestamp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
room = roomState.roomOverview,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,26 +116,7 @@ internal class MessengerViewModel(
|
|||||||
state.roomState.takeIfContent()?.let { content ->
|
state.roomState.takeIfContent()?.let { content ->
|
||||||
val roomState = content.roomState
|
val roomState = content.roomState
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val imageUri = copy.values.first().uri.value
|
chatEngine.send(SendMessage.ImageMessage(uri = copy.values.first().uri.value), roomState.roomOverview)
|
||||||
val meta = imageContentReader.meta(imageUri)
|
|
||||||
|
|
||||||
messageService.scheduleMessage(
|
|
||||||
MessageService.Message.ImageMessage(
|
|
||||||
MessageService.Message.Content.ImageContent(
|
|
||||||
uri = imageUri, MessageService.Message.Content.ImageContent.Meta(
|
|
||||||
height = meta.height,
|
|
||||||
width = meta.width,
|
|
||||||
size = meta.size,
|
|
||||||
fileName = meta.fileName,
|
|
||||||
mimeType = meta.mimeType,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
roomId = roomState.roomOverview.roomId,
|
|
||||||
sendEncrypted = roomState.roomOverview.isEncrypted,
|
|
||||||
localId = localIdFactory.create(),
|
|
||||||
timestampUtc = clock.millis(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,12 +136,6 @@ internal class MessengerViewModel(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events
|
|
||||||
.filterIsInstance<RoomEvent.Message>()
|
|
||||||
.filterNot { it.author.id == self }
|
|
||||||
.firstOrNull()
|
|
||||||
?.eventId
|
|
||||||
|
|
||||||
sealed interface MessengerAction {
|
sealed interface MessengerAction {
|
||||||
data class ComposerTextUpdate(val newValue: String) : MessengerAction
|
data class ComposerTextUpdate(val newValue: String) : MessengerAction
|
||||||
data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction
|
data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction
|
||||||
|
@ -2,34 +2,22 @@ package app.dapk.st.messenger
|
|||||||
|
|
||||||
import ViewModelTest
|
import ViewModelTest
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.core.extensions.takeIfContent
|
||||||
|
import app.dapk.st.engine.MessengerState
|
||||||
|
import app.dapk.st.engine.RoomState
|
||||||
|
import app.dapk.st.engine.SendMessage
|
||||||
import app.dapk.st.matrix.common.EventId
|
import app.dapk.st.matrix.common.EventId
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.common.UserId
|
import app.dapk.st.matrix.common.UserId
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import fake.FakeChatEngine
|
||||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
|
||||||
import app.dapk.st.matrix.room.RoomService
|
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
import fake.FakeCredentialsStore
|
|
||||||
import fake.FakeMessageOptionsStore
|
import fake.FakeMessageOptionsStore
|
||||||
import fake.FakeRoomStore
|
|
||||||
import fixture.*
|
import fixture.*
|
||||||
import internalfake.FakeLocalIdFactory
|
|
||||||
import io.mockk.coEvery
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import test.delegateReturn
|
|
||||||
import java.time.Clock
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
|
|
||||||
private const val A_CURRENT_TIMESTAMP = 10000L
|
|
||||||
private const val READ_RECEIPTS_ARE_DISABLED = true
|
private const val READ_RECEIPTS_ARE_DISABLED = true
|
||||||
private val A_ROOM_ID = aRoomId("messenger state room id")
|
private val A_ROOM_ID = aRoomId("messenger state room id")
|
||||||
private const val A_MESSAGE_CONTENT = "message content"
|
private const val A_MESSAGE_CONTENT = "message content"
|
||||||
private const val A_LOCAL_ID = "local.1111-2222-3333"
|
|
||||||
private val AN_EVENT_ID = anEventId("state event")
|
private val AN_EVENT_ID = anEventId("state event")
|
||||||
private val A_SELF_ID = aUserId("self")
|
private val A_SELF_ID = aUserId("self")
|
||||||
|
|
||||||
@ -37,23 +25,12 @@ class MessengerViewModelTest {
|
|||||||
|
|
||||||
private val runViewModelTest = ViewModelTest()
|
private val runViewModelTest = ViewModelTest()
|
||||||
|
|
||||||
private val fakeMessageService = FakeMessageService()
|
|
||||||
private val fakeRoomService = FakeRoomService()
|
|
||||||
private val fakeRoomStore = FakeRoomStore()
|
|
||||||
private val fakeCredentialsStore = FakeCredentialsStore().also { it.givenCredentials().returns(aUserCredentials(userId = A_SELF_ID)) }
|
|
||||||
private val fakeObserveTimelineUseCase = FakeObserveTimelineUseCase()
|
|
||||||
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
|
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
|
||||||
|
private val fakeChatEngine = FakeChatEngine()
|
||||||
|
|
||||||
private val viewModel = MessengerViewModel(
|
private val viewModel = MessengerViewModel(
|
||||||
fakeMessageService,
|
fakeChatEngine,
|
||||||
fakeRoomService,
|
fakeMessageOptionsStore.instance,
|
||||||
fakeRoomStore,
|
|
||||||
fakeCredentialsStore,
|
|
||||||
fakeObserveTimelineUseCase,
|
|
||||||
localIdFactory = FakeLocalIdFactory().also { it.givenCreate().returns(A_LOCAL_ID) }.instance,
|
|
||||||
imageContentReader = FakeImageContentReader(),
|
|
||||||
messageOptionsStore = fakeMessageOptionsStore.instance,
|
|
||||||
clock = fixedClock(A_CURRENT_TIMESTAMP),
|
|
||||||
factory = runViewModelTest.testMutableStateFactory(),
|
factory = runViewModelTest.testMutableStateFactory(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -73,10 +50,8 @@ class MessengerViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest {
|
fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest {
|
||||||
fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED)
|
fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED)
|
||||||
fakeRoomStore.expectUnit(times = 2) { it.markRead(A_ROOM_ID) }
|
|
||||||
fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, AN_EVENT_ID, isPrivate = READ_RECEIPTS_ARE_DISABLED) }
|
|
||||||
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
||||||
fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state))
|
fakeChatEngine.givenMessages(A_ROOM_ID, READ_RECEIPTS_ARE_DISABLED).returns(flowOf(state))
|
||||||
|
|
||||||
viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID, attachments = null))
|
viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID, attachments = null))
|
||||||
|
|
||||||
@ -98,9 +73,10 @@ class MessengerViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given composer message state when posting send text, then resets composer state and sends message`() = runViewModelTest {
|
fun `given composer message state when posting send text, then resets composer state and sends message`() = runViewModelTest {
|
||||||
fakeMessageService.expectUnit { it.scheduleMessage(expectEncryptedMessage(A_ROOM_ID, A_LOCAL_ID, A_CURRENT_TIMESTAMP, A_MESSAGE_CONTENT)) }
|
val initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT)
|
||||||
|
fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT), initialState.roomState.takeIfContent()!!.roomState.roomOverview) }
|
||||||
|
|
||||||
viewModel.test(initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT)).post(MessengerAction.ComposerSendText)
|
viewModel.test(initialState = initialState).post(MessengerAction.ComposerSendText)
|
||||||
|
|
||||||
assertStates<MessengerScreenState>({ copy(composerState = ComposerState.Text("", reply = null)) })
|
assertStates<MessengerScreenState>({ copy(composerState = ComposerState.Text("", reply = null)) })
|
||||||
verifyExpects()
|
verifyExpects()
|
||||||
@ -114,9 +90,8 @@ class MessengerViewModelTest {
|
|||||||
return aMessageScreenState(roomId, aMessengerState(roomState = roomState), messageContent)
|
return aMessageScreenState(roomId, aMessengerState(roomState = roomState), messageContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expectEncryptedMessage(roomId: RoomId, localId: String, timestamp: Long, messageContent: String): MessageService.Message.TextMessage {
|
private fun expectTextMessage(messageContent: String): SendMessage.TextMessage {
|
||||||
val content = MessageService.Message.Content.TextContent(body = messageContent)
|
return SendMessage.TextMessage(messageContent, reply = null)
|
||||||
return MessageService.Message.TextMessage(content, sendEncrypted = true, roomId, localId, timestamp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun aMessengerStateWithEvent(eventId: EventId, selfId: UserId) = aRoomStateWithEventId(eventId).toMessengerState(selfId)
|
private fun aMessengerStateWithEvent(eventId: EventId, selfId: UserId) = aRoomStateWithEventId(eventId).toMessengerState(selfId)
|
||||||
@ -135,27 +110,3 @@ fun aMessageScreenState(roomId: RoomId = aRoomId(), roomState: MessengerState, m
|
|||||||
roomState = Lce.Content(roomState),
|
roomState = Lce.Content(roomState),
|
||||||
composerState = ComposerState.Text(value = messageContent ?: "", reply = null)
|
composerState = ComposerState.Text(value = messageContent ?: "", reply = null)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aMessengerState(
|
|
||||||
self: UserId = aUserId(),
|
|
||||||
roomState: RoomState,
|
|
||||||
typing: SyncService.SyncEvent.Typing? = null
|
|
||||||
) = MessengerState(self, roomState, typing)
|
|
||||||
|
|
||||||
class FakeObserveTimelineUseCase : ObserveTimelineUseCase by mockk() {
|
|
||||||
fun given(roomId: RoomId, selfId: UserId) = coEvery { this@FakeObserveTimelineUseCase.invoke(roomId, selfId) }.delegateReturn()
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeMessageService : MessageService by mockk() {
|
|
||||||
|
|
||||||
fun givenEchos(roomId: RoomId) = every { localEchos(roomId) }.delegateReturn()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class FakeRoomService : RoomService by mockk() {
|
|
||||||
fun givenFindMember(roomId: RoomId, userId: UserId) = coEvery { findMember(roomId, userId) }.delegateReturn()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fixedClock(timestamp: Long = 0) = Clock.fixed(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC)
|
|
||||||
|
|
||||||
class FakeImageContentReader : ImageContentReader by mockk()
|
|
@ -4,5 +4,5 @@ apply plugin: 'kotlin-parcelize'
|
|||||||
dependencies {
|
dependencies {
|
||||||
compileOnly project(":domains:android:stub")
|
compileOnly project(":domains:android:stub")
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation project(":matrix:common")
|
implementation project(":chat-engine")
|
||||||
}
|
}
|
@ -1,8 +1,7 @@
|
|||||||
applyAndroidLibraryModule(project)
|
applyAndroidLibraryModule(project)
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":matrix:services:push")
|
implementation project(":chat-engine")
|
||||||
implementation project(":matrix:services:sync")
|
|
||||||
implementation project(':domains:store')
|
implementation project(':domains:store')
|
||||||
implementation project(":domains:android:work")
|
implementation project(":domains:android:work")
|
||||||
implementation project(':domains:android:push')
|
implementation project(':domains:android:push')
|
||||||
@ -12,12 +11,13 @@ dependencies {
|
|||||||
implementation project(":features:messenger")
|
implementation project(":features:messenger")
|
||||||
implementation project(":features:navigator")
|
implementation project(":features:navigator")
|
||||||
|
|
||||||
|
|
||||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||||
|
|
||||||
kotlinTest(it)
|
kotlinTest(it)
|
||||||
|
|
||||||
androidImportFixturesWorkaround(project, project(":core"))
|
androidImportFixturesWorkaround(project, project(":core"))
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
|
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||||
}
|
}
|
@ -4,9 +4,9 @@ import android.app.Notification
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.dapk.st.core.DeviceMeta
|
import app.dapk.st.core.DeviceMeta
|
||||||
import app.dapk.st.core.whenPOrHigher
|
import app.dapk.st.core.whenPOrHigher
|
||||||
|
import app.dapk.st.engine.RoomOverview
|
||||||
import app.dapk.st.imageloader.IconLoader
|
import app.dapk.st.imageloader.IconLoader
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import app.dapk.st.navigator.IntentFactory
|
import app.dapk.st.navigator.IntentFactory
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ class NotificationFactory(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createInvite(inviteNotification: InviteNotification): AndroidNotification {
|
fun createInvite(inviteNotification: app.dapk.st.engine.InviteNotification): AndroidNotification {
|
||||||
val openAppIntent = intentFactory.notificationOpenApp(context)
|
val openAppIntent = intentFactory.notificationOpenApp(context)
|
||||||
return AndroidNotification(
|
return AndroidNotification(
|
||||||
channelId = INVITE_CHANNEL_ID,
|
channelId = INVITE_CHANNEL_ID,
|
||||||
|
@ -10,7 +10,7 @@ class NotificationInviteRenderer(
|
|||||||
private val androidNotificationBuilder: AndroidNotificationBuilder,
|
private val androidNotificationBuilder: AndroidNotificationBuilder,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun render(inviteNotification: InviteNotification) {
|
fun render(inviteNotification: app.dapk.st.engine.InviteNotification) {
|
||||||
notificationManager.notify(
|
notificationManager.notify(
|
||||||
inviteNotification.roomId.value,
|
inviteNotification.roomId.value,
|
||||||
INVITE_NOTIFICATION_ID,
|
INVITE_NOTIFICATION_ID,
|
||||||
@ -18,7 +18,7 @@ class NotificationInviteRenderer(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun InviteNotification.toAndroidNotification() = androidNotificationBuilder.build(
|
private fun app.dapk.st.engine.InviteNotification.toAndroidNotification() = androidNotificationBuilder.build(
|
||||||
notificationFactory.createInvite(this)
|
notificationFactory.createInvite(this)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,9 +5,9 @@ import app.dapk.st.core.AppLogTag
|
|||||||
import app.dapk.st.core.CoroutineDispatchers
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
import app.dapk.st.core.extensions.ifNull
|
import app.dapk.st.core.extensions.ifNull
|
||||||
import app.dapk.st.core.log
|
import app.dapk.st.core.log
|
||||||
|
import app.dapk.st.engine.RoomEvent
|
||||||
|
import app.dapk.st.engine.RoomOverview
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
private const val SUMMARY_NOTIFICATION_ID = 101
|
private const val SUMMARY_NOTIFICATION_ID = 101
|
||||||
|
@ -3,8 +3,8 @@ package app.dapk.st.notifications
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import app.dapk.st.core.DeviceMeta
|
import app.dapk.st.core.DeviceMeta
|
||||||
import app.dapk.st.core.whenPOrHigher
|
import app.dapk.st.core.whenPOrHigher
|
||||||
|
import app.dapk.st.engine.RoomOverview
|
||||||
import app.dapk.st.imageloader.IconLoader
|
import app.dapk.st.imageloader.IconLoader
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import app.dapk.st.notifications.AndroidNotificationStyle.Inbox
|
import app.dapk.st.notifications.AndroidNotificationStyle.Inbox
|
||||||
import app.dapk.st.notifications.AndroidNotificationStyle.Messaging
|
import app.dapk.st.notifications.AndroidNotificationStyle.Messaging
|
||||||
|
|
||||||
|
@ -5,16 +5,14 @@ import android.content.Context
|
|||||||
import app.dapk.st.core.CoroutineDispatchers
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
import app.dapk.st.core.DeviceMeta
|
import app.dapk.st.core.DeviceMeta
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.imageloader.IconLoader
|
import app.dapk.st.imageloader.IconLoader
|
||||||
import app.dapk.st.matrix.sync.OverviewStore
|
|
||||||
import app.dapk.st.matrix.sync.RoomStore
|
|
||||||
import app.dapk.st.navigator.IntentFactory
|
import app.dapk.st.navigator.IntentFactory
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
|
|
||||||
class NotificationsModule(
|
class NotificationsModule(
|
||||||
|
private val chatEngine: ChatEngine,
|
||||||
private val iconLoader: IconLoader,
|
private val iconLoader: IconLoader,
|
||||||
private val roomStore: RoomStore,
|
|
||||||
private val overviewStore: OverviewStore,
|
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val intentFactory: IntentFactory,
|
private val intentFactory: IntentFactory,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
@ -40,10 +38,9 @@ class NotificationsModule(
|
|||||||
)
|
)
|
||||||
return RenderNotificationsUseCase(
|
return RenderNotificationsUseCase(
|
||||||
notificationRenderer = notificationMessageRenderer,
|
notificationRenderer = notificationMessageRenderer,
|
||||||
observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore),
|
|
||||||
notificationChannels = NotificationChannels(notificationManager),
|
notificationChannels = NotificationChannels(notificationManager),
|
||||||
observeInviteNotificationsUseCase = ObserveInviteNotificationsUseCaseImpl(overviewStore),
|
inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder),
|
||||||
inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder)
|
chatEngine = chatEngine,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package app.dapk.st.notifications
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
import app.dapk.st.engine.NotificationDiff
|
||||||
|
import app.dapk.st.engine.RoomEvent
|
||||||
|
import app.dapk.st.engine.RoomOverview
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -9,18 +11,17 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
class RenderNotificationsUseCase(
|
class RenderNotificationsUseCase(
|
||||||
private val notificationRenderer: NotificationMessageRenderer,
|
private val notificationRenderer: NotificationMessageRenderer,
|
||||||
private val inviteRenderer: NotificationInviteRenderer,
|
private val inviteRenderer: NotificationInviteRenderer,
|
||||||
private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase,
|
private val chatEngine: ChatEngine,
|
||||||
private val observeInviteNotificationsUseCase: ObserveInviteNotificationsUseCase,
|
|
||||||
private val notificationChannels: NotificationChannels,
|
private val notificationChannels: NotificationChannels,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun listenForNotificationChanges(scope: CoroutineScope) {
|
suspend fun listenForNotificationChanges(scope: CoroutineScope) {
|
||||||
notificationChannels.initChannels()
|
notificationChannels.initChannels()
|
||||||
observeRenderableUnreadEventsUseCase()
|
chatEngine.notificationsMessages()
|
||||||
.onEach { (each, diff) -> renderUnreadChange(each, diff) }
|
.onEach { (each, diff) -> renderUnreadChange(each, diff) }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
|
|
||||||
observeInviteNotificationsUseCase()
|
chatEngine.notificationsInvites()
|
||||||
.onEach { inviteRenderer.render(it) }
|
.onEach { inviteRenderer.render(it) }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package app.dapk.st.notifications
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import app.dapk.st.engine.RoomEvent
|
||||||
import app.dapk.st.matrix.common.RoomMember
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
|
|
||||||
class RoomEventsToNotifiableMapper {
|
class RoomEventsToNotifiableMapper {
|
||||||
|
|
||||||
|
@ -4,8 +4,8 @@ import android.app.Notification
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import app.dapk.st.core.DeviceMeta
|
import app.dapk.st.core.DeviceMeta
|
||||||
|
import app.dapk.st.engine.RoomOverview
|
||||||
import app.dapk.st.matrix.common.AvatarUrl
|
import app.dapk.st.matrix.common.AvatarUrl
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import fake.FakeContext
|
import fake.FakeContext
|
||||||
import fixture.NotificationDelegateFixtures.anAndroidNotification
|
import fixture.NotificationDelegateFixtures.anAndroidNotification
|
||||||
import fixture.NotificationDelegateFixtures.anInboxStyle
|
import fixture.NotificationDelegateFixtures.anInboxStyle
|
||||||
@ -137,7 +137,7 @@ class NotificationFactoryTest {
|
|||||||
fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT)
|
fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT)
|
||||||
val content = "Content message"
|
val content = "Content message"
|
||||||
val result = notificationFactory.createInvite(
|
val result = notificationFactory.createInvite(
|
||||||
InviteNotification(
|
app.dapk.st.engine.InviteNotification(
|
||||||
content = content,
|
content = content,
|
||||||
A_ROOM_ID,
|
A_ROOM_ID,
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package app.dapk.st.notifications
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import app.dapk.st.engine.RoomEvent
|
||||||
|
import app.dapk.st.engine.RoomOverview
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import fake.FakeNotificationFactory
|
import fake.FakeNotificationFactory
|
||||||
import fake.FakeNotificationManager
|
import fake.FakeNotificationManager
|
||||||
import fake.aFakeNotification
|
import fake.aFakeNotification
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package app.dapk.st.notifications
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import app.dapk.st.engine.RoomEvent
|
||||||
|
import app.dapk.st.engine.RoomOverview
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import app.dapk.st.navigator.IntentFactory
|
import app.dapk.st.navigator.IntentFactory
|
||||||
import fixture.NotificationDelegateFixtures.anAndroidNotification
|
import fixture.NotificationDelegateFixtures.anAndroidNotification
|
||||||
import fixture.NotificationFixtures.aDismissRoomNotification
|
import fixture.NotificationFixtures.aDismissRoomNotification
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package app.dapk.st.notifications
|
package app.dapk.st.notifications
|
||||||
|
|
||||||
|
import app.dapk.st.engine.UnreadNotifications
|
||||||
import fake.*
|
import fake.*
|
||||||
import fixture.NotificationDiffFixtures.aNotificationDiff
|
import fixture.NotificationDiffFixtures.aNotificationDiff
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
@ -14,25 +15,23 @@ class RenderNotificationsUseCaseTest {
|
|||||||
|
|
||||||
private val fakeNotificationMessageRenderer = FakeNotificationMessageRenderer()
|
private val fakeNotificationMessageRenderer = FakeNotificationMessageRenderer()
|
||||||
private val fakeNotificationInviteRenderer = FakeNotificationInviteRenderer()
|
private val fakeNotificationInviteRenderer = FakeNotificationInviteRenderer()
|
||||||
private val fakeObserveUnreadNotificationsUseCase = FakeObserveUnreadNotificationsUseCase()
|
|
||||||
private val fakeObserveInviteNotificationsUseCase = FakeObserveInviteNotificationsUseCase()
|
|
||||||
private val fakeNotificationChannels = FakeNotificationChannels().also {
|
private val fakeNotificationChannels = FakeNotificationChannels().also {
|
||||||
it.instance.expect { it.initChannels() }
|
it.instance.expect { it.initChannels() }
|
||||||
}
|
}
|
||||||
|
private val fakeChatEngine = FakeChatEngine()
|
||||||
|
|
||||||
private val renderNotificationsUseCase = RenderNotificationsUseCase(
|
private val renderNotificationsUseCase = RenderNotificationsUseCase(
|
||||||
fakeNotificationMessageRenderer.instance,
|
fakeNotificationMessageRenderer.instance,
|
||||||
fakeNotificationInviteRenderer.instance,
|
fakeNotificationInviteRenderer.instance,
|
||||||
fakeObserveUnreadNotificationsUseCase,
|
fakeChatEngine,
|
||||||
fakeObserveInviteNotificationsUseCase,
|
|
||||||
fakeNotificationChannels.instance,
|
fakeNotificationChannels.instance,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given events, when listening for changes then initiates channels once`() = runTest {
|
fun `given events, when listening for changes then initiates channels once`() = runTest {
|
||||||
fakeNotificationMessageRenderer.instance.expect { it.render(any()) }
|
fakeNotificationMessageRenderer.instance.expect { it.render(any()) }
|
||||||
fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS)
|
fakeChatEngine.givenNotificationsMessages().emits(AN_UNREAD_NOTIFICATIONS)
|
||||||
fakeObserveInviteNotificationsUseCase.given().emits()
|
fakeChatEngine.givenNotificationsInvites().emits()
|
||||||
|
|
||||||
renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher()))
|
renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher()))
|
||||||
|
|
||||||
@ -42,8 +41,8 @@ class RenderNotificationsUseCaseTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `given renderable unread events, when listening for changes, then renders change`() = runTest {
|
fun `given renderable unread events, when listening for changes, then renders change`() = runTest {
|
||||||
fakeNotificationMessageRenderer.instance.expect { it.render(any()) }
|
fakeNotificationMessageRenderer.instance.expect { it.render(any()) }
|
||||||
fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS)
|
fakeChatEngine.givenNotificationsMessages().emits(AN_UNREAD_NOTIFICATIONS)
|
||||||
fakeObserveInviteNotificationsUseCase.given().emits()
|
fakeChatEngine.givenNotificationsInvites().emits()
|
||||||
|
|
||||||
renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher()))
|
renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher()))
|
||||||
|
|
||||||
|
@ -2,14 +2,14 @@ package fake
|
|||||||
|
|
||||||
import app.dapk.st.notifications.NotificationMessageRenderer
|
import app.dapk.st.notifications.NotificationMessageRenderer
|
||||||
import app.dapk.st.notifications.NotificationState
|
import app.dapk.st.notifications.NotificationState
|
||||||
import app.dapk.st.notifications.UnreadNotifications
|
import app.dapk.st.engine.UnreadNotifications
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
|
||||||
class FakeNotificationMessageRenderer {
|
class FakeNotificationMessageRenderer {
|
||||||
val instance = mockk<NotificationMessageRenderer>()
|
val instance = mockk<NotificationMessageRenderer>()
|
||||||
|
|
||||||
fun verifyRenders(vararg unreadNotifications: UnreadNotifications) {
|
fun verifyRenders(vararg unreadNotifications: app.dapk.st.engine.UnreadNotifications) {
|
||||||
unreadNotifications.forEach { unread ->
|
unreadNotifications.forEach { unread ->
|
||||||
coVerify {
|
coVerify {
|
||||||
instance.render(
|
instance.render(
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
applyAndroidComposeLibraryModule(project)
|
applyAndroidComposeLibraryModule(project)
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":matrix:services:sync")
|
implementation project(":chat-engine")
|
||||||
implementation project(":matrix:services:room")
|
|
||||||
implementation project(":matrix:services:profile")
|
|
||||||
implementation project(":features:settings")
|
implementation project(":features:settings")
|
||||||
implementation project(':domains:store')
|
implementation project(':domains:store')
|
||||||
implementation project(":domains:android:compose-core")
|
implementation project(":domains:android:compose-core")
|
||||||
|
@ -2,19 +2,15 @@ package app.dapk.st.profile
|
|||||||
|
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.room.RoomService
|
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
|
|
||||||
class ProfileModule(
|
class ProfileModule(
|
||||||
private val profileService: ProfileService,
|
private val chatEngine: ChatEngine,
|
||||||
private val syncService: SyncService,
|
|
||||||
private val roomService: RoomService,
|
|
||||||
private val errorTracker: ErrorTracker,
|
private val errorTracker: ErrorTracker,
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun profileViewModel(): ProfileViewModel {
|
fun profileViewModel(): ProfileViewModel {
|
||||||
return ProfileViewModel(profileService, syncService, roomService, errorTracker)
|
return ProfileViewModel(chatEngine, errorTracker)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -21,8 +21,8 @@ import app.dapk.st.core.Lce
|
|||||||
import app.dapk.st.core.LifecycleEffect
|
import app.dapk.st.core.LifecycleEffect
|
||||||
import app.dapk.st.core.components.CenteredLoading
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
import app.dapk.st.design.components.*
|
import app.dapk.st.design.components.*
|
||||||
import app.dapk.st.matrix.sync.InviteMeta
|
import app.dapk.st.engine.RoomInvite
|
||||||
import app.dapk.st.matrix.sync.RoomInvite
|
import app.dapk.st.engine.RoomInvite.InviteMeta
|
||||||
import app.dapk.st.settings.SettingsActivity
|
import app.dapk.st.settings.SettingsActivity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -3,9 +3,8 @@ package app.dapk.st.profile
|
|||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.design.components.Route
|
import app.dapk.st.design.components.Route
|
||||||
import app.dapk.st.design.components.SpiderPage
|
import app.dapk.st.design.components.SpiderPage
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.engine.Me
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
import app.dapk.st.engine.RoomInvite
|
||||||
import app.dapk.st.matrix.sync.RoomInvite
|
|
||||||
|
|
||||||
data class ProfileScreenState(
|
data class ProfileScreenState(
|
||||||
val page: SpiderPage<out Page>,
|
val page: SpiderPage<out Page>,
|
||||||
@ -14,12 +13,12 @@ data class ProfileScreenState(
|
|||||||
sealed interface Page {
|
sealed interface Page {
|
||||||
data class Profile(val content: Lce<Content>) : Page {
|
data class Profile(val content: Lce<Content>) : Page {
|
||||||
data class Content(
|
data class Content(
|
||||||
val me: ProfileService.Me,
|
val me: Me,
|
||||||
val invitationsCount: Int,
|
val invitationsCount: Int,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Invitations(val content: Lce<List<RoomInvite>>): Page
|
data class Invitations(val content: Lce<List<RoomInvite>>) : Page
|
||||||
|
|
||||||
object Routes {
|
object Routes {
|
||||||
val profile = Route<Profile>("Profile")
|
val profile = Route<Profile>("Profile")
|
||||||
|
@ -5,25 +5,20 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
import app.dapk.st.design.components.SpiderPage
|
import app.dapk.st.design.components.SpiderPage
|
||||||
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.room.ProfileService
|
|
||||||
import app.dapk.st.matrix.room.RoomService
|
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
import app.dapk.st.viewmodel.DapkViewModel
|
import app.dapk.st.viewmodel.DapkViewModel
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ProfileViewModel(
|
class ProfileViewModel(
|
||||||
private val profileService: ProfileService,
|
private val chatEngine: ChatEngine,
|
||||||
private val syncService: SyncService,
|
|
||||||
private val roomService: RoomService,
|
|
||||||
private val errorTracker: ErrorTracker,
|
private val errorTracker: ErrorTracker,
|
||||||
) : DapkViewModel<ProfileScreenState, ProfileEvent>(
|
) : DapkViewModel<ProfileScreenState, ProfileEvent>(
|
||||||
ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false))
|
ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false))
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var syncingJob: Job? = null
|
|
||||||
private var currentPageJob: Job? = null
|
private var currentPageJob: Job? = null
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
@ -31,15 +26,13 @@ class ProfileViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun goToProfile() {
|
private fun goToProfile() {
|
||||||
syncingJob = syncService.startSyncing().launchIn(viewModelScope)
|
|
||||||
|
|
||||||
combine(
|
combine(
|
||||||
flow {
|
flow {
|
||||||
val result = runCatching { profileService.me(forceRefresh = true) }
|
val result = runCatching { chatEngine.me(forceRefresh = true) }
|
||||||
.onFailure { errorTracker.track(it, "Loading profile") }
|
.onFailure { errorTracker.track(it, "Loading profile") }
|
||||||
emit(result)
|
emit(result)
|
||||||
},
|
},
|
||||||
syncService.invites(),
|
chatEngine.invites(),
|
||||||
transform = { me, invites -> me to invites }
|
transform = { me, invites -> me to invites }
|
||||||
)
|
)
|
||||||
.onEach { (me, invites) ->
|
.onEach { (me, invites) ->
|
||||||
@ -57,7 +50,7 @@ class ProfileViewModel(
|
|||||||
fun goToInvitations() {
|
fun goToInvitations() {
|
||||||
updateState { copy(page = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))) }
|
updateState { copy(page = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))) }
|
||||||
|
|
||||||
syncService.invites()
|
chatEngine.invites()
|
||||||
.onEach {
|
.onEach {
|
||||||
updatePageState<Page.Invitations> {
|
updatePageState<Page.Invitations> {
|
||||||
copy(content = Lce.Content(it))
|
copy(content = Lce.Content(it))
|
||||||
@ -89,13 +82,13 @@ class ProfileViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun acceptRoomInvite(roomId: RoomId) {
|
fun acceptRoomInvite(roomId: RoomId) {
|
||||||
launchCatching { roomService.joinRoom(roomId) }.fold(
|
launchCatching { chatEngine.joinRoom(roomId) }.fold(
|
||||||
onError = {}
|
onError = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rejectRoomInvite(roomId: RoomId) {
|
fun rejectRoomInvite(roomId: RoomId) {
|
||||||
launchCatching { roomService.rejectJoinRoom(roomId) }.fold(
|
launchCatching { chatEngine.rejectJoinRoom(roomId) }.fold(
|
||||||
onError = {
|
onError = {
|
||||||
Log.e("!!!", it.message, it)
|
Log.e("!!!", it.message, it)
|
||||||
}
|
}
|
||||||
@ -115,7 +108,7 @@ class ProfileViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
syncingJob?.cancel()
|
currentPageJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
applyAndroidComposeLibraryModule(project)
|
applyAndroidComposeLibraryModule(project)
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":matrix:services:sync")
|
implementation project(":chat-engine")
|
||||||
implementation project(":matrix:services:crypto")
|
|
||||||
implementation project(":features:navigator")
|
implementation project(":features:navigator")
|
||||||
implementation project(':domains:store')
|
implementation project(':domains:store')
|
||||||
implementation project(':domains:android:push')
|
implementation project(':domains:android:push')
|
||||||
@ -13,11 +12,10 @@ dependencies {
|
|||||||
|
|
||||||
kotlinTest(it)
|
kotlinTest(it)
|
||||||
|
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
|
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:services:crypto"))
|
|
||||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||||
androidImportFixturesWorkaround(project, project(":core"))
|
androidImportFixturesWorkaround(project, project(":core"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||||
|
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||||
}
|
}
|
@ -5,16 +5,14 @@ import app.dapk.st.core.*
|
|||||||
import app.dapk.st.domain.StoreModule
|
import app.dapk.st.domain.StoreModule
|
||||||
import app.dapk.st.domain.application.eventlog.LoggingStore
|
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.matrix.crypto.CryptoService
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
import app.dapk.st.push.PushModule
|
import app.dapk.st.push.PushModule
|
||||||
import app.dapk.st.settings.eventlogger.EventLoggerViewModel
|
import app.dapk.st.settings.eventlogger.EventLoggerViewModel
|
||||||
|
|
||||||
class SettingsModule(
|
class SettingsModule(
|
||||||
|
private val chatEngine: ChatEngine,
|
||||||
private val storeModule: StoreModule,
|
private val storeModule: StoreModule,
|
||||||
private val pushModule: PushModule,
|
private val pushModule: PushModule,
|
||||||
private val cryptoService: CryptoService,
|
|
||||||
private val syncService: SyncService,
|
|
||||||
private val contentResolver: ContentResolver,
|
private val contentResolver: ContentResolver,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val deviceMeta: DeviceMeta,
|
private val deviceMeta: DeviceMeta,
|
||||||
@ -26,10 +24,9 @@ class SettingsModule(
|
|||||||
|
|
||||||
internal fun settingsViewModel(): SettingsViewModel {
|
internal fun settingsViewModel(): SettingsViewModel {
|
||||||
return SettingsViewModel(
|
return SettingsViewModel(
|
||||||
|
chatEngine,
|
||||||
storeModule.cacheCleaner(),
|
storeModule.cacheCleaner(),
|
||||||
contentResolver,
|
contentResolver,
|
||||||
cryptoService,
|
|
||||||
syncService,
|
|
||||||
UriFilenameResolver(contentResolver, coroutineDispatchers),
|
UriFilenameResolver(contentResolver, coroutineDispatchers),
|
||||||
SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore),
|
SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore),
|
||||||
pushModule.pushTokenRegistrars(),
|
pushModule.pushTokenRegistrars(),
|
||||||
|
@ -42,7 +42,7 @@ import app.dapk.st.core.components.Header
|
|||||||
import app.dapk.st.core.extensions.takeAs
|
import app.dapk.st.core.extensions.takeAs
|
||||||
import app.dapk.st.core.getActivity
|
import app.dapk.st.core.getActivity
|
||||||
import app.dapk.st.design.components.*
|
import app.dapk.st.design.components.*
|
||||||
import app.dapk.st.matrix.crypto.ImportResult
|
import app.dapk.st.engine.ImportResult
|
||||||
import app.dapk.st.navigator.Navigator
|
import app.dapk.st.navigator.Navigator
|
||||||
import app.dapk.st.settings.SettingsEvent.*
|
import app.dapk.st.settings.SettingsEvent.*
|
||||||
import app.dapk.st.settings.eventlogger.EventLogActivity
|
import app.dapk.st.settings.eventlogger.EventLogActivity
|
||||||
|
@ -2,10 +2,9 @@ package app.dapk.st.settings
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.core.LceWithProgress
|
|
||||||
import app.dapk.st.design.components.Route
|
import app.dapk.st.design.components.Route
|
||||||
import app.dapk.st.design.components.SpiderPage
|
import app.dapk.st.design.components.SpiderPage
|
||||||
import app.dapk.st.matrix.crypto.ImportResult
|
import app.dapk.st.engine.ImportResult
|
||||||
import app.dapk.st.push.Registrar
|
import app.dapk.st.push.Registrar
|
||||||
|
|
||||||
internal data class SettingsScreenState(
|
internal data class SettingsScreenState(
|
||||||
|
@ -9,9 +9,8 @@ import app.dapk.st.design.components.SpiderPage
|
|||||||
import app.dapk.st.domain.StoreCleaner
|
import app.dapk.st.domain.StoreCleaner
|
||||||
import app.dapk.st.domain.application.eventlog.LoggingStore
|
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||||
import app.dapk.st.matrix.crypto.CryptoService
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.crypto.ImportResult
|
import app.dapk.st.engine.ImportResult
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
import app.dapk.st.push.PushTokenRegistrars
|
import app.dapk.st.push.PushTokenRegistrars
|
||||||
import app.dapk.st.push.Registrar
|
import app.dapk.st.push.Registrar
|
||||||
import app.dapk.st.settings.SettingItem.Id.*
|
import app.dapk.st.settings.SettingItem.Id.*
|
||||||
@ -26,10 +25,9 @@ import kotlinx.coroutines.launch
|
|||||||
private const val PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/"
|
private const val PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/"
|
||||||
|
|
||||||
internal class SettingsViewModel(
|
internal class SettingsViewModel(
|
||||||
|
private val chatEngine: ChatEngine,
|
||||||
private val cacheCleaner: StoreCleaner,
|
private val cacheCleaner: StoreCleaner,
|
||||||
private val contentResolver: ContentResolver,
|
private val contentResolver: ContentResolver,
|
||||||
private val cryptoService: CryptoService,
|
|
||||||
private val syncService: SyncService,
|
|
||||||
private val uriFilenameResolver: UriFilenameResolver,
|
private val uriFilenameResolver: UriFilenameResolver,
|
||||||
private val settingsItemFactory: SettingsItemFactory,
|
private val settingsItemFactory: SettingsItemFactory,
|
||||||
private val pushTokenRegistrars: PushTokenRegistrars,
|
private val pushTokenRegistrars: PushTokenRegistrars,
|
||||||
@ -142,26 +140,13 @@ internal class SettingsViewModel(
|
|||||||
fun importFromFileKeys(file: Uri, passphrase: String) {
|
fun importFromFileKeys(file: Uri, passphrase: String) {
|
||||||
updatePageState<Page.ImportRoomKey> { copy(importProgress = ImportResult.Update(0)) }
|
updatePageState<Page.ImportRoomKey> { copy(importProgress = ImportResult.Update(0)) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
with(cryptoService) {
|
with(chatEngine) {
|
||||||
runCatching { contentResolver.openInputStream(file)!! }
|
runCatching { contentResolver.openInputStream(file)!! }
|
||||||
.fold(
|
.fold(
|
||||||
onSuccess = { fileStream ->
|
onSuccess = { fileStream ->
|
||||||
fileStream.importRoomKeys(passphrase)
|
fileStream.importRoomKeys(passphrase)
|
||||||
.onEach {
|
.onEach {
|
||||||
updatePageState<Page.ImportRoomKey> { copy(importProgress = it) }
|
updatePageState<Page.ImportRoomKey> { copy(importProgress = it) }
|
||||||
when (it) {
|
|
||||||
is ImportResult.Error -> {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
is ImportResult.Update -> {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
is ImportResult.Success -> {
|
|
||||||
syncService.forceManualRefresh(it.roomIds.toList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
},
|
},
|
||||||
|
@ -3,9 +3,8 @@ package app.dapk.st.settings
|
|||||||
import ViewModelTest
|
import ViewModelTest
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.design.components.SpiderPage
|
import app.dapk.st.design.components.SpiderPage
|
||||||
import app.dapk.st.matrix.crypto.ImportResult
|
import app.dapk.st.engine.ImportResult
|
||||||
import fake.*
|
import fake.*
|
||||||
import fake.FakeStoreCleaner
|
|
||||||
import fixture.aRoomId
|
import fixture.aRoomId
|
||||||
import internalfake.FakeSettingsItemFactory
|
import internalfake.FakeSettingsItemFactory
|
||||||
import internalfake.FakeUriFilenameResolver
|
import internalfake.FakeUriFilenameResolver
|
||||||
@ -35,20 +34,18 @@ internal class SettingsViewModelTest {
|
|||||||
|
|
||||||
private val fakeStoreCleaner = FakeStoreCleaner()
|
private val fakeStoreCleaner = FakeStoreCleaner()
|
||||||
private val fakeContentResolver = FakeContentResolver()
|
private val fakeContentResolver = FakeContentResolver()
|
||||||
private val fakeCryptoService = FakeCryptoService()
|
|
||||||
private val fakeSyncService = FakeSyncService()
|
|
||||||
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
|
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
|
||||||
private val fakePushTokenRegistrars = FakePushRegistrars()
|
private val fakePushTokenRegistrars = FakePushRegistrars()
|
||||||
private val fakeSettingsItemFactory = FakeSettingsItemFactory()
|
private val fakeSettingsItemFactory = FakeSettingsItemFactory()
|
||||||
private val fakeThemeStore = FakeThemeStore()
|
private val fakeThemeStore = FakeThemeStore()
|
||||||
private val fakeLoggingStore = FakeLoggingStore()
|
private val fakeLoggingStore = FakeLoggingStore()
|
||||||
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
|
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
|
||||||
|
private val fakeChatEngine = FakeChatEngine()
|
||||||
|
|
||||||
private val viewModel = SettingsViewModel(
|
private val viewModel = SettingsViewModel(
|
||||||
|
fakeChatEngine,
|
||||||
fakeStoreCleaner,
|
fakeStoreCleaner,
|
||||||
fakeContentResolver.instance,
|
fakeContentResolver.instance,
|
||||||
fakeCryptoService,
|
|
||||||
fakeSyncService,
|
|
||||||
fakeUriFilenameResolver.instance,
|
fakeUriFilenameResolver.instance,
|
||||||
fakeSettingsItemFactory.instance,
|
fakeSettingsItemFactory.instance,
|
||||||
fakePushTokenRegistrars.instance,
|
fakePushTokenRegistrars.instance,
|
||||||
@ -174,9 +171,8 @@ internal class SettingsViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given success when importing room keys, then emits progress`() = runViewModelTest {
|
fun `given success when importing room keys, then emits progress`() = runViewModelTest {
|
||||||
fakeSyncService.expectUnit { it.forceManualRefresh(A_LIST_OF_ROOM_IDS) }
|
|
||||||
fakeContentResolver.givenFile(A_URI.instance).returns(AN_INPUT_STREAM.instance)
|
fakeContentResolver.givenFile(A_URI.instance).returns(AN_INPUT_STREAM.instance)
|
||||||
fakeCryptoService.givenImportKeys(AN_INPUT_STREAM.instance, A_PASSPHRASE).returns(flowOf(AN_IMPORT_SUCCESS))
|
fakeChatEngine.givenImportKeys(AN_INPUT_STREAM.instance, A_PASSPHRASE).returns(flowOf(AN_IMPORT_SUCCESS))
|
||||||
|
|
||||||
viewModel
|
viewModel
|
||||||
.test(initialState = SettingsScreenState(A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION))
|
.test(initialState = SettingsScreenState(A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION))
|
||||||
|
@ -4,9 +4,7 @@ dependencies {
|
|||||||
implementation project(":domains:android:compose-core")
|
implementation project(":domains:android:compose-core")
|
||||||
implementation project(":domains:android:viewmodel")
|
implementation project(":domains:android:viewmodel")
|
||||||
implementation project(':domains:store')
|
implementation project(':domains:store')
|
||||||
implementation project(':matrix:services:sync')
|
implementation project(':chat-engine')
|
||||||
implementation project(':matrix:services:room')
|
|
||||||
implementation project(':matrix:services:message')
|
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation project(":design-library")
|
implementation project(":design-library")
|
||||||
implementation project(":features:navigator")
|
implementation project(":features:navigator")
|
||||||
|
@ -1,21 +1,20 @@
|
|||||||
package app.dapk.st.share
|
package app.dapk.st.share
|
||||||
|
|
||||||
import app.dapk.st.matrix.room.RoomService
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
class FetchRoomsUseCase(
|
class FetchRoomsUseCase(
|
||||||
private val syncSyncService: SyncService,
|
private val chatEngine: ChatEngine,
|
||||||
private val roomService: RoomService,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun bar(): List<Item> {
|
suspend fun fetch(): List<Item> {
|
||||||
return syncSyncService.overview().first().map {
|
return chatEngine.directory().first().map {
|
||||||
|
val overview = it.overview
|
||||||
Item(
|
Item(
|
||||||
it.roomId,
|
overview.roomId,
|
||||||
it.roomAvatarUrl,
|
overview.roomAvatarUrl,
|
||||||
it.roomName ?: "",
|
overview.roomName ?: "",
|
||||||
roomService.findMembersSummary(it.roomId).map { it.displayName ?: it.id.value }
|
chatEngine.findMembersSummary(overview.roomId).map { it.displayName ?: it.id.value }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
package app.dapk.st.share
|
package app.dapk.st.share
|
||||||
|
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
import app.dapk.st.matrix.room.RoomService
|
import app.dapk.st.engine.ChatEngine
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
|
||||||
|
|
||||||
class ShareEntryModule(
|
class ShareEntryModule(
|
||||||
private val syncService: SyncService,
|
private val chatEngine: ChatEngine,
|
||||||
private val roomService: RoomService,
|
|
||||||
) : ProvidableModule {
|
) : ProvidableModule {
|
||||||
|
|
||||||
fun shareEntryViewModel(): ShareEntryViewModel {
|
fun shareEntryViewModel(): ShareEntryViewModel {
|
||||||
return ShareEntryViewModel(FetchRoomsUseCase(syncService, roomService))
|
return ShareEntryViewModel(FetchRoomsUseCase(chatEngine))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,7 +22,7 @@ class ShareEntryViewModel(
|
|||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
syncJob = viewModelScope.launch {
|
syncJob = viewModelScope.launch {
|
||||||
state = DirectoryScreenState.Content(fetchRoomsUseCase.bar())
|
state = DirectoryScreenState.Content(fetchRoomsUseCase.fetch())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
34
matrix-chat-engine/build.gradle
Normal file
34
matrix-chat-engine/build.gradle
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java-test-fixtures'
|
||||||
|
id 'kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||||
|
|
||||||
|
implementation project(":core")
|
||||||
|
implementation project(":chat-engine")
|
||||||
|
|
||||||
|
implementation project(":domains:olm")
|
||||||
|
|
||||||
|
implementation project(":matrix:matrix")
|
||||||
|
implementation project(":matrix:matrix-http-ktor")
|
||||||
|
implementation project(":matrix:services:auth")
|
||||||
|
implementation project(":matrix:services:sync")
|
||||||
|
implementation project(":matrix:services:room")
|
||||||
|
implementation project(":matrix:services:push")
|
||||||
|
implementation project(":matrix:services:message")
|
||||||
|
implementation project(":matrix:services:device")
|
||||||
|
implementation project(":matrix:services:crypto")
|
||||||
|
implementation project(":matrix:services:profile")
|
||||||
|
|
||||||
|
kotlinTest(it)
|
||||||
|
kotlinFixtures(it)
|
||||||
|
|
||||||
|
testImplementation(testFixtures(project(":matrix:services:sync")))
|
||||||
|
testImplementation(testFixtures(project(":matrix:services:message")))
|
||||||
|
testImplementation(testFixtures(project(":matrix:common")))
|
||||||
|
testImplementation(testFixtures(project(":core")))
|
||||||
|
testImplementation(testFixtures(project(":domains:store")))
|
||||||
|
testImplementation(testFixtures(project(":chat-engine")))
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package app.dapk.st.directory
|
package app.dapk.st.engine
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.matrix.common.CredentialsStore
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
@ -6,22 +6,12 @@ import app.dapk.st.matrix.common.RoomMember
|
|||||||
import app.dapk.st.matrix.common.UserId
|
import app.dapk.st.matrix.common.UserId
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
import app.dapk.st.matrix.room.RoomService
|
import app.dapk.st.matrix.room.RoomService
|
||||||
import app.dapk.st.matrix.sync.*
|
import app.dapk.st.matrix.sync.RoomStore
|
||||||
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing
|
import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
@JvmInline
|
internal class DirectoryUseCase(
|
||||||
value class UnreadCount(val value: Int)
|
|
||||||
|
|
||||||
typealias DirectoryState = List<RoomFoo>
|
|
||||||
|
|
||||||
data class RoomFoo(
|
|
||||||
val overview: RoomOverview,
|
|
||||||
val unreadCount: UnreadCount,
|
|
||||||
val typing: Typing?
|
|
||||||
)
|
|
||||||
|
|
||||||
class DirectoryUseCase(
|
|
||||||
private val syncService: SyncService,
|
private val syncService: SyncService,
|
||||||
private val messageService: MessageService,
|
private val messageService: MessageService,
|
||||||
private val roomService: RoomService,
|
private val roomService: RoomService,
|
||||||
@ -38,10 +28,10 @@ class DirectoryUseCase(
|
|||||||
syncService.events()
|
syncService.events()
|
||||||
) { overviewState, localEchos, unread, events ->
|
) { overviewState, localEchos, unread, events ->
|
||||||
overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview ->
|
overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview ->
|
||||||
RoomFoo(
|
DirectoryItem(
|
||||||
overview = roomOverview,
|
overview = roomOverview,
|
||||||
unreadCount = UnreadCount(unread[roomOverview.roomId] ?: 0),
|
unreadCount = UnreadCount(unread[roomOverview.roomId] ?: 0),
|
||||||
typing = events.filterIsInstance<Typing>().firstOrNull { it.roomId == roomOverview.roomId }
|
typing = events.filterIsInstance<Typing>().firstOrNull { it.roomId == roomOverview.roomId }?.engine()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,14 +39,9 @@ class DirectoryUseCase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun overviewDatasource() = combine(
|
private fun overviewDatasource() = combine(
|
||||||
syncService.startSyncing().map { false }.onStart { emit(true) },
|
syncService.startSyncing(),
|
||||||
syncService.overview()
|
syncService.overview().map { it.map { it.engine() } }
|
||||||
) { isFirstLoad, overview ->
|
) { _, overview -> overview }.filterNotNull()
|
||||||
when {
|
|
||||||
isFirstLoad && overview.isEmpty() -> null
|
|
||||||
else -> overview
|
|
||||||
}
|
|
||||||
}.filterNotNull()
|
|
||||||
|
|
||||||
private suspend fun OverviewState.mergeWithLocalEchos(localEchos: Map<RoomId, List<MessageService.LocalEcho>>, userId: UserId): OverviewState {
|
private suspend fun OverviewState.mergeWithLocalEchos(localEchos: Map<RoomId, List<MessageService.LocalEcho>>, userId: UserId): OverviewState {
|
||||||
return when {
|
return when {
|
||||||
@ -81,7 +66,7 @@ class DirectoryUseCase(
|
|||||||
val latestEcho = echos.maxByOrNull { it.timestampUtc }
|
val latestEcho = echos.maxByOrNull { it.timestampUtc }
|
||||||
return if (latestEcho != null && latestEcho.timestampUtc > (this.lastMessage?.utcTimestamp ?: 0)) {
|
return if (latestEcho != null && latestEcho.timestampUtc > (this.lastMessage?.utcTimestamp ?: 0)) {
|
||||||
this.copy(
|
this.copy(
|
||||||
lastMessage = LastMessage(
|
lastMessage = RoomOverview.LastMessage(
|
||||||
content = when (val message = latestEcho.message) {
|
content = when (val message = latestEcho.message) {
|
||||||
is MessageService.Message.TextMessage -> message.content.body
|
is MessageService.Message.TextMessage -> message.content.body
|
||||||
is MessageService.Message.ImageMessage -> "\uD83D\uDCF7"
|
is MessageService.Message.ImageMessage -> "\uD83D\uDCF7"
|
||||||
@ -96,3 +81,6 @@ class DirectoryUseCase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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.EventId
|
||||||
import app.dapk.st.matrix.common.RoomMember
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
import app.dapk.st.matrix.sync.MessageMeta
|
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
|
|
||||||
internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
|
internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
|
||||||
|
|
@ -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.AppLogTag
|
||||||
import app.dapk.st.core.log
|
import app.dapk.st.core.log
|
||||||
import app.dapk.st.matrix.common.CredentialsStore
|
import app.dapk.st.matrix.common.CredentialsStore
|
||||||
import app.dapk.st.matrix.common.EventId
|
import app.dapk.st.matrix.common.EventId
|
||||||
|
import app.dapk.st.matrix.common.JsonString
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import app.dapk.st.matrix.message.BackgroundScheduler
|
||||||
import app.dapk.st.matrix.sync.RoomStore
|
import app.dapk.st.matrix.sync.RoomStore
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
import app.dapk.st.push.PushHandler
|
|
||||||
import app.dapk.st.push.PushTokenPayload
|
|
||||||
import app.dapk.st.work.WorkScheduler
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
private var previousJob: Job? = null
|
private var previousJob: Job? = null
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class MatrixPushHandler(
|
class MatrixPushHandler(
|
||||||
private val workScheduler: WorkScheduler,
|
private val backgroundScheduler: BackgroundScheduler,
|
||||||
private val credentialsStore: CredentialsStore,
|
private val credentialsStore: CredentialsStore,
|
||||||
private val syncService: SyncService,
|
private val syncService: SyncService,
|
||||||
private val roomStore: RoomStore,
|
private val roomStore: RoomStore,
|
||||||
) : PushHandler {
|
) : PushHandler {
|
||||||
|
|
||||||
override fun onNewToken(payload: PushTokenPayload) {
|
override fun onNewToken(payload: JsonString) {
|
||||||
log(AppLogTag.PUSH, "new push token received")
|
log(AppLogTag.PUSH, "new push token received")
|
||||||
workScheduler.schedule(
|
backgroundScheduler.schedule(
|
||||||
WorkScheduler.WorkTask(
|
key = "2",
|
||||||
|
task = BackgroundScheduler.Task(
|
||||||
type = "push_token",
|
type = "push_token",
|
||||||
jobId = 2,
|
jsonPayload = payload
|
||||||
jsonPayload = Json.encodeToString(PushTokenPayload.serializer(), payload)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -66,7 +64,7 @@ class MatrixPushHandler(
|
|||||||
|
|
||||||
private suspend fun waitForEvent(timeout: Long, eventId: EventId): EventId? {
|
private suspend fun waitForEvent(timeout: Long, eventId: EventId): EventId? {
|
||||||
return withTimeoutOrNull(timeout) {
|
return withTimeoutOrNull(timeout) {
|
||||||
combine(syncService.startSyncing().startInstantly(), syncService.observeEvent(eventId)) { _, event -> event }
|
combine(syncService.startSyncing(), syncService.observeEvent(eventId)) { _, event -> event }
|
||||||
.firstOrNull {
|
.firstOrNull {
|
||||||
it == eventId
|
it == eventId
|
||||||
}
|
}
|
||||||
@ -75,11 +73,9 @@ class MatrixPushHandler(
|
|||||||
|
|
||||||
private suspend fun waitForUnreadChange(timeout: Long): String? {
|
private suspend fun waitForUnreadChange(timeout: Long): String? {
|
||||||
return withTimeoutOrNull(timeout) {
|
return withTimeoutOrNull(timeout) {
|
||||||
combine(syncService.startSyncing().startInstantly(), roomStore.observeUnread()) { _, unread -> unread }
|
combine(syncService.startSyncing(), roomStore.observeUnread()) { _, unread -> unread }
|
||||||
.first()
|
.first()
|
||||||
"ignored"
|
"ignored"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private fun Flow<Unit>.startInstantly() = this.onStart { emit(Unit) }
|
|
||||||
}
|
|
@ -1,10 +1,8 @@
|
|||||||
package app.dapk.st.messenger
|
package app.dapk.st.engine
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.EventId
|
import app.dapk.st.matrix.common.EventId
|
||||||
import app.dapk.st.matrix.common.RoomMember
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
|
||||||
|
|
||||||
internal typealias MergeWithLocalEchosUseCase = (RoomState, RoomMember, List<MessageService.LocalEcho>) -> RoomState
|
internal typealias MergeWithLocalEchosUseCase = (RoomState, RoomMember, List<MessageService.LocalEcho>) -> RoomState
|
||||||
|
|
@ -1,17 +1,16 @@
|
|||||||
package app.dapk.st.notifications
|
package app.dapk.st.engine
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.RoomId
|
|
||||||
import app.dapk.st.matrix.sync.InviteMeta
|
import app.dapk.st.matrix.sync.InviteMeta
|
||||||
import app.dapk.st.matrix.sync.OverviewStore
|
import app.dapk.st.matrix.sync.OverviewStore
|
||||||
import app.dapk.st.matrix.sync.RoomInvite
|
import app.dapk.st.matrix.sync.RoomInvite
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
internal typealias ObserveInviteNotificationsUseCase = suspend () -> Flow<InviteNotification>
|
internal typealias ObserveInviteNotificationsUseCase = () -> Flow<InviteNotification>
|
||||||
|
|
||||||
class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase {
|
class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase {
|
||||||
|
|
||||||
override suspend fun invoke(): Flow<InviteNotification> {
|
override fun invoke(): Flow<InviteNotification> {
|
||||||
return overviewStore.latestInvites()
|
return overviewStore.latestInvites()
|
||||||
.diff()
|
.diff()
|
||||||
.drop(1)
|
.drop(1)
|
||||||
@ -43,8 +42,3 @@ class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewS
|
|||||||
private fun <T> Flow<Set<T>>.flatten() = this.flatMapConcat { items ->
|
private fun <T> Flow<Set<T>>.flatten() = this.flatMapConcat { items ->
|
||||||
flow { items.forEach { this.emit(it) } }
|
flow { items.forEach { this.emit(it) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
data class InviteNotification(
|
|
||||||
val content: String,
|
|
||||||
val roomId: RoomId
|
|
||||||
)
|
|
@ -1,4 +1,4 @@
|
|||||||
package app.dapk.st.notifications
|
package app.dapk.st.engine
|
||||||
|
|
||||||
import app.dapk.st.core.AppLogTag
|
import app.dapk.st.core.AppLogTag
|
||||||
import app.dapk.st.core.extensions.clearAndPutAll
|
import app.dapk.st.core.extensions.clearAndPutAll
|
||||||
@ -6,17 +6,16 @@ import app.dapk.st.core.extensions.containsKey
|
|||||||
import app.dapk.st.core.log
|
import app.dapk.st.core.log
|
||||||
import app.dapk.st.matrix.common.EventId
|
import app.dapk.st.matrix.common.EventId
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import app.dapk.st.matrix.sync.RoomStore
|
import app.dapk.st.matrix.sync.RoomStore
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent
|
||||||
|
import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview
|
||||||
|
|
||||||
typealias UnreadNotifications = Pair<Map<RoomOverview, List<RoomEvent>>, NotificationDiff>
|
internal typealias ObserveUnreadNotificationsUseCase = () -> Flow<UnreadNotifications>
|
||||||
internal typealias ObserveUnreadNotificationsUseCase = suspend () -> Flow<UnreadNotifications>
|
|
||||||
|
|
||||||
class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase {
|
class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase {
|
||||||
|
|
||||||
override suspend fun invoke(): Flow<UnreadNotifications> {
|
override fun invoke(): Flow<UnreadNotifications> {
|
||||||
return roomStore.observeUnread()
|
return roomStore.observeUnread()
|
||||||
.mapWithDiff()
|
.mapWithDiff()
|
||||||
.avoidShowingPreviousNotificationsOnLaunch()
|
.avoidShowingPreviousNotificationsOnLaunch()
|
||||||
@ -25,28 +24,7 @@ class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) :
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Flow<UnreadNotifications>.onlyRenderableChanges(): Flow<UnreadNotifications> {
|
private fun Flow<Map<MatrixRoomOverview, List<MatrixRoomEvent>>>.mapWithDiff(): Flow<Pair<Map<MatrixRoomOverview, List<MatrixRoomEvent>>, NotificationDiff>> {
|
||||||
val inferredCurrentNotifications = mutableMapOf<RoomId, List<RoomEvent>>()
|
|
||||||
return this
|
|
||||||
.filter { (_, diff) ->
|
|
||||||
when {
|
|
||||||
diff.changedOrNew.isEmpty() && diff.removed.isEmpty() -> {
|
|
||||||
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> {
|
|
||||||
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onEach { (allUnread, _) -> inferredCurrentNotifications.clearAndPutAll(allUnread.mapKeys { it.key.roomId }) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Flow<Map<RoomOverview, List<RoomEvent>>>.mapWithDiff(): Flow<Pair<Map<RoomOverview, List<RoomEvent>>, NotificationDiff>> {
|
|
||||||
val previousUnreadEvents = mutableMapOf<RoomId, List<TimestampedEventId>>()
|
val previousUnreadEvents = mutableMapOf<RoomId, List<TimestampedEventId>>()
|
||||||
return this.map { each ->
|
return this.map { each ->
|
||||||
val allUnreadIds = each.toTimestampedIds()
|
val allUnreadIds = each.toTimestampedIds()
|
||||||
@ -83,19 +61,39 @@ private fun Map<RoomId, List<TimestampedEventId>>?.toLatestTimestamps() = this?.
|
|||||||
|
|
||||||
private fun Map<RoomId, List<TimestampedEventId>>.toEventIds() = this.mapValues { it.value.map { it.first } }
|
private fun Map<RoomId, List<TimestampedEventId>>.toEventIds() = this.mapValues { it.value.map { it.first } }
|
||||||
|
|
||||||
private fun Map<RoomOverview, List<RoomEvent>>.toTimestampedIds() = this
|
private fun Map<MatrixRoomOverview, List<MatrixRoomEvent>>.toTimestampedIds() = this
|
||||||
.mapValues { it.value.toEventIds() }
|
.mapValues { it.value.toEventIds() }
|
||||||
.mapKeys { it.key.roomId }
|
.mapKeys { it.key.roomId }
|
||||||
|
|
||||||
private fun List<RoomEvent>.toEventIds() = this.map { it.eventId to it.utcTimestamp }
|
private fun List<MatrixRoomEvent>.toEventIds() = this.map { it.eventId to it.utcTimestamp }
|
||||||
|
|
||||||
private fun <T> Flow<T>.avoidShowingPreviousNotificationsOnLaunch() = drop(1)
|
private fun <T> Flow<T>.avoidShowingPreviousNotificationsOnLaunch() = drop(1)
|
||||||
|
|
||||||
data class NotificationDiff(
|
private fun Flow<Pair<Map<MatrixRoomOverview, List<MatrixRoomEvent>>, NotificationDiff>>.onlyRenderableChanges(): Flow<UnreadNotifications> {
|
||||||
val unchanged: Map<RoomId, List<EventId>>,
|
val inferredCurrentNotifications = mutableMapOf<RoomId, List<MatrixRoomEvent>>()
|
||||||
val changedOrNew: Map<RoomId, List<EventId>>,
|
return this
|
||||||
val removed: Map<RoomId, List<EventId>>,
|
.filter { (_, diff) ->
|
||||||
val newRooms: Set<RoomId>
|
when {
|
||||||
)
|
diff.changedOrNew.isEmpty() && diff.removed.isEmpty() -> {
|
||||||
|
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> {
|
||||||
|
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onEach { (allUnread, _) -> inferredCurrentNotifications.clearAndPutAll(allUnread.mapKeys { it.key.roomId }) }
|
||||||
|
.map {
|
||||||
|
val engineModels = it.first
|
||||||
|
.mapKeys { it.key.engine() }
|
||||||
|
.mapValues { it.value.map { it.engine() } }
|
||||||
|
engineModels to it.second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typealias TimestampedEventId = Pair<EventId, Long>
|
typealias TimestampedEventId = Pair<EventId, Long>
|
@ -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.RoomId
|
||||||
import app.dapk.st.matrix.common.RoomMember
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
import app.dapk.st.matrix.common.UserId
|
import app.dapk.st.matrix.common.UserId
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
import app.dapk.st.matrix.room.RoomService
|
import app.dapk.st.matrix.room.RoomService
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow<MessengerState>
|
internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow<MessengerState>
|
||||||
|
|
||||||
@ -37,22 +36,16 @@ internal class TimelineUseCaseImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId },
|
typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId }?.engine(),
|
||||||
self = userId,
|
self = userId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun roomDatasource(roomId: RoomId) = combine(
|
private fun roomDatasource(roomId: RoomId) = combine(
|
||||||
syncService.startSyncing().startAndIgnoreEmissions(),
|
syncService.startSyncing(),
|
||||||
syncService.room(roomId)
|
syncService.room(roomId).map { it.engine() }
|
||||||
) { _, room -> room }
|
) { _, room -> room }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null)
|
private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null)
|
||||||
|
|
||||||
data class MessengerState(
|
|
||||||
val self: UserId,
|
|
||||||
val roomState: RoomState,
|
|
||||||
val typing: SyncService.SyncEvent.Typing?
|
|
||||||
)
|
|
@ -1,10 +1,10 @@
|
|||||||
package app.dapk.st.messenger
|
package app.dapk.st.engine
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.EventId
|
import app.dapk.st.matrix.common.EventId
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
import app.dapk.st.matrix.sync.MessageMeta
|
import app.dapk.st.matrix.sync.MessageMeta
|
||||||
|
import fake.FakeMetaMapper
|
||||||
import fixture.*
|
import fixture.*
|
||||||
import internalfake.FakeMetaMapper
|
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ class LocalEchoMapperTest {
|
|||||||
result shouldBeEqualTo aRoomMessageEvent(
|
result shouldBeEqualTo aRoomMessageEvent(
|
||||||
eventId = echo.eventId!!,
|
eventId = echo.eventId!!,
|
||||||
content = AN_ECHO_CONTENT.content.body,
|
content = AN_ECHO_CONTENT.content.body,
|
||||||
meta = A_META
|
meta = A_META.engine()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,25 +40,25 @@ class LocalEchoMapperTest {
|
|||||||
result shouldBeEqualTo aRoomMessageEvent(
|
result shouldBeEqualTo aRoomMessageEvent(
|
||||||
eventId = anEventId(echo.localId),
|
eventId = anEventId(echo.localId),
|
||||||
content = AN_ECHO_CONTENT.content.body,
|
content = AN_ECHO_CONTENT.content.body,
|
||||||
meta = A_META
|
meta = A_META.engine()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when merging with echo then updates meta with the echos meta`() = runWith(localEchoMapper) {
|
fun `when merging with echo then updates meta with the echos meta`() = runWith(localEchoMapper) {
|
||||||
val previousMeta = MessageMeta.LocalEcho("previous", MessageMeta.LocalEcho.State.Sending)
|
val previousMeta = MessageMeta.LocalEcho("previous", MessageMeta.LocalEcho.State.Sending).engine()
|
||||||
val event = aRoomMessageEvent(meta = previousMeta)
|
val event = aRoomMessageEvent(meta = previousMeta)
|
||||||
val echo = aLocalEcho()
|
val echo = aLocalEcho()
|
||||||
fakeMetaMapper.given(echo).returns(A_META)
|
fakeMetaMapper.given(echo).returns(A_META.engine() as app.dapk.st.engine.MessageMeta.LocalEcho)
|
||||||
|
|
||||||
val result = event.mergeWith(echo)
|
val result = event.mergeWith(echo)
|
||||||
|
|
||||||
result shouldBeEqualTo aRoomMessageEvent(meta = A_META)
|
result shouldBeEqualTo aRoomMessageEvent(meta = A_META.engine())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenEcho(eventId: EventId? = null, localId: String = "", meta: MessageMeta.LocalEcho = A_META): MessageService.LocalEcho {
|
private fun givenEcho(eventId: EventId? = null, localId: String = "", meta: MessageMeta.LocalEcho = A_META): MessageService.LocalEcho {
|
||||||
return aLocalEcho(eventId = eventId, message = aTextMessage(localId = localId)).also {
|
return aLocalEcho(eventId = eventId, message = aTextMessage(localId = localId)).also {
|
||||||
fakeMetaMapper.given(it).returns(meta)
|
fakeMetaMapper.given(it).returns(meta.engine() as app.dapk.st.engine.MessageMeta.LocalEcho)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,11 +1,8 @@
|
|||||||
package app.dapk.st.messenger
|
package app.dapk.st.engine
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.EventId
|
import app.dapk.st.matrix.common.EventId
|
||||||
import app.dapk.st.matrix.common.MessageType
|
|
||||||
import app.dapk.st.matrix.common.RoomId
|
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
import fixture.*
|
import fixture.*
|
||||||
import internalfake.FakeLocalEventMapper
|
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@ -18,7 +15,7 @@ private val ANOTHER_ROOM_MESSAGE_EVENT = A_ROOM_MESSAGE_EVENT.copy(eventId = anE
|
|||||||
|
|
||||||
class MergeWithLocalEchosUseCaseTest {
|
class MergeWithLocalEchosUseCaseTest {
|
||||||
|
|
||||||
private val fakeLocalEchoMapper = FakeLocalEventMapper()
|
private val fakeLocalEchoMapper = fake.FakeLocalEventMapper()
|
||||||
private val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(fakeLocalEchoMapper.instance)
|
private val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(fakeLocalEchoMapper.instance)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -60,4 +57,4 @@ class MergeWithLocalEchosUseCaseTest {
|
|||||||
aTextMessage(aTextContent(body)),
|
aTextMessage(aTextContent(body)),
|
||||||
state,
|
state,
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
package app.dapk.st.messenger
|
package app.dapk.st.engine
|
||||||
|
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
import app.dapk.st.matrix.sync.MessageMeta
|
|
||||||
import fixture.aLocalEcho
|
import fixture.aLocalEcho
|
||||||
import fixture.aTextMessage
|
import fixture.aTextMessage
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
@ -1,24 +1,27 @@
|
|||||||
package app.dapk.st.notifications
|
package app.dapk.st.engine
|
||||||
|
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
|
||||||
import app.dapk.st.matrix.sync.RoomOverview
|
|
||||||
import fake.FakeRoomStore
|
import fake.FakeRoomStore
|
||||||
import fixture.NotificationDiffFixtures.aNotificationDiff
|
import fixture.NotificationDiffFixtures.aNotificationDiff
|
||||||
|
import fixture.aMatrixRoomMessageEvent
|
||||||
|
import fixture.aMatrixRoomOverview
|
||||||
import fixture.aRoomId
|
import fixture.aRoomId
|
||||||
import fixture.aRoomMessageEvent
|
|
||||||
import fixture.aRoomOverview
|
|
||||||
import fixture.anEventId
|
import fixture.anEventId
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent
|
||||||
|
import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview
|
||||||
|
|
||||||
private val NO_UNREADS = emptyMap<RoomOverview, List<RoomEvent>>()
|
private val NO_UNREADS = emptyMap<MatrixRoomOverview, List<MatrixRoomEvent>>()
|
||||||
private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000)
|
private val A_MESSAGE = aMatrixRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000)
|
||||||
private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000)
|
private val A_MESSAGE_2 = aMatrixRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000)
|
||||||
private val A_ROOM_OVERVIEW = aRoomOverview(roomId = aRoomId("1"))
|
private val A_ROOM_OVERVIEW = aMatrixRoomOverview(roomId = aRoomId("1"))
|
||||||
private val A_ROOM_OVERVIEW_2 = aRoomOverview(roomId = aRoomId("2"))
|
private val A_ROOM_OVERVIEW_2 = aMatrixRoomOverview(roomId = aRoomId("2"))
|
||||||
|
|
||||||
|
private fun MatrixRoomOverview.withUnreads(vararg events: MatrixRoomEvent) = mapOf(this to events.toList())
|
||||||
|
private fun MatrixRoomOverview.toDiff(vararg events: MatrixRoomEvent) = mapOf(this.roomId to events.map { it.eventId })
|
||||||
|
|
||||||
class ObserveUnreadRenderNotificationsUseCaseTest {
|
class ObserveUnreadRenderNotificationsUseCaseTest {
|
||||||
|
|
||||||
@ -33,7 +36,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
|
|||||||
val result = useCase.invoke().toList()
|
val result = useCase.invoke().toList()
|
||||||
|
|
||||||
result shouldBeEqualTo listOf(
|
result shouldBeEqualTo listOf(
|
||||||
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff(
|
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff(
|
||||||
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
|
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
|
||||||
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
|
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
|
||||||
)
|
)
|
||||||
@ -47,11 +50,11 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
|
|||||||
val result = useCase.invoke().toList()
|
val result = useCase.invoke().toList()
|
||||||
|
|
||||||
result shouldBeEqualTo listOf(
|
result shouldBeEqualTo listOf(
|
||||||
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff(
|
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff(
|
||||||
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
|
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
|
||||||
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
|
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
|
||||||
),
|
),
|
||||||
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2))
|
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2).engine() to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +67,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
|
|||||||
val result = useCase.invoke().toList()
|
val result = useCase.invoke().toList()
|
||||||
|
|
||||||
result shouldBeEqualTo listOf(
|
result shouldBeEqualTo listOf(
|
||||||
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2))
|
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2).engine() to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +95,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
|
|||||||
val result = useCase.invoke().toList()
|
val result = useCase.invoke().toList()
|
||||||
|
|
||||||
result shouldBeEqualTo listOf(
|
result shouldBeEqualTo listOf(
|
||||||
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff(
|
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff(
|
||||||
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
|
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
|
||||||
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
|
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
|
||||||
),
|
),
|
||||||
@ -110,8 +113,10 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
|
|||||||
result shouldBeEqualTo emptyList()
|
result shouldBeEqualTo emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun givenNoInitialUnreads(vararg unreads: Map<RoomOverview, List<RoomEvent>>) = fakeRoomStore.givenUnreadEvents(flowOf(NO_UNREADS, *unreads))
|
private fun givenNoInitialUnreads(vararg unreads: Map<MatrixRoomOverview, List<MatrixRoomEvent>>) =
|
||||||
|
fakeRoomStore.givenUnreadEvents(flowOf(NO_UNREADS, *unreads))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun RoomOverview.withUnreads(vararg events: RoomEvent) = mapOf(this to events.toList())
|
private fun Map<MatrixRoomOverview, List<MatrixRoomEvent>>.engine() = this
|
||||||
private fun RoomOverview.toDiff(vararg events: RoomEvent) = mapOf(this.roomId to events.map { it.eventId })
|
.mapKeys { it.key.engine() }
|
||||||
|
.mapValues { it.value.map { it.engine() } }
|
@ -1,13 +1,15 @@
|
|||||||
package app.dapk.st.messenger
|
package app.dapk.st.engine
|
||||||
|
|
||||||
import FlowTestObserver
|
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.common.RoomMember
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
|
import app.dapk.st.matrix.common.UserId
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
|
import app.dapk.st.matrix.room.RoomService
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
import app.dapk.st.matrix.sync.RoomState
|
||||||
import app.dapk.st.matrix.sync.SyncService
|
import app.dapk.st.matrix.sync.SyncService
|
||||||
import fake.FakeSyncService
|
import fake.FakeSyncService
|
||||||
import fixture.*
|
import fixture.*
|
||||||
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -16,12 +18,13 @@ import kotlinx.coroutines.flow.collect
|
|||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import test.FlowTestObserver
|
||||||
import test.delegateReturn
|
import test.delegateReturn
|
||||||
|
|
||||||
private val A_ROOM_ID = aRoomId()
|
private val A_ROOM_ID = aRoomId()
|
||||||
private val AN_USER_ID = aUserId()
|
private val AN_USER_ID = aUserId()
|
||||||
private val A_ROOM_STATE = aRoomState()
|
private val A_ROOM_STATE = aMatrixRoomState()
|
||||||
private val A_MERGED_ROOM_STATE = A_ROOM_STATE.copy(events = listOf(aRoomMessageEvent(content = "a merged event")))
|
private val A_MERGED_ROOM_STATE = A_ROOM_STATE.copy(events = listOf(aMatrixRoomMessageEvent(content = "a merged event")))
|
||||||
private val A_LOCAL_ECHOS_LIST = listOf(aLocalEcho())
|
private val A_LOCAL_ECHOS_LIST = listOf(aLocalEcho())
|
||||||
private val A_ROOM_MEMBER = aRoomMember()
|
private val A_ROOM_MEMBER = aRoomMember()
|
||||||
|
|
||||||
@ -47,7 +50,7 @@ class TimelineUseCaseTest {
|
|||||||
.test(this)
|
.test(this)
|
||||||
.assertValues(
|
.assertValues(
|
||||||
listOf(
|
listOf(
|
||||||
aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE)
|
aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE.engine())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -57,13 +60,13 @@ class TimelineUseCaseTest {
|
|||||||
givenSyncEmission(roomState = A_ROOM_STATE, echos = A_LOCAL_ECHOS_LIST)
|
givenSyncEmission(roomState = A_ROOM_STATE, echos = A_LOCAL_ECHOS_LIST)
|
||||||
fakeRoomService.givenFindMember(A_ROOM_ID, AN_USER_ID).returns(A_ROOM_MEMBER)
|
fakeRoomService.givenFindMember(A_ROOM_ID, AN_USER_ID).returns(A_ROOM_MEMBER)
|
||||||
|
|
||||||
fakeMergeWithLocalEchosUseCase.givenMerging(A_ROOM_STATE, A_ROOM_MEMBER, A_LOCAL_ECHOS_LIST).returns(A_MERGED_ROOM_STATE)
|
fakeMergeWithLocalEchosUseCase.givenMerging(A_ROOM_STATE, A_ROOM_MEMBER, A_LOCAL_ECHOS_LIST).returns(A_MERGED_ROOM_STATE.engine())
|
||||||
|
|
||||||
timelineUseCase.invoke(A_ROOM_ID, AN_USER_ID)
|
timelineUseCase.invoke(A_ROOM_ID, AN_USER_ID)
|
||||||
.test(this)
|
.test(this)
|
||||||
.assertValues(
|
.assertValues(
|
||||||
listOf(
|
listOf(
|
||||||
aMessengerState(self = AN_USER_ID, roomState = A_MERGED_ROOM_STATE)
|
aMessengerState(self = AN_USER_ID, roomState = A_MERGED_ROOM_STATE.engine())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -81,7 +84,11 @@ class TimelineUseCaseTest {
|
|||||||
.test(this)
|
.test(this)
|
||||||
.assertValues(
|
.assertValues(
|
||||||
listOf(
|
listOf(
|
||||||
aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE, typing = aTypingSyncEvent(A_ROOM_ID, members = listOf(A_ROOM_MEMBER)))
|
aMessengerState(
|
||||||
|
self = AN_USER_ID,
|
||||||
|
roomState = A_ROOM_STATE.engine(),
|
||||||
|
typing = aTypingSyncEvent(A_ROOM_ID, members = listOf(A_ROOM_MEMBER)).engine()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -104,11 +111,27 @@ suspend fun <T> Flow<T>.test(scope: CoroutineScope) = FlowTestObserver(scope, th
|
|||||||
|
|
||||||
class FakeMergeWithLocalEchosUseCase : MergeWithLocalEchosUseCase by mockk() {
|
class FakeMergeWithLocalEchosUseCase : MergeWithLocalEchosUseCase by mockk() {
|
||||||
fun givenMerging(roomState: RoomState, roomMember: RoomMember, echos: List<MessageService.LocalEcho>) = every {
|
fun givenMerging(roomState: RoomState, roomMember: RoomMember, echos: List<MessageService.LocalEcho>) = every {
|
||||||
this@FakeMergeWithLocalEchosUseCase.invoke(roomState, roomMember, echos)
|
this@FakeMergeWithLocalEchosUseCase.invoke(roomState.engine(), roomMember, echos)
|
||||||
}.delegateReturn()
|
}.delegateReturn()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aTypingSyncEvent(
|
fun aTypingSyncEvent(
|
||||||
roomId: RoomId = aRoomId(),
|
roomId: RoomId = aRoomId(),
|
||||||
members: List<RoomMember> = listOf(aRoomMember())
|
members: List<RoomMember> = listOf(aRoomMember())
|
||||||
) = SyncService.SyncEvent.Typing(roomId, members)
|
) = SyncService.SyncEvent.Typing(roomId, members)
|
||||||
|
|
||||||
|
class FakeMessageService : MessageService by mockk() {
|
||||||
|
|
||||||
|
fun givenEchos(roomId: RoomId) = every { localEchos(roomId) }.delegateReturn()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeRoomService : RoomService by mockk() {
|
||||||
|
fun givenFindMember(roomId: RoomId, userId: UserId) = coEvery { findMember(roomId, userId) }.delegateReturn()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aMessengerState(
|
||||||
|
self: UserId = aUserId(),
|
||||||
|
roomState: app.dapk.st.engine.RoomState,
|
||||||
|
typing: Typing? = null
|
||||||
|
) = MessengerState(self, roomState, typing)
|
@ -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.common.RoomMember
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
import app.dapk.st.messenger.LocalEchoMapper
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
package internalfake
|
package fake
|
||||||
|
|
||||||
import app.dapk.st.messenger.LocalIdFactory
|
import app.dapk.st.engine.LocalIdFactory
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import test.delegateReturn
|
import test.delegateReturn
|
@ -1,7 +1,7 @@
|
|||||||
package internalfake
|
package fake
|
||||||
|
|
||||||
|
import app.dapk.st.engine.MetaMapper
|
||||||
import app.dapk.st.matrix.message.MessageService
|
import app.dapk.st.matrix.message.MessageService
|
||||||
import app.dapk.st.messenger.MetaMapper
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import test.delegateReturn
|
import test.delegateReturn
|
@ -1,6 +1,6 @@
|
|||||||
package fake
|
package fake
|
||||||
|
|
||||||
import app.dapk.st.notifications.ObserveInviteNotificationsUseCase
|
import app.dapk.st.engine.ObserveInviteNotificationsUseCase
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import test.delegateEmit
|
import test.delegateEmit
|
@ -1,6 +1,6 @@
|
|||||||
package fake
|
package fake
|
||||||
|
|
||||||
import app.dapk.st.notifications.ObserveUnreadNotificationsUseCase
|
import app.dapk.st.engine.ObserveUnreadNotificationsUseCase
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import test.delegateEmit
|
import test.delegateEmit
|
@ -12,7 +12,7 @@ private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
|
|||||||
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
|
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
|
||||||
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
|
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
|
||||||
|
|
||||||
class MediaDecrypter(private val base64: Base64) {
|
class MatrixMediaDecrypter(private val base64: Base64) {
|
||||||
|
|
||||||
fun decrypt(input: InputStream, k: String, iv: String): Collector {
|
fun decrypt(input: InputStream, k: String, iv: String): Collector {
|
||||||
val key = base64.decode(k.replace('-', '+').replace('_', '/'))
|
val key = base64.decode(k.replace('-', '+').replace('_', '/'))
|
@ -1,9 +1,11 @@
|
|||||||
package app.dapk.st.matrix.message
|
package app.dapk.st.matrix.message
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.common.JsonString
|
||||||
|
|
||||||
interface BackgroundScheduler {
|
interface BackgroundScheduler {
|
||||||
|
|
||||||
fun schedule(key: String, task: Task)
|
fun schedule(key: String, task: Task)
|
||||||
|
|
||||||
data class Task(val type: String, val jsonPayload: String)
|
data class Task(val type: String, val jsonPayload: JsonString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.dapk.st.matrix.message.internal
|
package app.dapk.st.matrix.message.internal
|
||||||
|
|
||||||
import app.dapk.st.matrix.MatrixTaskRunner
|
import app.dapk.st.matrix.MatrixTaskRunner
|
||||||
|
import app.dapk.st.matrix.common.JsonString
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.http.MatrixHttpClient
|
import app.dapk.st.matrix.http.MatrixHttpClient
|
||||||
import app.dapk.st.matrix.message.*
|
import app.dapk.st.matrix.message.*
|
||||||
@ -69,13 +70,13 @@ internal class DefaultMessageService(
|
|||||||
is MessageService.Message.TextMessage -> {
|
is MessageService.Message.TextMessage -> {
|
||||||
BackgroundScheduler.Task(
|
BackgroundScheduler.Task(
|
||||||
type = MATRIX_MESSAGE_TASK_TYPE,
|
type = MATRIX_MESSAGE_TASK_TYPE,
|
||||||
Json.encodeToString(MessageService.Message.TextMessage.serializer(), this)
|
JsonString(Json.encodeToString(MessageService.Message.TextMessage.serializer(), this))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is MessageService.Message.ImageMessage -> BackgroundScheduler.Task(
|
is MessageService.Message.ImageMessage -> BackgroundScheduler.Task(
|
||||||
type = MATRIX_IMAGE_MESSAGE_TASK_TYPE,
|
type = MATRIX_IMAGE_MESSAGE_TASK_TYPE,
|
||||||
Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this)
|
JsonString(Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,15 @@ interface SyncService : MatrixService {
|
|||||||
fun invites(): Flow<InviteState>
|
fun invites(): Flow<InviteState>
|
||||||
fun overview(): Flow<OverviewState>
|
fun overview(): Flow<OverviewState>
|
||||||
fun room(roomId: RoomId): Flow<RoomState>
|
fun room(roomId: RoomId): Flow<RoomState>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to keep the background syncing alive
|
||||||
|
* Emits once, either when the initial sync completes or immediately if has already sync'd once
|
||||||
|
*/
|
||||||
fun startSyncing(): Flow<Unit>
|
fun startSyncing(): Flow<Unit>
|
||||||
fun events(roomId: RoomId? = null): Flow<List<SyncEvent>>
|
fun events(roomId: RoomId? = null): Flow<List<SyncEvent>>
|
||||||
suspend fun observeEvent(eventId: EventId): Flow<EventId>
|
suspend fun observeEvent(eventId: EventId): Flow<EventId>
|
||||||
suspend fun forceManualRefresh(roomIds: List<RoomId>)
|
suspend fun forceManualRefresh(roomIds: Set<RoomId>)
|
||||||
|
|
||||||
@JvmInline
|
@JvmInline
|
||||||
value class FilterId(val value: String)
|
value class FilterId(val value: String)
|
||||||
@ -31,6 +36,7 @@ interface SyncService : MatrixService {
|
|||||||
|
|
||||||
data class Typing(override val roomId: RoomId, val members: List<RoomMember>) : SyncEvent
|
data class Typing(override val roomId: RoomId, val members: List<RoomMember>) : SyncEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MatrixServiceInstaller.installSyncService(
|
fun MatrixServiceInstaller.installSyncService(
|
||||||
|
@ -24,7 +24,7 @@ private val syncSubscriptionCount = AtomicInteger()
|
|||||||
|
|
||||||
internal class DefaultSyncService(
|
internal class DefaultSyncService(
|
||||||
httpClient: MatrixHttpClient,
|
httpClient: MatrixHttpClient,
|
||||||
syncStore: SyncStore,
|
private val syncStore: SyncStore,
|
||||||
private val overviewStore: OverviewStore,
|
private val overviewStore: OverviewStore,
|
||||||
private val roomStore: RoomStore,
|
private val roomStore: RoomStore,
|
||||||
filterStore: FilterStore,
|
filterStore: FilterStore,
|
||||||
@ -104,13 +104,24 @@ internal class DefaultSyncService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startSyncing() = syncFlow
|
override fun startSyncing(): Flow<Unit> {
|
||||||
|
return flow { emit(syncStore.read(SyncStore.SyncKey.Overview) != null) }.flatMapMerge { hasSynced ->
|
||||||
|
when (hasSynced) {
|
||||||
|
true -> syncFlow.filter { false }.onStart { emit(Unit) }
|
||||||
|
false -> {
|
||||||
|
var counter = 0
|
||||||
|
syncFlow.filter { counter < 1 }.onEach { counter++ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun invites() = overviewStore.latestInvites()
|
override fun invites() = overviewStore.latestInvites()
|
||||||
override fun overview() = overviewStore.latest()
|
override fun overview() = overviewStore.latest()
|
||||||
override fun room(roomId: RoomId) = roomStore.latest(roomId)
|
override fun room(roomId: RoomId) = roomStore.latest(roomId)
|
||||||
override fun events(roomId: RoomId?) = roomId?.let { syncEventsFlow.map { it.filter { it.roomId == roomId } }.distinctUntilChanged() } ?: syncEventsFlow
|
override fun events(roomId: RoomId?) = roomId?.let { syncEventsFlow.map { it.filter { it.roomId == roomId } }.distinctUntilChanged() } ?: syncEventsFlow
|
||||||
override suspend fun observeEvent(eventId: EventId) = roomStore.observeEvent(eventId)
|
override suspend fun observeEvent(eventId: EventId) = roomStore.observeEvent(eventId)
|
||||||
override suspend fun forceManualRefresh(roomIds: List<RoomId>) {
|
override suspend fun forceManualRefresh(roomIds: Set<RoomId>) {
|
||||||
coroutineDispatchers.withIoContext {
|
coroutineDispatchers.withIoContext {
|
||||||
roomIds.map {
|
roomIds.map {
|
||||||
async {
|
async {
|
||||||
|
@ -15,7 +15,7 @@ import org.junit.Test
|
|||||||
|
|
||||||
private const val A_DECRYPTED_MESSAGE_CONTENT = "decrypted - content"
|
private const val A_DECRYPTED_MESSAGE_CONTENT = "decrypted - content"
|
||||||
private val AN_ENCRYPTED_ROOM_CONTENT = aMegolmV1()
|
private val AN_ENCRYPTED_ROOM_CONTENT = aMegolmV1()
|
||||||
private val AN_ENCRYPTED_ROOM_MESSAGE = aRoomMessageEvent(encryptedContent = AN_ENCRYPTED_ROOM_CONTENT)
|
private val AN_ENCRYPTED_ROOM_MESSAGE = aMatrixRoomMessageEvent(encryptedContent = AN_ENCRYPTED_ROOM_CONTENT)
|
||||||
private val AN_ENCRYPTED_ROOM_REPLY = aRoomReplyMessageEvent(
|
private val AN_ENCRYPTED_ROOM_REPLY = aRoomReplyMessageEvent(
|
||||||
message = AN_ENCRYPTED_ROOM_MESSAGE,
|
message = AN_ENCRYPTED_ROOM_MESSAGE,
|
||||||
replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event"))
|
replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event"))
|
||||||
@ -37,7 +37,7 @@ class RoomEventsDecrypterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given clear message event, when decrypting, then does nothing`() = runTest {
|
fun `given clear message event, when decrypting, then does nothing`() = runTest {
|
||||||
val aClearMessageEvent = aRoomMessageEvent(encryptedContent = null)
|
val aClearMessageEvent = aMatrixRoomMessageEvent(encryptedContent = null)
|
||||||
val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent))
|
val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent))
|
||||||
|
|
||||||
result shouldBeEqualTo listOf(aClearMessageEvent)
|
result shouldBeEqualTo listOf(aClearMessageEvent)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package app.dapk.st.matrix.sync.internal.sync
|
package app.dapk.st.matrix.sync.internal.sync
|
||||||
|
|
||||||
import fake.FakeRoomStore
|
import fake.FakeRoomStore
|
||||||
import fixture.aRoomMessageEvent
|
import fixture.aMatrixRoomMessageEvent
|
||||||
import fixture.anEventId
|
import fixture.anEventId
|
||||||
import internalfixture.aTimelineTextEventContent
|
import internalfixture.aTimelineTextEventContent
|
||||||
import internalfixture.anApiTimelineTextEvent
|
import internalfixture.anApiTimelineTextEvent
|
||||||
@ -11,8 +11,8 @@ import org.junit.Test
|
|||||||
|
|
||||||
private val AN_EVENT_ID = anEventId()
|
private val AN_EVENT_ID = anEventId()
|
||||||
private val A_TIMELINE_EVENT = anApiTimelineTextEvent(AN_EVENT_ID, content = aTimelineTextEventContent(body = "timeline event"))
|
private val A_TIMELINE_EVENT = anApiTimelineTextEvent(AN_EVENT_ID, content = aTimelineTextEventContent(body = "timeline event"))
|
||||||
private val A_ROOM_EVENT = aRoomMessageEvent(AN_EVENT_ID, content = "previous room event")
|
private val A_ROOM_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = "previous room event")
|
||||||
private val A_PERSISTED_EVENT = aRoomMessageEvent(AN_EVENT_ID, content = "persisted event")
|
private val A_PERSISTED_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = "persisted event")
|
||||||
|
|
||||||
class EventLookupUseCaseTest {
|
class EventLookupUseCaseTest {
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import app.dapk.st.matrix.sync.RoomEvent
|
|||||||
import app.dapk.st.matrix.sync.internal.request.ApiEncryptedContent
|
import app.dapk.st.matrix.sync.internal.request.ApiEncryptedContent
|
||||||
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
|
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
|
||||||
import fake.FakeErrorTracker
|
import fake.FakeErrorTracker
|
||||||
import fake.FakeMatrixLogger
|
|
||||||
import fake.FakeRoomMembersService
|
import fake.FakeRoomMembersService
|
||||||
import fixture.*
|
import fixture.*
|
||||||
import internalfixture.*
|
import internalfixture.*
|
||||||
@ -41,7 +40,7 @@ internal class RoomEventCreatorTest {
|
|||||||
|
|
||||||
val result = with(roomEventCreator) { megolmEvent.toRoomEvent(A_ROOM_ID) }
|
val result = with(roomEventCreator) { megolmEvent.toRoomEvent(A_ROOM_ID) }
|
||||||
|
|
||||||
result shouldBeEqualTo aRoomMessageEvent(
|
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||||
eventId = megolmEvent.eventId,
|
eventId = megolmEvent.eventId,
|
||||||
utcTimestamp = megolmEvent.utcTimestamp,
|
utcTimestamp = megolmEvent.utcTimestamp,
|
||||||
content = "Encrypted message",
|
content = "Encrypted message",
|
||||||
@ -74,7 +73,7 @@ internal class RoomEventCreatorTest {
|
|||||||
|
|
||||||
val result = with(roomEventCreator) { A_TEXT_EVENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
val result = with(roomEventCreator) { A_TEXT_EVENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
||||||
|
|
||||||
result shouldBeEqualTo aRoomMessageEvent(
|
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||||
eventId = A_TEXT_EVENT.id,
|
eventId = A_TEXT_EVENT.id,
|
||||||
utcTimestamp = A_TEXT_EVENT.utcTimestamp,
|
utcTimestamp = A_TEXT_EVENT.utcTimestamp,
|
||||||
content = A_TEXT_EVENT_MESSAGE,
|
content = A_TEXT_EVENT_MESSAGE,
|
||||||
@ -88,7 +87,7 @@ internal class RoomEventCreatorTest {
|
|||||||
|
|
||||||
val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
||||||
|
|
||||||
result shouldBeEqualTo aRoomMessageEvent(
|
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||||
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
|
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
|
||||||
utcTimestamp = A_TEXT_EVENT_WITHOUT_CONTENT.utcTimestamp,
|
utcTimestamp = A_TEXT_EVENT_WITHOUT_CONTENT.utcTimestamp,
|
||||||
content = "redacted",
|
content = "redacted",
|
||||||
@ -103,7 +102,7 @@ internal class RoomEventCreatorTest {
|
|||||||
|
|
||||||
val result = with(roomEventCreator) { editEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
val result = with(roomEventCreator) { editEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
||||||
|
|
||||||
result shouldBeEqualTo aRoomMessageEvent(
|
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||||
eventId = editEvent.id,
|
eventId = editEvent.id,
|
||||||
utcTimestamp = editEvent.utcTimestamp,
|
utcTimestamp = editEvent.utcTimestamp,
|
||||||
content = editEvent.asTextContent().body!!,
|
content = editEvent.asTextContent().body!!,
|
||||||
@ -121,7 +120,7 @@ internal class RoomEventCreatorTest {
|
|||||||
|
|
||||||
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||||
|
|
||||||
result shouldBeEqualTo aRoomMessageEvent(
|
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||||
eventId = originalMessage.id,
|
eventId = originalMessage.id,
|
||||||
utcTimestamp = editedMessage.utcTimestamp,
|
utcTimestamp = editedMessage.utcTimestamp,
|
||||||
content = A_TEXT_EVENT_MESSAGE,
|
content = A_TEXT_EVENT_MESSAGE,
|
||||||
@ -133,13 +132,13 @@ internal class RoomEventCreatorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `given edited event which relates to a room event then updates existing message`() = runTest {
|
fun `given edited event which relates to a room event then updates existing message`() = runTest {
|
||||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||||
val originalMessage = aRoomMessageEvent()
|
val originalMessage = aMatrixRoomMessageEvent()
|
||||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||||
val lookup = givenLookup(originalMessage)
|
val lookup = givenLookup(originalMessage)
|
||||||
|
|
||||||
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||||
|
|
||||||
result shouldBeEqualTo aRoomMessageEvent(
|
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||||
eventId = originalMessage.eventId,
|
eventId = originalMessage.eventId,
|
||||||
utcTimestamp = editedMessage.utcTimestamp,
|
utcTimestamp = editedMessage.utcTimestamp,
|
||||||
content = A_TEXT_EVENT_MESSAGE,
|
content = A_TEXT_EVENT_MESSAGE,
|
||||||
@ -151,7 +150,7 @@ internal class RoomEventCreatorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `given edited event which relates to a room reply event then only updates message`() = runTest {
|
fun `given edited event which relates to a room reply event then only updates message`() = runTest {
|
||||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||||
val originalMessage = aRoomReplyMessageEvent(message = aRoomMessageEvent())
|
val originalMessage = aRoomReplyMessageEvent(message = aMatrixRoomMessageEvent())
|
||||||
val editedMessage = (originalMessage.message as RoomEvent.Message).toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
val editedMessage = (originalMessage.message as RoomEvent.Message).toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||||
val lookup = givenLookup(originalMessage)
|
val lookup = givenLookup(originalMessage)
|
||||||
|
|
||||||
@ -159,7 +158,7 @@ internal class RoomEventCreatorTest {
|
|||||||
|
|
||||||
result shouldBeEqualTo aRoomReplyMessageEvent(
|
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||||
replyingTo = originalMessage.replyingTo,
|
replyingTo = originalMessage.replyingTo,
|
||||||
message = aRoomMessageEvent(
|
message = aMatrixRoomMessageEvent(
|
||||||
eventId = originalMessage.eventId,
|
eventId = originalMessage.eventId,
|
||||||
utcTimestamp = editedMessage.utcTimestamp,
|
utcTimestamp = editedMessage.utcTimestamp,
|
||||||
content = A_TEXT_EVENT_MESSAGE,
|
content = A_TEXT_EVENT_MESSAGE,
|
||||||
@ -182,7 +181,7 @@ internal class RoomEventCreatorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given edited event is older than related room event then ignores edit`() = runTest {
|
fun `given edited event is older than related room event then ignores edit`() = runTest {
|
||||||
val originalMessage = aRoomMessageEvent(utcTimestamp = 1000)
|
val originalMessage = aMatrixRoomMessageEvent(utcTimestamp = 1000)
|
||||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
|
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||||
val lookup = givenLookup(originalMessage)
|
val lookup = givenLookup(originalMessage)
|
||||||
|
|
||||||
@ -199,7 +198,7 @@ internal class RoomEventCreatorTest {
|
|||||||
println(replyEvent.content)
|
println(replyEvent.content)
|
||||||
val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
||||||
|
|
||||||
result shouldBeEqualTo aRoomMessageEvent(
|
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||||
eventId = replyEvent.id,
|
eventId = replyEvent.id,
|
||||||
utcTimestamp = replyEvent.utcTimestamp,
|
utcTimestamp = replyEvent.utcTimestamp,
|
||||||
content = replyEvent.asTextContent().body!!,
|
content = replyEvent.asTextContent().body!!,
|
||||||
@ -217,13 +216,13 @@ internal class RoomEventCreatorTest {
|
|||||||
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||||
|
|
||||||
result shouldBeEqualTo aRoomReplyMessageEvent(
|
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||||
replyingTo = aRoomMessageEvent(
|
replyingTo = aMatrixRoomMessageEvent(
|
||||||
eventId = originalMessage.id,
|
eventId = originalMessage.id,
|
||||||
utcTimestamp = originalMessage.utcTimestamp,
|
utcTimestamp = originalMessage.utcTimestamp,
|
||||||
content = originalMessage.asTextContent().body!!,
|
content = originalMessage.asTextContent().body!!,
|
||||||
author = A_SENDER,
|
author = A_SENDER,
|
||||||
),
|
),
|
||||||
message = aRoomMessageEvent(
|
message = aMatrixRoomMessageEvent(
|
||||||
eventId = replyMessage.id,
|
eventId = replyMessage.id,
|
||||||
utcTimestamp = replyMessage.utcTimestamp,
|
utcTimestamp = replyMessage.utcTimestamp,
|
||||||
content = A_REPLY_EVENT_MESSAGE,
|
content = A_REPLY_EVENT_MESSAGE,
|
||||||
@ -235,7 +234,7 @@ internal class RoomEventCreatorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `given reply event which relates to a room event then maps to reply`() = runTest {
|
fun `given reply event which relates to a room event then maps to reply`() = runTest {
|
||||||
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
|
||||||
val originalMessage = aRoomMessageEvent()
|
val originalMessage = aMatrixRoomMessageEvent()
|
||||||
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
|
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
|
||||||
val lookup = givenLookup(originalMessage)
|
val lookup = givenLookup(originalMessage)
|
||||||
|
|
||||||
@ -243,7 +242,7 @@ internal class RoomEventCreatorTest {
|
|||||||
|
|
||||||
result shouldBeEqualTo aRoomReplyMessageEvent(
|
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||||
replyingTo = originalMessage,
|
replyingTo = originalMessage,
|
||||||
message = aRoomMessageEvent(
|
message = aMatrixRoomMessageEvent(
|
||||||
eventId = replyMessage.id,
|
eventId = replyMessage.id,
|
||||||
utcTimestamp = replyMessage.utcTimestamp,
|
utcTimestamp = replyMessage.utcTimestamp,
|
||||||
content = A_REPLY_EVENT_MESSAGE,
|
content = A_REPLY_EVENT_MESSAGE,
|
||||||
@ -263,7 +262,7 @@ internal class RoomEventCreatorTest {
|
|||||||
|
|
||||||
result shouldBeEqualTo aRoomReplyMessageEvent(
|
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||||
replyingTo = originalMessage.message,
|
replyingTo = originalMessage.message,
|
||||||
message = aRoomMessageEvent(
|
message = aMatrixRoomMessageEvent(
|
||||||
eventId = replyMessage.id,
|
eventId = replyMessage.id,
|
||||||
utcTimestamp = replyMessage.utcTimestamp,
|
utcTimestamp = replyMessage.utcTimestamp,
|
||||||
content = A_REPLY_EVENT_MESSAGE,
|
content = A_REPLY_EVENT_MESSAGE,
|
||||||
|
@ -5,8 +5,8 @@ import app.dapk.st.matrix.sync.RoomEvent
|
|||||||
import app.dapk.st.matrix.sync.RoomState
|
import app.dapk.st.matrix.sync.RoomState
|
||||||
import fake.FakeMatrixLogger
|
import fake.FakeMatrixLogger
|
||||||
import fake.FakeRoomDataSource
|
import fake.FakeRoomDataSource
|
||||||
import internalfake.FakeRoomEventsDecrypter
|
|
||||||
import fixture.*
|
import fixture.*
|
||||||
|
import internalfake.FakeRoomEventsDecrypter
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -15,13 +15,14 @@ import test.expect
|
|||||||
private val A_ROOM_ID = aRoomId()
|
private val A_ROOM_ID = aRoomId()
|
||||||
|
|
||||||
private object ARoom {
|
private object ARoom {
|
||||||
val MESSAGE_EVENT = aRoomMessageEvent(utcTimestamp = 0)
|
val MESSAGE_EVENT = aMatrixRoomMessageEvent(utcTimestamp = 0)
|
||||||
val ENCRYPTED_EVENT = anEncryptedRoomMessageEvent(utcTimestamp = 1)
|
val ENCRYPTED_EVENT = anEncryptedRoomMessageEvent(utcTimestamp = 1)
|
||||||
val DECRYPTED_EVENT = aRoomMessageEvent(utcTimestamp = 2)
|
val DECRYPTED_EVENT = aMatrixRoomMessageEvent(utcTimestamp = 2)
|
||||||
val PREVIOUS_STATE = RoomState(aRoomOverview(), listOf(MESSAGE_EVENT, ENCRYPTED_EVENT))
|
val PREVIOUS_STATE = RoomState(aMatrixRoomOverview(), listOf(MESSAGE_EVENT, ENCRYPTED_EVENT))
|
||||||
val DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT)
|
val DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT)
|
||||||
val NEW_STATE = RoomState(aRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS)
|
val NEW_STATE = RoomState(aMatrixRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val A_USER_CREDENTIALS = aUserCredentials()
|
private val A_USER_CREDENTIALS = aUserCredentials()
|
||||||
|
|
||||||
internal class RoomRefresherTest {
|
internal class RoomRefresherTest {
|
||||||
|
@ -19,7 +19,7 @@ private val A_ROOM_ID = aRoomId()
|
|||||||
private val ANY_LOOKUP_RESULT = LookupResult(anApiTimelineTextEvent(), roomEvent = null)
|
private val ANY_LOOKUP_RESULT = LookupResult(anApiTimelineTextEvent(), roomEvent = null)
|
||||||
private val AN_ENCRYPTED_TIMELINE_EVENT = anEncryptedApiTimelineEvent()
|
private val AN_ENCRYPTED_TIMELINE_EVENT = anEncryptedApiTimelineEvent()
|
||||||
private val A_TEXT_TIMELINE_EVENT = anApiTimelineTextEvent()
|
private val A_TEXT_TIMELINE_EVENT = anApiTimelineTextEvent()
|
||||||
private val A_MESSAGE_ROOM_EVENT = aRoomMessageEvent(anEventId("a-message"))
|
private val A_MESSAGE_ROOM_EVENT = aMatrixRoomMessageEvent(anEventId("a-message"))
|
||||||
private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message"))
|
private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message"))
|
||||||
private val A_LOOKUP_EVENT_ID = anEventId("lookup-id")
|
private val A_LOOKUP_EVENT_ID = anEventId("lookup-id")
|
||||||
private val A_USER_CREDENTIALS = aUserCredentials()
|
private val A_USER_CREDENTIALS = aUserCredentials()
|
||||||
@ -52,7 +52,7 @@ class TimelineEventsProcessorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given encrypted and text timeline events when processing then maps to room events`() = runTest {
|
fun `given encrypted and text timeline events when processing then maps to room events`() = runTest {
|
||||||
val previousEvents = listOf(aRoomMessageEvent(eventId = anEventId("previous-event")))
|
val previousEvents = listOf(aMatrixRoomMessageEvent(eventId = anEventId("previous-event")))
|
||||||
val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT)
|
val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT)
|
||||||
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
|
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
|
||||||
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
|
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user