Merge pull request #169 from ouchadam/release-candidate
[Auto] Release Candidate
This commit is contained in:
commit
0a6c4df6c4
|
@ -34,11 +34,14 @@ jobs:
|
||||||
touch .secrets/service-account.json
|
touch .secrets/service-account.json
|
||||||
touch .secrets/matrix.json
|
touch .secrets/matrix.json
|
||||||
echo -n '${{ secrets.UPLOAD_KEY }}' | base64 --decode >> .secrets/upload-key.jks
|
echo -n '${{ secrets.UPLOAD_KEY }}' | base64 --decode >> .secrets/upload-key.jks
|
||||||
|
echo -n '${{ secrets.FDROID_KEY }}' | base64 --decode >> .secrets/fdroid.keystore
|
||||||
echo -n '${{ secrets.SERVICE_ACCOUNT }}' | base64 --decode >> .secrets/service-account.json
|
echo -n '${{ secrets.SERVICE_ACCOUNT }}' | base64 --decode >> .secrets/service-account.json
|
||||||
echo -n '${{ secrets.MATRIX }}' | base64 --decode >> .secrets/matrix.json
|
echo -n '${{ secrets.MATRIX }}' | base64 --decode >> .secrets/matrix.json
|
||||||
|
|
||||||
- name: Assemble release variant
|
- name: Assemble release variant
|
||||||
run: ./tools/generate-release.sh ${{ secrets.STORE_PASS }}
|
run: |
|
||||||
|
./tools/generate-release.sh ${{ secrets.STORE_PASS }}
|
||||||
|
./tools/generate-fdroid-release.sh ${{ secrets.FDROID_STORE_PASS }}
|
||||||
|
|
||||||
- uses: actions/github-script@v6
|
- uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
|
@ -48,6 +51,7 @@ jobs:
|
||||||
const artifacts = {
|
const artifacts = {
|
||||||
bundle: '${{ github.workspace }}/app/build/outputs/bundle/release/app-release.aab',
|
bundle: '${{ github.workspace }}/app/build/outputs/bundle/release/app-release.aab',
|
||||||
mapping: '${{ github.workspace }}/app/build/outputs/mapping/release/mapping.txt',
|
mapping: '${{ github.workspace }}/app/build/outputs/mapping/release/mapping.txt',
|
||||||
|
fossApkPath: '${{ github.workspace }}/app/build/outputs/apk/release/app-foss-release-signed.apk',
|
||||||
}
|
}
|
||||||
await publishRelease(github, artifacts)
|
await publishRelease(github, artifacts)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: Nightly
|
name: Release Train
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="app.dapk.st">
|
package="app.dapk.st">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="app.dapk.st.SmallTalkApplication"
|
android:name="app.dapk.st.SmallTalkApplication"
|
||||||
|
@ -17,13 +19,13 @@
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:targetActivity="app.dapk.st.home.MainActivity">
|
android:targetActivity="app.dapk.st.home.MainActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts"/>
|
||||||
|
|
||||||
</activity-alias>
|
</activity-alias>
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import app.dapk.st.graph.AppModule
|
||||||
import app.dapk.st.home.HomeModule
|
import app.dapk.st.home.HomeModule
|
||||||
import app.dapk.st.login.LoginModule
|
import app.dapk.st.login.LoginModule
|
||||||
import app.dapk.st.messenger.MessengerModule
|
import app.dapk.st.messenger.MessengerModule
|
||||||
|
import app.dapk.st.messenger.gallery.ImageGalleryModule
|
||||||
import app.dapk.st.notifications.NotificationsModule
|
import app.dapk.st.notifications.NotificationsModule
|
||||||
import app.dapk.st.profile.ProfileModule
|
import app.dapk.st.profile.ProfileModule
|
||||||
import app.dapk.st.push.PushModule
|
import app.dapk.st.push.PushModule
|
||||||
|
@ -54,7 +55,9 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
||||||
|
|
||||||
private fun onApplicationLaunch(notificationsModule: NotificationsModule, storeModule: StoreModule) {
|
private fun onApplicationLaunch(notificationsModule: NotificationsModule, storeModule: StoreModule) {
|
||||||
applicationScope.launch {
|
applicationScope.launch {
|
||||||
featureModules.pushModule.pushTokenRegistrar().registerCurrentToken()
|
storeModule.credentialsStore().credentials()?.let {
|
||||||
|
featureModules.pushModule.pushTokenRegistrar().registerCurrentToken()
|
||||||
|
}
|
||||||
storeModule.localEchoStore.preload()
|
storeModule.localEchoStore.preload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +82,7 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
||||||
TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule
|
TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule
|
||||||
CoreAndroidModule::class -> appModule.coreAndroidModule
|
CoreAndroidModule::class -> appModule.coreAndroidModule
|
||||||
ShareEntryModule::class -> featureModules.shareEntryModule
|
ShareEntryModule::class -> featureModules.shareEntryModule
|
||||||
|
ImageGalleryModule::class -> featureModules.imageGalleryModule
|
||||||
else -> throw IllegalArgumentException("Unknown: $klass")
|
else -> throw IllegalArgumentException("Unknown: $klass")
|
||||||
} as T
|
} as T
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent
|
||||||
import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
|
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.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.MatrixPushHandler
|
||||||
|
@ -217,6 +218,10 @@ internal class FeatureModules internal constructor(
|
||||||
ShareEntryModule(matrixModules.sync, matrixModules.room)
|
ShareEntryModule(matrixModules.sync, matrixModules.room)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val imageGalleryModule by unsafeLazy {
|
||||||
|
ImageGalleryModule(context.contentResolver, coroutineDispatchers)
|
||||||
|
}
|
||||||
|
|
||||||
val pushModule by unsafeLazy {
|
val pushModule by unsafeLazy {
|
||||||
domainModules.pushModule
|
domainModules.pushModule
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,7 +132,7 @@ ext.kotlinTest = { dependencies ->
|
||||||
dependencies.testImplementation Dependencies.mavenCentral.kluent
|
dependencies.testImplementation Dependencies.mavenCentral.kluent
|
||||||
dependencies.testImplementation Dependencies.mavenCentral.kotlinTest
|
dependencies.testImplementation Dependencies.mavenCentral.kotlinTest
|
||||||
dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10"
|
dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10"
|
||||||
dependencies.testImplementation 'io.mockk:mockk:1.12.8'
|
dependencies.testImplementation 'io.mockk:mockk:1.13.2'
|
||||||
dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||||
|
|
||||||
dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
|
dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1'
|
||||||
|
@ -140,7 +140,7 @@ ext.kotlinTest = { dependencies ->
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.kotlinFixtures = { dependencies ->
|
ext.kotlinFixtures = { dependencies ->
|
||||||
dependencies.testFixturesImplementation 'io.mockk:mockk:1.12.7'
|
dependencies.testFixturesImplementation 'io.mockk:mockk:1.13.1'
|
||||||
dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent
|
dependencies.testFixturesImplementation Dependencies.mavenCentral.kluent
|
||||||
dependencies.testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
dependencies.testFixturesImplementation Dependencies.mavenCentral.kotlinCoroutinesCore
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,10 +82,12 @@ ext.Dependencies.with {
|
||||||
includeGroup "io.ktor"
|
includeGroup "io.ktor"
|
||||||
includeGroup "io.coil-kt"
|
includeGroup "io.coil-kt"
|
||||||
includeGroup "io.mockk"
|
includeGroup "io.mockk"
|
||||||
|
includeGroup "io.perfmark"
|
||||||
includeGroup "info.picocli"
|
includeGroup "info.picocli"
|
||||||
includeGroup "us.fatehi"
|
includeGroup "us.fatehi"
|
||||||
includeGroup "jakarta.xml.bind"
|
includeGroup "jakarta.xml.bind"
|
||||||
includeGroup "jakarta.activation"
|
includeGroup "jakarta.activation"
|
||||||
|
includeGroup "javax.annotation"
|
||||||
includeGroup "javax.inject"
|
includeGroup "javax.inject"
|
||||||
includeGroup "junit"
|
includeGroup "junit"
|
||||||
includeGroup "jline"
|
includeGroup "jline"
|
||||||
|
@ -102,14 +104,14 @@ ext.Dependencies.with {
|
||||||
|
|
||||||
google = new DependenciesContainer()
|
google = new DependenciesContainer()
|
||||||
google.with {
|
google.with {
|
||||||
androidGradlePlugin = "com.android.tools.build:gradle:7.2.2"
|
androidGradlePlugin = "com.android.tools.build:gradle:7.3.0"
|
||||||
|
|
||||||
androidxComposeUi = "androidx.compose.ui:ui:${composeVer}"
|
androidxComposeUi = "androidx.compose.ui:ui:${composeVer}"
|
||||||
androidxComposeFoundation = "androidx.compose.foundation:foundation:${composeVer}"
|
androidxComposeFoundation = "androidx.compose.foundation:foundation:${composeVer}"
|
||||||
androidxComposeMaterial = "androidx.compose.material3:material3:1.0.0-beta01"
|
androidxComposeMaterial = "androidx.compose.material3:material3:1.0.0-beta01"
|
||||||
androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}"
|
androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}"
|
||||||
androidxActivityCompose = "androidx.activity:activity-compose:1.4.0"
|
androidxActivityCompose = "androidx.activity:activity-compose:1.4.0"
|
||||||
kotlinCompilerExtensionVersion = "1.3.0"
|
kotlinCompilerExtensionVersion = "1.3.1"
|
||||||
|
|
||||||
firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.1"
|
firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.1"
|
||||||
jdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5"
|
jdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5"
|
||||||
|
@ -143,7 +145,7 @@ ext.Dependencies.with {
|
||||||
|
|
||||||
junit = "junit:junit:4.13.2"
|
junit = "junit:junit:4.13.2"
|
||||||
kluent = "org.amshove.kluent:kluent:1.68"
|
kluent = "org.amshove.kluent:kluent:1.68"
|
||||||
mockk = 'io.mockk:mockk:1.12.8'
|
mockk = 'io.mockk:mockk:1.13.2'
|
||||||
|
|
||||||
matrixOlm = "org.matrix.android:olm-sdk:3.2.12"
|
matrixOlm = "org.matrix.android:olm-sdk:3.2.12"
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,8 @@ private fun createExtended(scheme: ColorScheme) = ExtendedColors(
|
||||||
onSelfBubble = scheme.onPrimary,
|
onSelfBubble = scheme.onPrimary,
|
||||||
othersBubble = scheme.secondaryContainer,
|
othersBubble = scheme.secondaryContainer,
|
||||||
onOthersBubble = scheme.onSecondaryContainer,
|
onOthersBubble = scheme.onSecondaryContainer,
|
||||||
selfBubbleReplyBackground = Color(0x40EAEAEA),
|
selfBubbleReplyBackground = scheme.primary.copy(alpha = 0.2f),
|
||||||
otherBubbleReplyBackground = Color(0x20EAEAEA),
|
otherBubbleReplyBackground = scheme.primary.copy(alpha = 0.2f),
|
||||||
missingImageColors = listOf(
|
missingImageColors = listOf(
|
||||||
Color(0xFFf7c7f7) to Color(0xFFdf20de),
|
Color(0xFFf7c7f7) to Color(0xFFdf20de),
|
||||||
Color(0xFFe5d7f6) to Color(0xFF7b30cf),
|
Color(0xFFe5d7f6) to Color(0xFF7b30cf),
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
package app.dapk.st.core
|
package app.dapk.st.core
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import app.dapk.st.core.extensions.unsafeLazy
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
import app.dapk.st.design.components.SmallTalkTheme
|
import app.dapk.st.design.components.SmallTalkTheme
|
||||||
import app.dapk.st.design.components.ThemeConfig
|
import app.dapk.st.design.components.ThemeConfig
|
||||||
import app.dapk.st.navigator.navigator
|
import app.dapk.st.navigator.navigator
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlin.coroutines.resume
|
||||||
import androidx.activity.compose.setContent as _setContent
|
import androidx.activity.compose.setContent as _setContent
|
||||||
|
|
||||||
abstract class DapkActivity : ComponentActivity(), EffectScope {
|
abstract class DapkActivity : ComponentActivity(), EffectScope {
|
||||||
|
@ -27,7 +31,6 @@ abstract class DapkActivity : ComponentActivity(), EffectScope {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
this.themeConfig = ThemeConfig(themeStore.isMaterialYouEnabled())
|
this.themeConfig = ThemeConfig(themeStore.isMaterialYouEnabled())
|
||||||
|
|
||||||
|
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
||||||
}
|
}
|
||||||
|
@ -58,10 +61,40 @@ abstract class DapkActivity : ComponentActivity(), EffectScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (needsBackLeakWorkaround && !onBackPressedDispatcher.hasEnabledCallbacks()) {
|
if (needsBackLeakWorkaround && !onBackPressedDispatcher.hasEnabledCallbacks()) {
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
} else
|
} else {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected suspend fun ensurePermission(permission: String): PermissionResult {
|
||||||
|
return when {
|
||||||
|
checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED -> PermissionResult.Granted
|
||||||
|
|
||||||
|
shouldShowRequestPermissionRationale(permission) -> PermissionResult.ShowRational
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val isGranted = suspendCancellableCoroutine { continuation ->
|
||||||
|
val callback: (result: Boolean) -> Unit = { result -> continuation.resume(result) }
|
||||||
|
val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission(), callback)
|
||||||
|
launcher.launch(permission)
|
||||||
|
continuation.invokeOnCancellation { launcher.unregister() }
|
||||||
|
}
|
||||||
|
|
||||||
|
when (isGranted) {
|
||||||
|
true -> PermissionResult.Granted
|
||||||
|
false -> PermissionResult.Denied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed interface PermissionResult {
|
||||||
|
object Granted : PermissionResult
|
||||||
|
object ShowRational : PermissionResult
|
||||||
|
object Denied : PermissionResult
|
||||||
|
}
|
||||||
|
|
|
@ -3,5 +3,5 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly 'org.json:json:20220320'
|
compileOnly 'org.json:json:20220924'
|
||||||
}
|
}
|
|
@ -170,6 +170,8 @@ class OlmWrapper(
|
||||||
val inBound = OlmInboundGroupSession(roomCryptoSession.key)
|
val inBound = OlmInboundGroupSession(roomCryptoSession.key)
|
||||||
olmStore.persist(roomCryptoSession.id, inBound)
|
olmStore.persist(roomCryptoSession.id, inBound)
|
||||||
|
|
||||||
|
logger.crypto("Creating megolm: ${roomCryptoSession.id}")
|
||||||
|
|
||||||
return roomCryptoSession
|
return roomCryptoSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,7 +183,7 @@ class OlmWrapper(
|
||||||
private suspend fun deviceCrypto(input: OlmSessionInput): DeviceCryptoSession? {
|
private suspend fun deviceCrypto(input: OlmSessionInput): DeviceCryptoSession? {
|
||||||
return olmStore.readSessions(listOf(input.identity))?.let {
|
return olmStore.readSessions(listOf(input.identity))?.let {
|
||||||
DeviceCryptoSession(
|
DeviceCryptoSession(
|
||||||
input.deviceId, input.userId, input.identity, input.fingerprint, it
|
input.deviceId, input.userId, input.identity, input.fingerprint, it.map { it.second }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,10 +27,10 @@ internal class RoomPersistence(
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : RoomStore {
|
) : RoomStore {
|
||||||
|
|
||||||
override suspend fun persist(roomId: RoomId, state: RoomState) {
|
override suspend fun persist(roomId: RoomId, events: List<RoomEvent>) {
|
||||||
coroutineDispatchers.withIoContext {
|
coroutineDispatchers.withIoContext {
|
||||||
database.transaction {
|
database.transaction {
|
||||||
state.events.forEach {
|
events.forEach {
|
||||||
database.roomEventQueries.insertRoomEvent(roomId, it)
|
database.roomEventQueries.insertRoomEvent(roomId, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,9 +38,16 @@ internal class RoomPersistence(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun remove(rooms: List<RoomId>) {
|
override suspend fun remove(rooms: List<RoomId>) {
|
||||||
coroutineDispatchers
|
coroutineDispatchers.withIoContext {
|
||||||
database.roomEventQueries.transaction {
|
database.roomEventQueries.transaction {
|
||||||
rooms.forEach { database.roomEventQueries.remove(it.value) }
|
rooms.forEach { database.roomEventQueries.remove(it.value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(eventId: EventId) {
|
||||||
|
coroutineDispatchers.withIoContext {
|
||||||
|
database.roomEventQueries.removeEvent(eventId.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,4 +36,8 @@ LIMIT 100;
|
||||||
|
|
||||||
remove:
|
remove:
|
||||||
DELETE FROM dbRoomEvent
|
DELETE FROM dbRoomEvent
|
||||||
WHERE room_id = ?;
|
WHERE room_id = ?;
|
||||||
|
|
||||||
|
removeEvent:
|
||||||
|
DELETE FROM dbRoomEvent
|
||||||
|
WHERE event_id = ?;
|
|
@ -13,4 +13,5 @@ dependencies {
|
||||||
implementation project(':domains:store')
|
implementation project(':domains:store')
|
||||||
implementation project(":core")
|
implementation project(":core")
|
||||||
implementation project(":design-library")
|
implementation project(":design-library")
|
||||||
|
implementation Dependencies.mavenCentral.coil
|
||||||
}
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.dapk.st.home">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.dapk.st.home">
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
<activity android:name="app.dapk.st.home.MainActivity"/>
|
<activity android:name="app.dapk.st.home.MainActivity"/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -27,6 +27,7 @@ class MainActivity : DapkActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
homeViewModel.events.onEach {
|
homeViewModel.events.onEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
HomeEvent.Relaunch -> recreate()
|
HomeEvent.Relaunch -> recreate()
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="app.dapk.st.messenger">
|
package="app.dapk.st.messenger">
|
||||||
|
|
||||||
<application>
|
<application>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MessengerActivity"
|
android:name=".MessengerActivity"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize"/>
|
||||||
|
|
||||||
<activity android:name=".roomsettings.RoomSettingsActivity" />
|
<activity android:name=".roomsettings.RoomSettingsActivity"/>
|
||||||
|
<activity android:name=".gallery.ImageGalleryActivity"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package app.dapk.st.messenger
|
package app.dapk.st.messenger
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Environment
|
||||||
import app.dapk.st.core.Base64
|
import app.dapk.st.core.Base64
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.crypto.MediaDecrypter
|
import app.dapk.st.matrix.crypto.MediaDecrypter
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
|
@ -14,14 +16,16 @@ import coil.request.Options
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.Buffer
|
import okio.BufferedSource
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class DecryptingFetcherFactory(private val context: Context, base64: Base64) : Fetcher.Factory<RoomEvent.Image> {
|
class DecryptingFetcherFactory(private val context: Context, base64: Base64, private val roomId: RoomId) : Fetcher.Factory<RoomEvent.Image> {
|
||||||
|
|
||||||
private val mediaDecrypter = MediaDecrypter(base64)
|
private val mediaDecrypter = MediaDecrypter(base64)
|
||||||
|
|
||||||
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)
|
return DecryptingFetcher(data, context, mediaDecrypter, roomId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,23 +35,48 @@ class DecryptingFetcher(
|
||||||
private val data: RoomEvent.Image,
|
private val data: RoomEvent.Image,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val mediaDecrypter: MediaDecrypter,
|
private val mediaDecrypter: MediaDecrypter,
|
||||||
|
roomId: RoomId,
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
|
|
||||||
override suspend fun fetch(): FetchResult {
|
private val directory by lazy {
|
||||||
val response = http.newCall(Request.Builder().url(data.imageMeta.url).build()).execute()
|
context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!.resolve("SmallTalk/${roomId.value}").also { it.mkdirs() }
|
||||||
val outputStream = when {
|
|
||||||
data.imageMeta.keys != null -> handleEncrypted(response, data.imageMeta.keys!!)
|
|
||||||
else -> response.body?.source() ?: throw IllegalArgumentException("No bitmap response found")
|
|
||||||
}
|
|
||||||
return SourceResult(ImageSource(outputStream, context), null, DataSource.NETWORK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEncrypted(response: Response, keys: RoomEvent.Image.ImageMeta.Keys): Buffer {
|
override suspend fun fetch(): FetchResult {
|
||||||
return response.body?.byteStream()?.let { byteStream ->
|
val diskCacheKey = data.imageMeta.url.hashCode().toString()
|
||||||
Buffer().also { buffer ->
|
val diskCachedFile = directory.resolve(diskCacheKey)
|
||||||
mediaDecrypter.decrypt(byteStream, keys.k, keys.iv).collect { buffer.write(it) }
|
val path = diskCachedFile.toOkioPath()
|
||||||
|
|
||||||
|
return when {
|
||||||
|
diskCachedFile.exists() -> SourceResult(ImageSource(path), null, DataSource.DISK)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
diskCachedFile.createNewFile()
|
||||||
|
val response = http.newCall(Request.Builder().url(data.imageMeta.url).build()).execute()
|
||||||
|
when {
|
||||||
|
data.imageMeta.keys != null -> response.writeDecrypted(diskCachedFile, data.imageMeta.keys!!)
|
||||||
|
else -> response.body?.source()?.writeToFile(diskCachedFile) ?: throw IllegalArgumentException("No bitmap response found")
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceResult(ImageSource(path), null, DataSource.NETWORK)
|
||||||
}
|
}
|
||||||
} ?: Buffer()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Response.writeDecrypted(file: File, keys: RoomEvent.Image.ImageMeta.Keys) {
|
||||||
|
this.body?.byteStream()?.let { byteStream ->
|
||||||
|
file.outputStream().use { output ->
|
||||||
|
mediaDecrypter.decrypt(byteStream, keys.k, keys.iv).collect { output.write(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BufferedSource.writeToFile(file: File) {
|
||||||
|
this.inputStream().use { input ->
|
||||||
|
file.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,16 +5,16 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import app.dapk.st.core.DapkActivity
|
import app.dapk.st.core.*
|
||||||
import app.dapk.st.core.extensions.unsafeLazy
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
import app.dapk.st.core.module
|
|
||||||
import app.dapk.st.core.viewModel
|
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
|
import app.dapk.st.messenger.gallery.GetImageFromGallery
|
||||||
import app.dapk.st.navigator.MessageAttachment
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@ -50,11 +50,26 @@ class MessengerActivity : DapkActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val payload = readPayload<MessagerActivityPayload>()
|
val payload = readPayload<MessagerActivityPayload>()
|
||||||
val factory = module.decryptingFetcherFactory()
|
val factory = module.decryptingFetcherFactory(RoomId(payload.roomId))
|
||||||
|
|
||||||
|
val galleryLauncher = registerForActivityResult(GetImageFromGallery()) {
|
||||||
|
it?.let { uri ->
|
||||||
|
viewModel.post(
|
||||||
|
MessengerAction.ComposerImageUpdate(
|
||||||
|
MessageAttachment(
|
||||||
|
AndroidUri(it.toString()),
|
||||||
|
MimeType.Image,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
Surface(Modifier.fillMaxSize()) {
|
Surface(Modifier.fillMaxSize()) {
|
||||||
CompositionLocalProvider(LocalDecyptingFetcherFactory provides factory) {
|
CompositionLocalProvider(LocalDecyptingFetcherFactory provides factory) {
|
||||||
MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator)
|
MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator, galleryLauncher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import app.dapk.st.core.Base64
|
import app.dapk.st.core.Base64
|
||||||
import app.dapk.st.core.ProvidableModule
|
import app.dapk.st.core.ProvidableModule
|
||||||
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.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.RoomStore
|
import app.dapk.st.matrix.sync.RoomStore
|
||||||
|
@ -30,5 +31,5 @@ class MessengerModule(
|
||||||
return TimelineUseCaseImpl(syncService, messageService, roomService, mergeWithLocalEchosUseCase)
|
return TimelineUseCaseImpl(syncService, messageService, roomService, mergeWithLocalEchosUseCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun decryptingFetcherFactory() = DecryptingFetcherFactory(context, base64)
|
internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, base64, roomId)
|
||||||
}
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
package app.dapk.st.messenger
|
package app.dapk.st.messenger
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.*
|
import androidx.compose.foundation.lazy.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
@ -13,6 +15,7 @@ import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Image
|
||||||
import androidx.compose.material.icons.filled.Send
|
import androidx.compose.material.icons.filled.Send
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
@ -33,6 +36,7 @@ import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.core.LifecycleEffect
|
import app.dapk.st.core.LifecycleEffect
|
||||||
import app.dapk.st.core.StartObserving
|
import app.dapk.st.core.StartObserving
|
||||||
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.design.components.*
|
import app.dapk.st.design.components.*
|
||||||
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
|
||||||
|
@ -40,6 +44,7 @@ import app.dapk.st.matrix.sync.MessageMeta
|
||||||
import app.dapk.st.matrix.sync.RoomEvent
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
import app.dapk.st.matrix.sync.RoomEvent.Message
|
import app.dapk.st.matrix.sync.RoomEvent.Message
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
import app.dapk.st.matrix.sync.RoomState
|
||||||
|
import app.dapk.st.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
|
||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
@ -47,10 +52,16 @@ import coil.request.ImageRequest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun MessengerScreen(roomId: RoomId, attachments: List<MessageAttachment>?, viewModel: MessengerViewModel, navigator: Navigator) {
|
internal fun MessengerScreen(
|
||||||
|
roomId: RoomId,
|
||||||
|
attachments: List<MessageAttachment>?,
|
||||||
|
viewModel: MessengerViewModel,
|
||||||
|
navigator: Navigator,
|
||||||
|
galleryLauncher: ActivityResultLauncher<ImageGalleryActivityPayload>
|
||||||
|
) {
|
||||||
val state = viewModel.state
|
val state = viewModel.state
|
||||||
|
|
||||||
viewModel.ObserveEvents()
|
viewModel.ObserveEvents(galleryLauncher)
|
||||||
LifecycleEffect(
|
LifecycleEffect(
|
||||||
onStart = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) },
|
onStart = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) },
|
||||||
onStop = { viewModel.post(MessengerAction.OnMessengerGone) }
|
onStop = { viewModel.post(MessengerAction.OnMessengerGone) }
|
||||||
|
@ -63,9 +74,9 @@ internal fun MessengerScreen(roomId: RoomId, attachments: List<MessageAttachment
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = {
|
Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = {
|
||||||
OverflowMenu {
|
// OverflowMenu {
|
||||||
DropdownMenuItem(text = { Text("Settings", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = {})
|
// DropdownMenuItem(text = { Text("Settings", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = {})
|
||||||
}
|
// }
|
||||||
})
|
})
|
||||||
when (state.composerState) {
|
when (state.composerState) {
|
||||||
is ComposerState.Text -> {
|
is ComposerState.Text -> {
|
||||||
|
@ -74,6 +85,7 @@ internal fun MessengerScreen(roomId: RoomId, attachments: List<MessageAttachment
|
||||||
state.composerState,
|
state.composerState,
|
||||||
onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) },
|
onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) },
|
||||||
onSend = { viewModel.post(MessengerAction.ComposerSendText) },
|
onSend = { viewModel.post(MessengerAction.ComposerSendText) },
|
||||||
|
onAttach = { viewModel.startAttachment() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,10 +101,16 @@ internal fun MessengerScreen(roomId: RoomId, attachments: List<MessageAttachment
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessengerViewModel.ObserveEvents() {
|
private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLauncher<ImageGalleryActivityPayload>) {
|
||||||
StartObserving {
|
StartObserving {
|
||||||
this@ObserveEvents.events.launch {
|
this@ObserveEvents.events.launch {
|
||||||
// TODO()
|
when (it) {
|
||||||
|
MessengerEvent.SelectImageAttachment -> {
|
||||||
|
state.roomState.takeIfContent()?.let {
|
||||||
|
galleryLauncher.launch(ImageGalleryActivityPayload(it.roomState.roomOverview.roomName ?: ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,6 +276,7 @@ private fun MessageImage(content: BubbleContent<RoomEvent.Image>) {
|
||||||
painter = rememberAsyncImagePainter(
|
painter = rememberAsyncImagePainter(
|
||||||
model = ImageRequest.Builder(context)
|
model = ImageRequest.Builder(context)
|
||||||
.fetcherFactory(LocalDecyptingFetcherFactory.current)
|
.fetcherFactory(LocalDecyptingFetcherFactory.current)
|
||||||
|
.memoryCacheKey(content.message.imageMeta.url)
|
||||||
.data(content.message)
|
.data(content.message)
|
||||||
.build()
|
.build()
|
||||||
),
|
),
|
||||||
|
@ -408,6 +427,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.background(if (content.isNotSelf) SmallTalkTheme.extendedColors.otherBubbleReplyBackground else SmallTalkTheme.extendedColors.selfBubbleReplyBackground)
|
.background(if (content.isNotSelf) SmallTalkTheme.extendedColors.otherBubbleReplyBackground else SmallTalkTheme.extendedColors.selfBubbleReplyBackground)
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
) {
|
) {
|
||||||
|
@ -417,13 +437,13 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
text = replyName,
|
text = replyName,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
color = MaterialTheme.colorScheme.onPrimary
|
color = content.textColor()
|
||||||
)
|
)
|
||||||
when (val replyingTo = content.message.replyingTo) {
|
when (val replyingTo = content.message.replyingTo) {
|
||||||
is Message -> {
|
is Message -> {
|
||||||
Text(
|
Text(
|
||||||
text = replyingTo.content,
|
text = replyingTo.content,
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
color = content.textColor(),
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
modifier = Modifier.wrapContentSize(),
|
modifier = Modifier.wrapContentSize(),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
|
@ -437,6 +457,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
||||||
painter = rememberAsyncImagePainter(
|
painter = rememberAsyncImagePainter(
|
||||||
model = ImageRequest.Builder(context)
|
model = ImageRequest.Builder(context)
|
||||||
.fetcherFactory(LocalDecyptingFetcherFactory.current)
|
.fetcherFactory(LocalDecyptingFetcherFactory.current)
|
||||||
|
.memoryCacheKey(replyingTo.imageMeta.url)
|
||||||
.data(replyingTo)
|
.data(replyingTo)
|
||||||
.build()
|
.build()
|
||||||
),
|
),
|
||||||
|
@ -478,7 +499,8 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
||||||
modifier = Modifier.size(message.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)),
|
modifier = Modifier.size(message.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)),
|
||||||
painter = rememberAsyncImagePainter(
|
painter = rememberAsyncImagePainter(
|
||||||
model = ImageRequest.Builder(context)
|
model = ImageRequest.Builder(context)
|
||||||
.data(content.message)
|
.data(message)
|
||||||
|
.memoryCacheKey(message.imageMeta.url)
|
||||||
.fetcherFactory(LocalDecyptingFetcherFactory.current)
|
.fetcherFactory(LocalDecyptingFetcherFactory.current)
|
||||||
.build()
|
.build()
|
||||||
),
|
),
|
||||||
|
@ -550,7 +572,7 @@ private fun RowScope.SendStatus(message: RoomEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit) {
|
private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit, onAttach: () -> Unit) {
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -576,7 +598,17 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un
|
||||||
onValueChange = { onTextChange(it) },
|
onValueChange = { onTextChange(it) },
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
textStyle = LocalTextStyle.current.copy(color = SmallTalkTheme.extendedColors.onOthersBubble),
|
textStyle = LocalTextStyle.current.copy(color = SmallTalkTheme.extendedColors.onOthersBubble),
|
||||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true)
|
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences, autoCorrect = true),
|
||||||
|
decorationBox = { innerField ->
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Box(modifier = Modifier.weight(1f).padding(end = 4.dp)) { innerField() }
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.clickable { onAttach() }.wrapContentWidth().align(Alignment.Bottom),
|
||||||
|
imageVector = Icons.Filled.Image,
|
||||||
|
contentDescription = "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,9 @@ data class MessengerScreenState(
|
||||||
val composerState: ComposerState,
|
val composerState: ComposerState,
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface MessengerEvent
|
sealed interface MessengerEvent {
|
||||||
|
object SelectImageAttachment : MessengerEvent
|
||||||
|
}
|
||||||
|
|
||||||
sealed interface ComposerState {
|
sealed interface ComposerState {
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ internal class MessengerViewModel(
|
||||||
is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue)) }
|
is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue)) }
|
||||||
MessengerAction.ComposerSendText -> sendMessage()
|
MessengerAction.ComposerSendText -> sendMessage()
|
||||||
MessengerAction.ComposerClear -> updateState { copy(composerState = ComposerState.Text("")) }
|
MessengerAction.ComposerClear -> updateState { copy(composerState = ComposerState.Text("")) }
|
||||||
|
is MessengerAction.ComposerImageUpdate -> updateState { copy(composerState = ComposerState.Attachments(listOf(action.newValue))) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,6 +101,7 @@ internal class MessengerViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ComposerState.Attachments -> {
|
is ComposerState.Attachments -> {
|
||||||
val copy = composerState.copy()
|
val copy = composerState.copy()
|
||||||
updateState { copy(composerState = ComposerState.Text("")) }
|
updateState { copy(composerState = ComposerState.Text("")) }
|
||||||
|
@ -123,6 +125,12 @@ internal class MessengerViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startAttachment() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_events.emit(MessengerEvent.SelectImageAttachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events
|
private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events
|
||||||
|
@ -133,6 +141,7 @@ private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roo
|
||||||
|
|
||||||
sealed interface MessengerAction {
|
sealed interface MessengerAction {
|
||||||
data class ComposerTextUpdate(val newValue: String) : MessengerAction
|
data class ComposerTextUpdate(val newValue: String) : MessengerAction
|
||||||
|
data class ComposerImageUpdate(val newValue: MessageAttachment) : MessengerAction
|
||||||
object ComposerSendText : MessengerAction
|
object ComposerSendText : MessengerAction
|
||||||
object ComposerClear : MessengerAction
|
object ComposerClear : MessengerAction
|
||||||
data class OnMessengerVisible(val roomId: RoomId, val attachments: List<MessageAttachment>?) : MessengerAction
|
data class OnMessengerVisible(val roomId: RoomId, val attachments: List<MessageAttachment>?) : MessengerAction
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package app.dapk.st.messenger.gallery
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore.Images
|
||||||
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
import app.dapk.st.core.withIoContext
|
||||||
|
|
||||||
|
class FetchMediaFoldersUseCase(
|
||||||
|
private val contentResolver: ContentResolver,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun fetchFolders(): List<Folder> {
|
||||||
|
return dispatchers.withIoContext {
|
||||||
|
val projection = arrayOf(Images.Media._ID, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED)
|
||||||
|
val selection = "${isNotPending()} AND ${Images.Media.BUCKET_ID} AND ${Images.Media.MIME_TYPE} NOT LIKE ?"
|
||||||
|
val sortBy = "${Images.Media.BUCKET_DISPLAY_NAME} COLLATE NOCASE ASC, ${Images.Media.DATE_MODIFIED} DESC"
|
||||||
|
|
||||||
|
val folders = mutableMapOf<String, Folder>()
|
||||||
|
val contentUri = Images.Media.EXTERNAL_CONTENT_URI
|
||||||
|
contentResolver.query(contentUri, projection, selection, arrayOf("%image/svg%"), sortBy).use { cursor ->
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]))
|
||||||
|
val thumbnail = ContentUris.withAppendedId(contentUri, rowId)
|
||||||
|
val bucketId = cursor.getString(cursor.getColumnIndexOrThrow(projection[1]))
|
||||||
|
val title = cursor.getString(cursor.getColumnIndexOrThrow(projection[2])) ?: ""
|
||||||
|
val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(projection[3]))
|
||||||
|
val folder = folders.getOrPut(bucketId) { Folder(bucketId, title, thumbnail) }
|
||||||
|
folder.incrementItemCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
folders.values.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Folder(
|
||||||
|
val bucketId: String,
|
||||||
|
val title: String,
|
||||||
|
val thumbnail: Uri,
|
||||||
|
) {
|
||||||
|
private var _itemCount: Long = 0L
|
||||||
|
val itemCount: Long
|
||||||
|
get() = _itemCount
|
||||||
|
|
||||||
|
fun incrementItemCount() {
|
||||||
|
_itemCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package app.dapk.st.messenger.gallery
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
import app.dapk.st.core.withIoContext
|
||||||
|
|
||||||
|
class FetchMediaUseCase(private val contentResolver: ContentResolver, private val dispatchers: CoroutineDispatchers) {
|
||||||
|
|
||||||
|
private val projection = arrayOf(
|
||||||
|
MediaStore.Images.Media._ID,
|
||||||
|
MediaStore.Images.Media.MIME_TYPE,
|
||||||
|
MediaStore.Images.Media.DATE_MODIFIED,
|
||||||
|
MediaStore.Images.Media.ORIENTATION,
|
||||||
|
MediaStore.Images.Media.WIDTH,
|
||||||
|
MediaStore.Images.Media.HEIGHT,
|
||||||
|
MediaStore.Images.Media.SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
private val selection = MediaStore.Images.Media.BUCKET_ID + " = ? AND " + isNotPending() + " AND " + MediaStore.Images.Media.MIME_TYPE + " NOT LIKE ?"
|
||||||
|
|
||||||
|
suspend fun getMediaInBucket(bucketId: String): List<Media> {
|
||||||
|
|
||||||
|
return dispatchers.withIoContext {
|
||||||
|
val media = mutableListOf<Media>()
|
||||||
|
val selectionArgs = arrayOf(bucketId, "%image/svg%")
|
||||||
|
val sortBy = MediaStore.Images.Media.DATE_MODIFIED + " DESC"
|
||||||
|
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||||
|
contentResolver.query(contentUri, projection, selection, selectionArgs, sortBy).use { cursor ->
|
||||||
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
val rowId = cursor.getLong(cursor.getColumnIndexOrThrow(projection[0]))
|
||||||
|
val uri = ContentUris.withAppendedId(contentUri, rowId)
|
||||||
|
val mimetype = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE))
|
||||||
|
val date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED))
|
||||||
|
val orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.ORIENTATION))
|
||||||
|
val width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)))
|
||||||
|
val height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)))
|
||||||
|
val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE))
|
||||||
|
media.add(Media(rowId, uri, mimetype, width, height, size, date))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
media
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWidthColumn(orientation: Int) = if (orientation == 0 || orientation == 180) MediaStore.Images.Media.WIDTH else MediaStore.Images.Media.HEIGHT
|
||||||
|
|
||||||
|
private fun getHeightColumn(orientation: Int) =
|
||||||
|
if (orientation == 0 || orientation == 180) MediaStore.Images.Media.HEIGHT else MediaStore.Images.Media.WIDTH
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Media(
|
||||||
|
val id: Long,
|
||||||
|
val uri: Uri,
|
||||||
|
val mimeType: String,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val size: Long,
|
||||||
|
val dateModifiedEpochMillis: Long,
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
package app.dapk.st.messenger.gallery
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import app.dapk.st.core.*
|
||||||
|
import app.dapk.st.core.extensions.unsafeLazy
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
class ImageGalleryActivity : DapkActivity() {
|
||||||
|
|
||||||
|
private val module by unsafeLazy { module<ImageGalleryModule>() }
|
||||||
|
private val viewModel by viewModel {
|
||||||
|
val payload = intent.getParcelableExtra("key") as? ImageGalleryActivityPayload
|
||||||
|
module.imageGalleryViewModel(payload!!.roomName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val permissionState = mutableStateOf<Lce<PermissionResult>>(Lce.Loading())
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
permissionState.value = runCatching { ensurePermission(Manifest.permission.READ_EXTERNAL_STORAGE) }.fold(
|
||||||
|
onSuccess = { Lce.Content(it) },
|
||||||
|
onFailure = { Lce.Error(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
Surface {
|
||||||
|
PermissionGuard(permissionState) {
|
||||||
|
ImageGalleryScreen(viewModel, onTopLevelBack = { finish() }) { media ->
|
||||||
|
setResult(RESULT_OK, Intent().setData(media.uri))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Activity.PermissionGuard(state: State<Lce<PermissionResult>>, onGranted: @Composable () -> Unit) {
|
||||||
|
when (val content = state.value) {
|
||||||
|
is Lce.Content -> when (content.value) {
|
||||||
|
PermissionResult.Granted -> onGranted()
|
||||||
|
PermissionResult.Denied -> finish()
|
||||||
|
PermissionResult.ShowRational -> finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
is Lce.Error -> finish()
|
||||||
|
is Lce.Loading -> {
|
||||||
|
// loading should be quick, let's avoid displaying anything
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetImageFromGallery : ActivityResultContract<ImageGalleryActivityPayload, Uri?>() {
|
||||||
|
|
||||||
|
override fun createIntent(context: Context, input: ImageGalleryActivityPayload): Intent {
|
||||||
|
return Intent(context, ImageGalleryActivity::class.java)
|
||||||
|
.putExtra("key", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
|
||||||
|
return intent.takeIf { resultCode == Activity.RESULT_OK }?.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ImageGalleryActivityPayload(
|
||||||
|
val roomName: String,
|
||||||
|
) : Parcelable
|
|
@ -0,0 +1,18 @@
|
||||||
|
package app.dapk.st.messenger.gallery
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
|
import app.dapk.st.core.ProvidableModule
|
||||||
|
|
||||||
|
class ImageGalleryModule(
|
||||||
|
private val contentResolver: ContentResolver,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
) : ProvidableModule {
|
||||||
|
|
||||||
|
fun imageGalleryViewModel(roomName: String) = ImageGalleryViewModel(
|
||||||
|
FetchMediaFoldersUseCase(contentResolver, dispatchers),
|
||||||
|
FetchMediaUseCase(contentResolver, dispatchers),
|
||||||
|
roomName = roomName,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
package app.dapk.st.messenger.gallery
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.core.LifecycleEffect
|
||||||
|
import app.dapk.st.core.components.CenteredLoading
|
||||||
|
import app.dapk.st.design.components.GenericError
|
||||||
|
import app.dapk.st.design.components.Spider
|
||||||
|
import app.dapk.st.design.components.SpiderPage
|
||||||
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> Unit, onImageSelected: (Media) -> Unit) {
|
||||||
|
LifecycleEffect(onStart = {
|
||||||
|
viewModel.start()
|
||||||
|
})
|
||||||
|
|
||||||
|
val onNavigate: (SpiderPage<out ImageGalleryPage>?) -> Unit = {
|
||||||
|
when (it) {
|
||||||
|
null -> onTopLevelBack()
|
||||||
|
else -> viewModel.goTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) {
|
||||||
|
item(ImageGalleryPage.Routes.folders) {
|
||||||
|
ImageGalleryFolders(it) { folder ->
|
||||||
|
viewModel.selectFolder(folder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item(ImageGalleryPage.Routes.files) {
|
||||||
|
ImageGalleryMedia(it, onImageSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit) {
|
||||||
|
val screenWidth = LocalConfiguration.current.screenWidthDp
|
||||||
|
|
||||||
|
val gradient = Brush.verticalGradient(
|
||||||
|
colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.5f)),
|
||||||
|
)
|
||||||
|
|
||||||
|
when (val content = state.content) {
|
||||||
|
is Lce.Loading -> {
|
||||||
|
CenteredLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
is Lce.Content -> {
|
||||||
|
Column {
|
||||||
|
val columns = when {
|
||||||
|
screenWidth > 600 -> 4
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(columns),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
items(content.value, key = { it.bucketId }) {
|
||||||
|
Box(modifier = Modifier.fillMaxWidth().padding(2.dp).aspectRatio(1f)
|
||||||
|
.clickable { onClick(it) }) {
|
||||||
|
Image(
|
||||||
|
painter = rememberAsyncImagePainter(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(it.thumbnail.toString())
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
contentDescription = "123",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxWidth().fillMaxHeight(0.6f).background(gradient).align(Alignment.BottomStart))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart).padding(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(it.title, fontSize = 13.sp, color = Color.White)
|
||||||
|
Text(it.itemCount.toString(), fontSize = 11.sp, color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Lce.Error -> GenericError { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> Unit) {
|
||||||
|
val screenWidth = LocalConfiguration.current.screenWidthDp
|
||||||
|
|
||||||
|
Column {
|
||||||
|
val columns = when {
|
||||||
|
screenWidth > 600 -> 4
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val content = state.content) {
|
||||||
|
is Lce.Loading -> {
|
||||||
|
CenteredLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
is Lce.Content -> {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(columns),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
val modifier = Modifier.fillMaxWidth().padding(2.dp).aspectRatio(1f)
|
||||||
|
items(content.value, key = { it.id }) {
|
||||||
|
Box(modifier = modifier.clickable { onFileSelected(it) }) {
|
||||||
|
Image(
|
||||||
|
painter = rememberAsyncImagePainter(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(it.uri.toString())
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
),
|
||||||
|
contentDescription = "123",
|
||||||
|
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is Lce.Error -> GenericError { }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
package app.dapk.st.messenger.gallery
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import app.dapk.st.core.Lce
|
||||||
|
import app.dapk.st.design.components.Route
|
||||||
|
import app.dapk.st.design.components.SpiderPage
|
||||||
|
import app.dapk.st.viewmodel.DapkViewModel
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ImageGalleryViewModel(
|
||||||
|
private val foldersUseCase: FetchMediaFoldersUseCase,
|
||||||
|
private val fetchMediaUseCase: FetchMediaUseCase,
|
||||||
|
roomName: String,
|
||||||
|
) : DapkViewModel<ImageGalleryState, ImageGalleryEvent>(
|
||||||
|
initialState = ImageGalleryState(
|
||||||
|
page = SpiderPage(
|
||||||
|
route = ImageGalleryPage.Routes.folders,
|
||||||
|
label = "Send to $roomName",
|
||||||
|
parent = null,
|
||||||
|
state = ImageGalleryPage.Folders(Lce.Loading())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var currentPageJob: Job? = null
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
currentPageJob?.cancel()
|
||||||
|
currentPageJob = viewModelScope.launch {
|
||||||
|
val folders = foldersUseCase.fetchFolders()
|
||||||
|
updatePageState<ImageGalleryPage.Folders> { copy(content = Lce.Content(folders)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun goTo(page: SpiderPage<out ImageGalleryPage>) {
|
||||||
|
currentPageJob?.cancel()
|
||||||
|
updateState { copy(page = page) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectFolder(folder: Folder) {
|
||||||
|
currentPageJob?.cancel()
|
||||||
|
|
||||||
|
updateState {
|
||||||
|
copy(
|
||||||
|
page = SpiderPage(
|
||||||
|
route = ImageGalleryPage.Routes.files,
|
||||||
|
label = page.label,
|
||||||
|
parent = ImageGalleryPage.Routes.folders,
|
||||||
|
state = ImageGalleryPage.Files(Lce.Loading())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPageJob = viewModelScope.launch {
|
||||||
|
val media = fetchMediaUseCase.getMediaInBucket(folder.bucketId)
|
||||||
|
updatePageState<ImageGalleryPage.Files> {
|
||||||
|
copy(content = Lce.Content(media))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private inline fun <reified S : ImageGalleryPage> updatePageState(crossinline block: S.() -> S) {
|
||||||
|
val page = state.page
|
||||||
|
val currentState = page.state
|
||||||
|
require(currentState is S)
|
||||||
|
updateState { copy(page = (page as SpiderPage<S>).copy(state = block(page.state))) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ImageGalleryState(
|
||||||
|
val page: SpiderPage<out ImageGalleryPage>,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
sealed interface ImageGalleryPage {
|
||||||
|
data class Folders(val content: Lce<List<Folder>>) : ImageGalleryPage
|
||||||
|
data class Files(val content: Lce<List<Media>>) : ImageGalleryPage
|
||||||
|
|
||||||
|
object Routes {
|
||||||
|
val folders = Route<Folders>("Folders")
|
||||||
|
val files = Route<Files>("Files")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface ImageGalleryEvent
|
|
@ -0,0 +1,6 @@
|
||||||
|
package app.dapk.st.messenger.gallery
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
|
|
||||||
|
fun isNotPending() = if (Build.VERSION.SDK_INT <= 28) MediaStore.Images.Media.DATA + " NOT NULL" else MediaStore.MediaColumns.IS_PENDING + " != 1"
|
|
@ -81,7 +81,7 @@ data class MessageAttachment(val uri: AndroidUri, val type: MimeType) : Parcelab
|
||||||
private companion object : Parceler<MessageAttachment> {
|
private companion object : Parceler<MessageAttachment> {
|
||||||
override fun create(parcel: Parcel): MessageAttachment {
|
override fun create(parcel: Parcel): MessageAttachment {
|
||||||
val uri = AndroidUri(parcel.readString()!!)
|
val uri = AndroidUri(parcel.readString()!!)
|
||||||
val type = when(parcel.readString()!!) {
|
val type = when (parcel.readString()!!) {
|
||||||
"mimetype-image" -> MimeType.Image
|
"mimetype-image" -> MimeType.Image
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,11 @@ data class ServiceDependencies(
|
||||||
|
|
||||||
interface MatrixServiceInstaller {
|
interface MatrixServiceInstaller {
|
||||||
fun serializers(builder: SerializersModuleBuilder.() -> Unit)
|
fun serializers(builder: SerializersModuleBuilder.() -> Unit)
|
||||||
fun install(factory: MatrixService.Factory)
|
fun <T : MatrixService> install(factory: MatrixService.Factory): InstallExtender<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstallExtender<T : MatrixService> {
|
||||||
|
fun proxy(proxy: (T) -> T)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MatrixServiceProvider {
|
interface MatrixServiceProvider {
|
||||||
|
|
|
@ -11,15 +11,22 @@ internal class ServiceInstaller {
|
||||||
private val services = mutableMapOf<Any, MatrixService>()
|
private val services = mutableMapOf<Any, MatrixService>()
|
||||||
private val serviceInstaller = object : MatrixServiceInstaller {
|
private val serviceInstaller = object : MatrixServiceInstaller {
|
||||||
|
|
||||||
val serviceCollector = mutableListOf<MatrixService.Factory>()
|
val serviceCollector = mutableListOf<Pair<MatrixService.Factory, (MatrixService) -> MatrixService>>()
|
||||||
val serializers = mutableListOf<SerializersModuleBuilder.() -> Unit>()
|
val serializers = mutableListOf<SerializersModuleBuilder.() -> Unit>()
|
||||||
|
|
||||||
override fun serializers(builder: SerializersModuleBuilder.() -> Unit) {
|
override fun serializers(builder: SerializersModuleBuilder.() -> Unit) {
|
||||||
serializers.add(builder)
|
serializers.add(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun install(factory: MatrixService.Factory) {
|
override fun <T : MatrixService> install(factory: MatrixService.Factory): InstallExtender<T> {
|
||||||
serviceCollector.add(factory)
|
val mutableProxy = MutableProxy<T>()
|
||||||
|
return object : InstallExtender<T> {
|
||||||
|
override fun proxy(proxy: (T) -> T) {
|
||||||
|
mutableProxy.value = proxy
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
serviceCollector.add(factory to mutableProxy)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,9 +46,9 @@ internal class ServiceInstaller {
|
||||||
val serviceProvider = object : MatrixServiceProvider {
|
val serviceProvider = object : MatrixServiceProvider {
|
||||||
override fun <T : MatrixService> getService(key: ServiceKey) = this@ServiceInstaller.getService<T>(key)
|
override fun <T : MatrixService> getService(key: ServiceKey) = this@ServiceInstaller.getService<T>(key)
|
||||||
}
|
}
|
||||||
serviceInstaller.serviceCollector.forEach {
|
serviceInstaller.serviceCollector.forEach { (factory, extender) ->
|
||||||
val (key, service) = it.create(ServiceDependencies(httpClient, json, serviceProvider, logger))
|
val (key, service) = factory.create(ServiceDependencies(httpClient, json, serviceProvider, logger))
|
||||||
services[key] = service
|
services[key] = extender(service)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,4 +64,13 @@ internal class ServiceInstaller {
|
||||||
?: throw IllegalArgumentException("No service available to handle ${task.type}")
|
?: throw IllegalArgumentException("No service available to handle ${task.type}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MutableProxy<T : MatrixService> : (MatrixService) -> MatrixService {
|
||||||
|
|
||||||
|
var value: (T) -> T = { it }
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun invoke(service: MatrixService) = value(service as T)
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package app.dapk.st.matrix.auth
|
package app.dapk.st.matrix.auth
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.InstallExtender
|
||||||
import app.dapk.st.matrix.MatrixClient
|
import app.dapk.st.matrix.MatrixClient
|
||||||
import app.dapk.st.matrix.MatrixService
|
import app.dapk.st.matrix.MatrixService
|
||||||
import app.dapk.st.matrix.MatrixServiceInstaller
|
import app.dapk.st.matrix.MatrixServiceInstaller
|
||||||
|
@ -25,8 +26,8 @@ interface AuthService : MatrixService {
|
||||||
|
|
||||||
fun MatrixServiceInstaller.installAuthService(
|
fun MatrixServiceInstaller.installAuthService(
|
||||||
credentialsStore: CredentialsStore,
|
credentialsStore: CredentialsStore,
|
||||||
) {
|
): InstallExtender<AuthService> {
|
||||||
this.install { (httpClient, json) ->
|
return this.install { (httpClient, json) ->
|
||||||
SERVICE_KEY to DefaultAuthService(httpClient, credentialsStore, json)
|
SERVICE_KEY to DefaultAuthService(httpClient, credentialsStore, json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,7 @@ package app.dapk.st.matrix.crypto
|
||||||
|
|
||||||
import app.dapk.st.core.Base64
|
import app.dapk.st.core.Base64
|
||||||
import app.dapk.st.core.CoroutineDispatchers
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
import app.dapk.st.matrix.MatrixService
|
import app.dapk.st.matrix.*
|
||||||
import app.dapk.st.matrix.MatrixServiceInstaller
|
|
||||||
import app.dapk.st.matrix.MatrixServiceProvider
|
|
||||||
import app.dapk.st.matrix.ServiceDepFactory
|
|
||||||
import app.dapk.st.matrix.common.*
|
import app.dapk.st.matrix.common.*
|
||||||
import app.dapk.st.matrix.crypto.internal.*
|
import app.dapk.st.matrix.crypto.internal.*
|
||||||
import app.dapk.st.matrix.device.deviceService
|
import app.dapk.st.matrix.device.deviceService
|
||||||
|
@ -136,8 +133,8 @@ fun MatrixServiceInstaller.installCryptoService(
|
||||||
roomMembersProvider: ServiceDepFactory<RoomMembersProvider>,
|
roomMembersProvider: ServiceDepFactory<RoomMembersProvider>,
|
||||||
base64: Base64,
|
base64: Base64,
|
||||||
coroutineDispatchers: CoroutineDispatchers,
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
) {
|
): InstallExtender<CryptoService> {
|
||||||
this.install { (_, _, services, logger) ->
|
return this.install { (_, _, services, logger) ->
|
||||||
val deviceService = services.deviceService()
|
val deviceService = services.deviceService()
|
||||||
val accountCryptoUseCase = FetchAccountCryptoUseCaseImpl(credentialsStore, olm, deviceService)
|
val accountCryptoUseCase = FetchAccountCryptoUseCaseImpl(credentialsStore, olm, deviceService)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package app.dapk.st.matrix.device
|
package app.dapk.st.matrix.device
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.InstallExtender
|
||||||
import app.dapk.st.matrix.MatrixService
|
import app.dapk.st.matrix.MatrixService
|
||||||
import app.dapk.st.matrix.MatrixServiceInstaller
|
import app.dapk.st.matrix.MatrixServiceInstaller
|
||||||
import app.dapk.st.matrix.MatrixServiceProvider
|
import app.dapk.st.matrix.MatrixServiceProvider
|
||||||
|
@ -122,8 +123,8 @@ sealed class ToDevicePayload {
|
||||||
sealed interface VerificationPayload
|
sealed interface VerificationPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MatrixServiceInstaller.installEncryptionService(knownDeviceStore: KnownDeviceStore) {
|
fun MatrixServiceInstaller.installEncryptionService(knownDeviceStore: KnownDeviceStore): InstallExtender<DeviceService> {
|
||||||
this.install { (httpClient, _, _, logger) ->
|
return this.install { (httpClient, _, _, logger) ->
|
||||||
SERVICE_KEY to DefaultDeviceService(httpClient, logger, knownDeviceStore)
|
SERVICE_KEY to DefaultDeviceService(httpClient, logger, knownDeviceStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
package app.dapk.st.matrix.message
|
package app.dapk.st.matrix.message
|
||||||
|
|
||||||
import app.dapk.st.core.Base64
|
import app.dapk.st.core.Base64
|
||||||
import app.dapk.st.matrix.MatrixService
|
import app.dapk.st.matrix.*
|
||||||
import app.dapk.st.matrix.MatrixServiceInstaller
|
|
||||||
import app.dapk.st.matrix.MatrixServiceProvider
|
|
||||||
import app.dapk.st.matrix.ServiceDepFactory
|
|
||||||
import app.dapk.st.matrix.common.AlgorithmName
|
import app.dapk.st.matrix.common.AlgorithmName
|
||||||
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.MessageType
|
||||||
|
@ -132,8 +129,8 @@ fun MatrixServiceInstaller.installMessageService(
|
||||||
imageContentReader: ImageContentReader,
|
imageContentReader: ImageContentReader,
|
||||||
messageEncrypter: ServiceDepFactory<MessageEncrypter> = ServiceDepFactory { MissingMessageEncrypter },
|
messageEncrypter: ServiceDepFactory<MessageEncrypter> = ServiceDepFactory { MissingMessageEncrypter },
|
||||||
mediaEncrypter: ServiceDepFactory<MediaEncrypter> = ServiceDepFactory { MissingMediaEncrypter },
|
mediaEncrypter: ServiceDepFactory<MediaEncrypter> = ServiceDepFactory { MissingMediaEncrypter },
|
||||||
) {
|
): InstallExtender<MessageService> {
|
||||||
this.install { (httpClient, _, installedServices) ->
|
return this.install { (httpClient, _, installedServices) ->
|
||||||
SERVICE_KEY to DefaultMessageService(
|
SERVICE_KEY to DefaultMessageService(
|
||||||
httpClient,
|
httpClient,
|
||||||
localEchoStore,
|
localEchoStore,
|
||||||
|
|
|
@ -45,6 +45,7 @@ sealed class ApiMessage {
|
||||||
data class Info(
|
data class Info(
|
||||||
@SerialName("h") val height: Int,
|
@SerialName("h") val height: Int,
|
||||||
@SerialName("w") val width: Int,
|
@SerialName("w") val width: Int,
|
||||||
|
@SerialName("mimetype") val mimeType: String,
|
||||||
@SerialName("size") val size: Long,
|
@SerialName("size") val size: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package app.dapk.st.matrix.message.internal
|
package app.dapk.st.matrix.message.internal
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.*
|
import app.dapk.st.matrix.common.EventId
|
||||||
|
import app.dapk.st.matrix.common.EventType
|
||||||
|
import app.dapk.st.matrix.common.JsonString
|
||||||
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.matrix.http.MatrixHttpClient
|
import app.dapk.st.matrix.http.MatrixHttpClient
|
||||||
import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest
|
import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest
|
||||||
import app.dapk.st.matrix.message.ApiSendResponse
|
import app.dapk.st.matrix.message.ApiSendResponse
|
||||||
|
@ -57,7 +60,7 @@ internal class SendMessageUseCase(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ApiMessageMapper.imageMessageRequest(message: Message.ImageMessage): HttpRequest<ApiSendResponse> {
|
private suspend fun imageMessageRequest(message: Message.ImageMessage): HttpRequest<ApiSendResponse> {
|
||||||
val imageMeta = imageContentReader.meta(message.content.uri)
|
val imageMeta = imageContentReader.meta(message.content.uri)
|
||||||
|
|
||||||
return when (message.sendEncrypted) {
|
return when (message.sendEncrypted) {
|
||||||
|
@ -91,11 +94,11 @@ internal class SendMessageUseCase(
|
||||||
info = ApiMessage.ImageMessage.ImageContent.Info(
|
info = ApiMessage.ImageMessage.ImageContent.Info(
|
||||||
height = imageMeta.height,
|
height = imageMeta.height,
|
||||||
width = imageMeta.width,
|
width = imageMeta.width,
|
||||||
size = imageMeta.size
|
size = imageMeta.size,
|
||||||
|
mimeType = imageMeta.mimeType,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
val json = JsonString(
|
val json = JsonString(
|
||||||
MatrixHttpClient.jsonWithDefaults.encodeToString(
|
MatrixHttpClient.jsonWithDefaults.encodeToString(
|
||||||
ApiMessage.ImageMessage.serializer(),
|
ApiMessage.ImageMessage.serializer(),
|
||||||
|
@ -134,7 +137,8 @@ internal class SendMessageUseCase(
|
||||||
ApiMessage.ImageMessage.ImageContent.Info(
|
ApiMessage.ImageMessage.ImageContent.Info(
|
||||||
height = imageMeta.height,
|
height = imageMeta.height,
|
||||||
width = imageMeta.width,
|
width = imageMeta.width,
|
||||||
size = imageMeta.size
|
size = imageMeta.size,
|
||||||
|
mimeType = imageMeta.mimeType,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -163,14 +167,4 @@ class ApiMessageMapper {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Message.ImageMessage.toContents(uri: MxUrl, image: ImageContentReader.ImageContent) = ApiMessage.ImageMessage.ImageContent(
|
|
||||||
url = uri,
|
|
||||||
filename = image.fileName,
|
|
||||||
ApiMessage.ImageMessage.ImageContent.Info(
|
|
||||||
height = image.height,
|
|
||||||
width = image.width,
|
|
||||||
size = image.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package app.dapk.st.matrix.room
|
package app.dapk.st.matrix.room
|
||||||
|
|
||||||
import app.dapk.st.core.SingletonFlows
|
import app.dapk.st.core.SingletonFlows
|
||||||
|
import app.dapk.st.matrix.InstallExtender
|
||||||
import app.dapk.st.matrix.MatrixService
|
import app.dapk.st.matrix.MatrixService
|
||||||
import app.dapk.st.matrix.MatrixServiceInstaller
|
import app.dapk.st.matrix.MatrixServiceInstaller
|
||||||
import app.dapk.st.matrix.MatrixServiceProvider
|
import app.dapk.st.matrix.MatrixServiceProvider
|
||||||
|
@ -29,8 +30,8 @@ fun MatrixServiceInstaller.installProfileService(
|
||||||
profileStore: ProfileStore,
|
profileStore: ProfileStore,
|
||||||
singletonFlows: SingletonFlows,
|
singletonFlows: SingletonFlows,
|
||||||
credentialsStore: CredentialsStore,
|
credentialsStore: CredentialsStore,
|
||||||
) {
|
): InstallExtender<ProfileService> {
|
||||||
this.install { (httpClient, _, _, logger) ->
|
return this.install { (httpClient, _, _, logger) ->
|
||||||
SERVICE_KEY to DefaultProfileService(httpClient, logger, profileStore, singletonFlows, credentialsStore)
|
SERVICE_KEY to DefaultProfileService(httpClient, logger, profileStore, singletonFlows, credentialsStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package app.dapk.st.matrix.push
|
package app.dapk.st.matrix.push
|
||||||
|
|
||||||
|
import app.dapk.st.matrix.InstallExtender
|
||||||
import app.dapk.st.matrix.MatrixClient
|
import app.dapk.st.matrix.MatrixClient
|
||||||
import app.dapk.st.matrix.MatrixService
|
import app.dapk.st.matrix.MatrixService
|
||||||
import app.dapk.st.matrix.MatrixServiceInstaller
|
import app.dapk.st.matrix.MatrixServiceInstaller
|
||||||
|
@ -38,8 +39,8 @@ interface PushService : MatrixService {
|
||||||
|
|
||||||
fun MatrixServiceInstaller.installPushService(
|
fun MatrixServiceInstaller.installPushService(
|
||||||
credentialsStore: CredentialsStore,
|
credentialsStore: CredentialsStore,
|
||||||
) {
|
): InstallExtender<PushService> {
|
||||||
this.install { (httpClient, _, _, logger) ->
|
return this.install { (httpClient, _, _, logger) ->
|
||||||
SERVICE_KEY to DefaultPushService(httpClient, credentialsStore, logger)
|
SERVICE_KEY to DefaultPushService(httpClient, credentialsStore, logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package app.dapk.st.matrix.room
|
package app.dapk.st.matrix.room
|
||||||
|
|
||||||
import app.dapk.st.matrix.MatrixService
|
import app.dapk.st.matrix.*
|
||||||
import app.dapk.st.matrix.MatrixServiceInstaller
|
|
||||||
import app.dapk.st.matrix.MatrixServiceProvider
|
|
||||||
import app.dapk.st.matrix.ServiceDepFactory
|
|
||||||
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.RoomMember
|
import app.dapk.st.matrix.common.RoomMember
|
||||||
|
@ -42,8 +39,8 @@ fun MatrixServiceInstaller.installRoomService(
|
||||||
memberStore: MemberStore,
|
memberStore: MemberStore,
|
||||||
roomMessenger: ServiceDepFactory<RoomMessenger>,
|
roomMessenger: ServiceDepFactory<RoomMessenger>,
|
||||||
roomInviteRemover: RoomInviteRemover,
|
roomInviteRemover: RoomInviteRemover,
|
||||||
) {
|
): InstallExtender<RoomService> {
|
||||||
this.install { (httpClient, _, services, logger) ->
|
return this.install { (httpClient, _, services, logger) ->
|
||||||
SERVICE_KEY to DefaultRoomService(
|
SERVICE_KEY to DefaultRoomService(
|
||||||
httpClient,
|
httpClient,
|
||||||
logger,
|
logger,
|
||||||
|
|
|
@ -35,6 +35,7 @@ sealed class RoomEvent {
|
||||||
@SerialName("meta") override val meta: MessageMeta,
|
@SerialName("meta") override val meta: MessageMeta,
|
||||||
@SerialName("encrypted_content") val encryptedContent: MegOlmV1? = null,
|
@SerialName("encrypted_content") val encryptedContent: MegOlmV1? = null,
|
||||||
@SerialName("edited") val edited: Boolean = false,
|
@SerialName("edited") val edited: Boolean = false,
|
||||||
|
@SerialName("redacted") val redacted: Boolean = false,
|
||||||
) : RoomEvent() {
|
) : RoomEvent() {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -7,8 +7,9 @@ import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface RoomStore {
|
interface RoomStore {
|
||||||
|
|
||||||
suspend fun persist(roomId: RoomId, state: RoomState)
|
suspend fun persist(roomId: RoomId, events: List<RoomEvent>)
|
||||||
suspend fun remove(rooms: List<RoomId>)
|
suspend fun remove(rooms: List<RoomId>)
|
||||||
|
suspend fun remove(eventId: EventId)
|
||||||
suspend fun retrieve(roomId: RoomId): RoomState?
|
suspend fun retrieve(roomId: RoomId): RoomState?
|
||||||
fun latest(roomId: RoomId): Flow<RoomState>
|
fun latest(roomId: RoomId): Flow<RoomState>
|
||||||
suspend fun insertUnread(roomId: RoomId, eventIds: List<EventId>)
|
suspend fun insertUnread(roomId: RoomId, eventIds: List<EventId>)
|
||||||
|
|
|
@ -2,10 +2,7 @@ package app.dapk.st.matrix.sync
|
||||||
|
|
||||||
import app.dapk.st.core.CoroutineDispatchers
|
import app.dapk.st.core.CoroutineDispatchers
|
||||||
import app.dapk.st.core.extensions.ErrorTracker
|
import app.dapk.st.core.extensions.ErrorTracker
|
||||||
import app.dapk.st.matrix.MatrixClient
|
import app.dapk.st.matrix.*
|
||||||
import app.dapk.st.matrix.MatrixService
|
|
||||||
import app.dapk.st.matrix.MatrixServiceInstaller
|
|
||||||
import app.dapk.st.matrix.ServiceDepFactory
|
|
||||||
import app.dapk.st.matrix.common.*
|
import app.dapk.st.matrix.common.*
|
||||||
import app.dapk.st.matrix.sync.internal.DefaultSyncService
|
import app.dapk.st.matrix.sync.internal.DefaultSyncService
|
||||||
import app.dapk.st.matrix.sync.internal.request.*
|
import app.dapk.st.matrix.sync.internal.request.*
|
||||||
|
@ -49,7 +46,7 @@ fun MatrixServiceInstaller.installSyncService(
|
||||||
errorTracker: ErrorTracker,
|
errorTracker: ErrorTracker,
|
||||||
coroutineDispatchers: CoroutineDispatchers,
|
coroutineDispatchers: CoroutineDispatchers,
|
||||||
syncConfig: SyncConfig = SyncConfig(),
|
syncConfig: SyncConfig = SyncConfig(),
|
||||||
) {
|
): InstallExtender<SyncService> {
|
||||||
this.serializers {
|
this.serializers {
|
||||||
polymorphicDefault(ApiTimelineEvent::class) {
|
polymorphicDefault(ApiTimelineEvent::class) {
|
||||||
ApiTimelineEvent.Ignored.serializer()
|
ApiTimelineEvent.Ignored.serializer()
|
||||||
|
@ -71,7 +68,7 @@ fun MatrixServiceInstaller.installSyncService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.install { (httpClient, json, services, logger) ->
|
return this.install { (httpClient, json, services, logger) ->
|
||||||
SERVICE_KEY to DefaultSyncService(
|
SERVICE_KEY to DefaultSyncService(
|
||||||
httpClient = httpClient,
|
httpClient = httpClient,
|
||||||
syncStore = syncStore,
|
syncStore = syncStore,
|
||||||
|
|
|
@ -82,7 +82,6 @@ internal sealed class ApiTimelineEvent {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@SerialName("m.room.member")
|
@SerialName("m.room.member")
|
||||||
internal data class RoomMember(
|
internal data class RoomMember(
|
||||||
|
@ -109,6 +108,15 @@ internal sealed class ApiTimelineEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("m.room.redaction")
|
||||||
|
internal data class RoomRedcation(
|
||||||
|
@SerialName("event_id") val id: EventId,
|
||||||
|
@SerialName("redacts") val redactedId: EventId,
|
||||||
|
@SerialName("origin_server_ts") val utcTimestamp: Long,
|
||||||
|
@SerialName("sender") val senderId: UserId,
|
||||||
|
) : ApiTimelineEvent()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
internal data class DecryptionStatus(
|
internal data class DecryptionStatus(
|
||||||
@SerialName("is_verified") val isVerified: Boolean
|
@SerialName("is_verified") val isVerified: Boolean
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package app.dapk.st.matrix.sync.internal.sync
|
package app.dapk.st.matrix.sync.internal.sync
|
||||||
|
|
||||||
import app.dapk.st.matrix.common.MatrixLogTag
|
import app.dapk.st.matrix.common.*
|
||||||
import app.dapk.st.matrix.common.MatrixLogger
|
import app.dapk.st.matrix.sync.RoomEvent
|
||||||
import app.dapk.st.matrix.common.RoomId
|
|
||||||
import app.dapk.st.matrix.common.matrixLog
|
|
||||||
import app.dapk.st.matrix.sync.RoomState
|
import app.dapk.st.matrix.sync.RoomState
|
||||||
import app.dapk.st.matrix.sync.RoomStore
|
import app.dapk.st.matrix.sync.RoomStore
|
||||||
|
|
||||||
|
@ -26,7 +24,7 @@ class RoomDataSource(
|
||||||
logger.matrixLog(MatrixLogTag.SYNC, "no changes, not persisting")
|
logger.matrixLog(MatrixLogTag.SYNC, "no changes, not persisting")
|
||||||
} else {
|
} else {
|
||||||
roomCache[roomId] = newState
|
roomCache[roomId] = newState
|
||||||
roomStore.persist(roomId, newState)
|
roomStore.persist(roomId, newState.events)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,4 +32,35 @@ class RoomDataSource(
|
||||||
roomsLeft.forEach { roomCache.remove(it) }
|
roomsLeft.forEach { roomCache.remove(it) }
|
||||||
roomStore.remove(roomsLeft)
|
roomStore.remove(roomsLeft)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun redact(roomId: RoomId, event: EventId) {
|
||||||
|
val eventToRedactFromCache = roomCache[roomId]?.events?.find { it.eventId == event }
|
||||||
|
val redactedEvent = when {
|
||||||
|
eventToRedactFromCache != null -> {
|
||||||
|
eventToRedactFromCache.redact().also { redacted ->
|
||||||
|
val cachedRoomState = roomCache[roomId]
|
||||||
|
requireNotNull(cachedRoomState)
|
||||||
|
roomCache[roomId] = cachedRoomState.replaceEvent(eventToRedactFromCache, redacted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> roomStore.findEvent(event)?.redact()
|
||||||
|
}
|
||||||
|
|
||||||
|
redactedEvent?.let { roomStore.persist(roomId, listOf(it)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomEvent.redact() = when (this) {
|
||||||
|
is RoomEvent.Image -> RoomEvent.Message(this.eventId, this.utcTimestamp, "Redacted", this.author, this.meta, redacted = true)
|
||||||
|
is RoomEvent.Message -> RoomEvent.Message(this.eventId, this.utcTimestamp, "Redacted", this.author, this.meta, redacted = true)
|
||||||
|
is RoomEvent.Reply -> RoomEvent.Message(this.eventId, this.utcTimestamp, "Redacted", this.author, this.meta, redacted = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomState.replaceEvent(old: RoomEvent, new: RoomEvent): RoomState {
|
||||||
|
val updatedEvents = this.events.toMutableList().apply {
|
||||||
|
remove(old)
|
||||||
|
add(new)
|
||||||
|
}
|
||||||
|
return this.copy(events = updatedEvents)
|
||||||
}
|
}
|
|
@ -21,6 +21,10 @@ internal class RoomProcessor(
|
||||||
val members = roomToProcess.apiSyncRoom.collectMembers(roomToProcess.userCredentials)
|
val members = roomToProcess.apiSyncRoom.collectMembers(roomToProcess.userCredentials)
|
||||||
roomMembersService.insert(roomToProcess.roomId, members)
|
roomMembersService.insert(roomToProcess.roomId, members)
|
||||||
|
|
||||||
|
roomToProcess.apiSyncRoom.timeline.apiTimelineEvents.filterIsInstance<ApiTimelineEvent.RoomRedcation>().forEach {
|
||||||
|
roomDataSource.redact(roomToProcess.roomId, it.redactedId)
|
||||||
|
}
|
||||||
|
|
||||||
val previousState = roomDataSource.read(roomToProcess.roomId)
|
val previousState = roomDataSource.read(roomToProcess.roomId)
|
||||||
|
|
||||||
val (newEvents, distinctEvents) = timelineEventsProcessor.process(
|
val (newEvents, distinctEvents) = timelineEventsProcessor.process(
|
||||||
|
|
|
@ -61,8 +61,6 @@ internal class SyncReducer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
roomDataSource.remove(roomsLeft)
|
|
||||||
|
|
||||||
return ReducerResult(
|
return ReducerResult(
|
||||||
newRooms,
|
newRooms,
|
||||||
(apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(),
|
(apiRoomsToProcess + roomsWithSideEffects).awaitAll().filterNotNull(),
|
||||||
|
|
|
@ -8,7 +8,6 @@ import app.dapk.st.matrix.sync.internal.SideEffectFlowIterator
|
||||||
import app.dapk.st.matrix.sync.internal.overview.ReducedSyncFilterUseCase
|
import app.dapk.st.matrix.sync.internal.overview.ReducedSyncFilterUseCase
|
||||||
import app.dapk.st.matrix.sync.internal.request.syncRequest
|
import app.dapk.st.matrix.sync.internal.request.syncRequest
|
||||||
import app.dapk.st.matrix.sync.internal.room.SyncSideEffects
|
import app.dapk.st.matrix.sync.internal.room.SyncSideEffects
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.cancellable
|
import kotlinx.coroutines.flow.cancellable
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
|
@ -25,19 +24,17 @@ internal class SyncUseCase(
|
||||||
private val syncConfig: SyncConfig,
|
private val syncConfig: SyncConfig,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun sync(): Flow<Unit> {
|
private val _flow = flow {
|
||||||
return flow {
|
val credentials = credentialsStore.credentials()!!
|
||||||
val credentials = credentialsStore.credentials()!!
|
val filterId = filterUseCase.reducedFilter(credentials.userId)
|
||||||
val filterId = filterUseCase.reducedFilter(credentials.userId)
|
with(flowIterator) {
|
||||||
with(flowIterator) {
|
loop<OverviewState>(
|
||||||
loop<OverviewState>(
|
initial = null,
|
||||||
initial = null,
|
onPost = { emit(Unit) },
|
||||||
onPost = { emit(Unit) },
|
onIteration = { onEachSyncIteration(filterId, credentials, previousState = it) }
|
||||||
onIteration = { onEachSyncIteration(filterId, credentials, previousState = it) }
|
)
|
||||||
)
|
}
|
||||||
}
|
}.cancellable()
|
||||||
}.cancellable()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun onEachSyncIteration(filterId: SyncService.FilterId, credentials: UserCredentials, previousState: OverviewState?): OverviewState? {
|
private suspend fun onEachSyncIteration(filterId: SyncService.FilterId, credentials: UserCredentials, previousState: OverviewState?): OverviewState? {
|
||||||
val syncToken = syncStore.read(key = SyncStore.SyncKey.Overview)
|
val syncToken = syncStore.read(key = SyncStore.SyncKey.Overview)
|
||||||
|
@ -85,4 +82,6 @@ internal class SyncUseCase(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sync() = _flow
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ internal class TimelineEventsProcessor(
|
||||||
is ApiTimelineEvent.TimelineMessage -> event.toRoomEvent(roomToProcess.userCredentials, roomToProcess.roomId) { eventId ->
|
is ApiTimelineEvent.TimelineMessage -> event.toRoomEvent(roomToProcess.userCredentials, roomToProcess.roomId) { eventId ->
|
||||||
eventLookupUseCase.lookup(eventId, decryptedTimeline, decryptedPreviousEvents)
|
eventLookupUseCase.lookup(eventId, decryptedTimeline, decryptedPreviousEvents)
|
||||||
}
|
}
|
||||||
|
is ApiTimelineEvent.RoomRedcation -> null
|
||||||
is ApiTimelineEvent.Encryption -> null
|
is ApiTimelineEvent.Encryption -> null
|
||||||
is ApiTimelineEvent.RoomAvatar -> null
|
is ApiTimelineEvent.RoomAvatar -> null
|
||||||
is ApiTimelineEvent.RoomCreate -> null
|
is ApiTimelineEvent.RoomCreate -> null
|
||||||
|
|
|
@ -9,7 +9,7 @@ test {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
kotlinTest(it)
|
kotlinTest(it)
|
||||||
testImplementation 'app.cash.turbine:turbine:0.10.0'
|
testImplementation 'app.cash.turbine:turbine:0.11.0'
|
||||||
|
|
||||||
testImplementation Dependencies.mavenCentral.kotlinSerializationJson
|
testImplementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ dependencies {
|
||||||
testImplementation project(":matrix:services:crypto")
|
testImplementation project(":matrix:services:crypto")
|
||||||
|
|
||||||
testImplementation rootProject.files("external/jolm.jar")
|
testImplementation rootProject.files("external/jolm.jar")
|
||||||
testImplementation 'org.json:json:20220320'
|
testImplementation 'org.json:json:20220924'
|
||||||
|
|
||||||
testImplementation Dependencies.mavenCentral.ktorJava
|
testImplementation Dependencies.mavenCentral.ktorJava
|
||||||
testImplementation Dependencies.mavenCentral.sqldelightInMemory
|
testImplementation Dependencies.mavenCentral.sqldelightInMemory
|
||||||
|
|
|
@ -123,18 +123,20 @@ class SmokeTest {
|
||||||
alice.expectTextMessage(SharedState.sharedRoom, message2)
|
alice.expectTextMessage(SharedState.sharedRoom, message2)
|
||||||
|
|
||||||
// Needs investigation
|
// Needs investigation
|
||||||
// val aliceSecondDevice = testMatrix(SharedState.alice, isTemp = true, withLogging = true).also { it.newlogin() }
|
val aliceSecondDevice = testMatrix(SharedState.alice, isTemp = true, withLogging = true).also { it.newlogin() }
|
||||||
// aliceSecondDevice.client.syncService().startSyncing().collectAsync {
|
aliceSecondDevice.client.syncService().startSyncing().collectAsync {
|
||||||
// val message3 = "from alice to bob and alice's second device".from(SharedState.alice.roomMember)
|
aliceSecondDevice.client.proxyDeviceService().waitForOneTimeKeysToBeUploaded()
|
||||||
// alice.sendTextMessage(SharedState.sharedRoom, message3.content, isEncrypted)
|
|
||||||
// aliceSecondDevice.expectTextMessage(SharedState.sharedRoom, message3)
|
val message3 = "from alice to bob and alice's second device".from(SharedState.alice.roomMember)
|
||||||
// bob.expectTextMessage(SharedState.sharedRoom, message3)
|
alice.sendTextMessage(SharedState.sharedRoom, message3.content, isEncrypted)
|
||||||
//
|
aliceSecondDevice.expectTextMessage(SharedState.sharedRoom, message3)
|
||||||
// val message4 = "from alice's second device to bob and alice's first device".from(SharedState.alice.roomMember)
|
bob.expectTextMessage(SharedState.sharedRoom, message3)
|
||||||
// aliceSecondDevice.sendTextMessage(SharedState.sharedRoom, message4.content, isEncrypted)
|
|
||||||
// alice.expectTextMessage(SharedState.sharedRoom, message4)
|
val message4 = "from alice's second device to bob and alice's first device".from(SharedState.alice.roomMember)
|
||||||
// bob.expectTextMessage(SharedState.sharedRoom, message4)
|
aliceSecondDevice.sendTextMessage(SharedState.sharedRoom, message4.content, isEncrypted)
|
||||||
// }
|
alice.expectTextMessage(SharedState.sharedRoom, message4)
|
||||||
|
bob.expectTextMessage(SharedState.sharedRoom, message4)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import app.dapk.st.matrix.crypto.RoomMembersProvider
|
||||||
import app.dapk.st.matrix.crypto.Verification
|
import app.dapk.st.matrix.crypto.Verification
|
||||||
import app.dapk.st.matrix.crypto.cryptoService
|
import app.dapk.st.matrix.crypto.cryptoService
|
||||||
import app.dapk.st.matrix.crypto.installCryptoService
|
import app.dapk.st.matrix.crypto.installCryptoService
|
||||||
|
import app.dapk.st.matrix.device.DeviceService
|
||||||
import app.dapk.st.matrix.device.deviceService
|
import app.dapk.st.matrix.device.deviceService
|
||||||
import app.dapk.st.matrix.device.installEncryptionService
|
import app.dapk.st.matrix.device.installEncryptionService
|
||||||
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
|
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
|
||||||
|
@ -39,6 +40,7 @@ import test.impl.PrintingErrorTracking
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
object TestUsers {
|
object TestUsers {
|
||||||
|
|
||||||
|
@ -93,7 +95,9 @@ class TestMatrix(
|
||||||
).also {
|
).also {
|
||||||
it.install {
|
it.install {
|
||||||
installAuthService(storeModule.credentialsStore())
|
installAuthService(storeModule.credentialsStore())
|
||||||
installEncryptionService(storeModule.knownDevicesStore())
|
installEncryptionService(storeModule.knownDevicesStore()).proxy {
|
||||||
|
ProxyDeviceService(it)
|
||||||
|
}
|
||||||
|
|
||||||
val olmAccountStore = OlmPersistenceWrapper(storeModule.olmStore(), base64)
|
val olmAccountStore = OlmPersistenceWrapper(storeModule.olmStore(), base64)
|
||||||
val olm = OlmWrapper(
|
val olm = OlmWrapper(
|
||||||
|
@ -355,4 +359,23 @@ class JavaImageContentReader : ImageContentReader {
|
||||||
|
|
||||||
override fun inputStream(uri: String) = File(uri).inputStream()
|
override fun inputStream(uri: String) = File(uri).inputStream()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ProxyDeviceService(private val deviceService: DeviceService) : DeviceService by deviceService {
|
||||||
|
|
||||||
|
private var oneTimeKeysContinuation: (() -> Unit)? = null
|
||||||
|
|
||||||
|
override suspend fun uploadOneTimeKeys(oneTimeKeys: DeviceService.OneTimeKeys) {
|
||||||
|
deviceService.uploadOneTimeKeys(oneTimeKeys)
|
||||||
|
oneTimeKeysContinuation?.invoke()?.also { oneTimeKeysContinuation = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun waitForOneTimeKeysToBeUploaded() {
|
||||||
|
suspendCancellableCoroutine { continuation ->
|
||||||
|
oneTimeKeysContinuation = { continuation.resume(Unit) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MatrixClient.proxyDeviceService() = this.deviceService() as ProxyDeviceService
|
|
@ -10,7 +10,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/androidpublisher": "^3.0.0",
|
"@googleapis/androidpublisher": "^3.0.0",
|
||||||
"matrix-js-sdk": "^19.4.0",
|
"matrix-js-sdk": "^19.7.0",
|
||||||
"request": "^2.88.2"
|
"request": "^2.88.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -631,9 +631,9 @@
|
||||||
"integrity": "sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA=="
|
"integrity": "sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA=="
|
||||||
},
|
},
|
||||||
"node_modules/matrix-js-sdk": {
|
"node_modules/matrix-js-sdk": {
|
||||||
"version": "19.4.0",
|
"version": "19.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-19.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-19.7.0.tgz",
|
||||||
"integrity": "sha512-B8Mm4jCsCHaMaChcdM3VhZDVKrn0nMSDtYvHmS15Iu8Pe0G4qmIpk2AoADBAL9U9yN3pCqvs3TDXaQhM8UxRRA==",
|
"integrity": "sha512-mFN1LBmEpYHCH6II1F8o7y8zJr0kn1yX7ga7tRXHbLJAlBS4bAXRsEoAzdv6OrV8/dS325JlVUYQLHFHQWjYxg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
|
@ -1448,9 +1448,9 @@
|
||||||
"integrity": "sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA=="
|
"integrity": "sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA=="
|
||||||
},
|
},
|
||||||
"matrix-js-sdk": {
|
"matrix-js-sdk": {
|
||||||
"version": "19.4.0",
|
"version": "19.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-19.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-19.7.0.tgz",
|
||||||
"integrity": "sha512-B8Mm4jCsCHaMaChcdM3VhZDVKrn0nMSDtYvHmS15Iu8Pe0G4qmIpk2AoADBAL9U9yN3pCqvs3TDXaQhM8UxRRA==",
|
"integrity": "sha512-mFN1LBmEpYHCH6II1F8o7y8zJr0kn1yX7ga7tRXHbLJAlBS4bAXRsEoAzdv6OrV8/dS325JlVUYQLHFHQWjYxg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@googleapis/androidpublisher": "^3.0.0",
|
"@googleapis/androidpublisher": "^3.0.0",
|
||||||
"matrix-js-sdk": "^19.4.0",
|
"matrix-js-sdk": "^19.7.0",
|
||||||
"request": "^2.88.2"
|
"request": "^2.88.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ export const release = async (github, version, applicationId, artifacts, config)
|
||||||
|
|
||||||
console.log(releaseResult.data.id)
|
console.log(releaseResult.data.id)
|
||||||
|
|
||||||
|
console.log("Uploading universal apk...")
|
||||||
await github.rest.repos.uploadReleaseAsset({
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
owner: config.owner,
|
owner: config.owner,
|
||||||
repo: config.repo,
|
repo: config.repo,
|
||||||
|
@ -64,6 +65,15 @@ export const release = async (github, version, applicationId, artifacts, config)
|
||||||
data: fs.readFileSync(universalApkPath)
|
data: fs.readFileSync(universalApkPath)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log("Uploading foss apk...")
|
||||||
|
await github.rest.repos.uploadReleaseAsset({
|
||||||
|
owner: config.owner,
|
||||||
|
repo: config.repo,
|
||||||
|
release_id: releaseResult.data.id,
|
||||||
|
name: `foss-signed-${version.name}.apk`,
|
||||||
|
data: fs.readFileSync(artifacts.fossApkPath)
|
||||||
|
})
|
||||||
|
|
||||||
console.log("Promoting beta draft release to live...")
|
console.log("Promoting beta draft release to live...")
|
||||||
await promoteDraftToLive(applicationId)
|
await promoteDraftToLive(applicationId)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ SIGNED=$WORKING_DIR/app-foss-release-signed.apk
|
||||||
ZIPALIGN=$(find "$ANDROID_HOME" -iname zipalign -print -quit)
|
ZIPALIGN=$(find "$ANDROID_HOME" -iname zipalign -print -quit)
|
||||||
APKSIGNER=$(find "$ANDROID_HOME" -iname apksigner -print -quit)
|
APKSIGNER=$(find "$ANDROID_HOME" -iname apksigner -print -quit)
|
||||||
|
|
||||||
./gradlew clean assembleRelease -Pfoss -Punsigned --no-daemon --no-configuration-cache --no-build-cache
|
./gradlew assembleRelease -Pfoss -Punsigned --no-daemon --no-configuration-cache --no-build-cache
|
||||||
|
|
||||||
$ZIPALIGN -v -p 4 $UNSIGNED $ALIGNED_UNSIGNED
|
$ZIPALIGN -v -p 4 $UNSIGNED $ALIGNED_UNSIGNED
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
|
|
||||||
./gradlew clean bundleRelease -Punsigned --no-daemon --no-configuration-cache --no-build-cache
|
./gradlew bundleRelease -Punsigned --no-daemon --no-configuration-cache --no-build-cache
|
||||||
|
|
||||||
WORKING_DIR=app/build/outputs/bundle/release
|
WORKING_DIR=app/build/outputs/bundle/release
|
||||||
RELEASE_AAB=$WORKING_DIR/app-release.aab
|
RELEASE_AAB=$WORKING_DIR/app-release.aab
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"code": 18,
|
"code": 19,
|
||||||
"name": "22/09/2022-V1"
|
"name": "29/09/2022-V1"
|
||||||
}
|
}
|
Loading…
Reference in New Issue