Skip to content

Commit e65a9fb

Browse files
committed
Merge branch 'develop'
2 parents 720f66f + 8573ffe commit e65a9fb

655 files changed

Lines changed: 69116 additions & 22041 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.cargo/config.toml‎

Lines changed: 0 additions & 6 deletions
This file was deleted.

‎.github/workflows/deploy-pages.yml‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ on:
44
push:
55
branches:
66
- main
7-
- master
87
- develop
98
paths:
109
- "website/**"
@@ -25,13 +24,13 @@ jobs:
2524
runs-on: ubuntu-24.04
2625
steps:
2726
- name: Checkout
28-
uses: actions/checkout@v4
27+
uses: actions/checkout@v5
2928

3029
- name: Setup Pages
31-
uses: actions/configure-pages@v5
30+
uses: actions/configure-pages@v6
3231

3332
- name: Upload artifact
34-
uses: actions/upload-pages-artifact@v4
33+
uses: actions/upload-pages-artifact@v5
3534
with:
3635
path: website
3736

@@ -44,4 +43,4 @@ jobs:
4443
steps:
4544
- name: Deploy to GitHub Pages
4645
id: deployment
47-
uses: actions/deploy-pages@v4
46+
uses: actions/deploy-pages@v5

‎.gitignore‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ captures/
3232

3333
# Native / Rust
3434
native/*-rs/target/
35+
native/mtproto-transport/target/
3536
vendor/libvpx/
3637
native/vpx/build/
3738
native/vpx/tmp/
@@ -50,4 +51,5 @@ Thumbs.db
5051
google-services.json
5152
**/google-services.json
5253
*firebase-adminsdk*
53-
/native/mtproto/src/main/jniLibs
54+
/native/mtproto/src/main/jniLibs
55+
/native/target

‎AGENTS.md‎

Lines changed: 109 additions & 166 deletions
Large diffs are not rendered by default.

‎app/build.gradle.kts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ val selectedAbis = if (!targetAbiProp.isNullOrBlank()) {
3737
} else {
3838
listOf("armeabi-v7a", "arm64-v8a", "x86_64")
3939
}
40-
val appVersionCode = 17
41-
val appVersionName = "0.4.0"
40+
val appVersionCode = 19
41+
val appVersionName = "0.4.2"
4242

4343
/** Short git SHA of the checked-out commit, shown next to the build type in settings. */
4444
val gitCommit: String = providers.exec {
@@ -169,6 +169,7 @@ dependencies {
169169

170170
implementation(libs.decompose)
171171
implementation(libs.decompose.compose)
172+
implementation(libs.androidx.window)
172173
implementation(libs.essenty.lifecycle)
173174
implementation(libs.mvikotlin)
174175
implementation(libs.mvikotlin.main)

‎app/proguard-rules.pro‎

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,7 @@
1-
# R8 / release keep rules.
2-
3-
-keepattributes SourceFile,LineNumberTable,*Annotation*,InnerClasses,EnclosingMethod,Signature,Exceptions,RuntimeVisibleAnnotations,AnnotationDefault
1+
-keepattributes SourceFile,LineNumberTable,*Annotation*,InnerClasses,EnclosingMethod,Signature
42
-renamesourcefileattribute SourceFile
5-
-keep class kotlin.Metadata { *; }
6-
7-
-keep class org.monogram.BuildConfig { *; }
8-
-keepclassmembers class org.monogram.BuildConfig {
9-
public static <fields>;
10-
}
11-
12-
-keepclasseswithmembernames class * {
13-
native <methods>;
14-
}
15-
16-
# Kotlinx
17-
-keepclassmembers class kotlinx.coroutines.** {
18-
volatile <fields>;
19-
}
20-
-dontwarn kotlinx.coroutines.**
21-
-dontnote kotlinx.serialization.AnnotationsKt
22-
-keep,includedescriptorclasses class org.monogram.**$$serializer { *; }
23-
-keepclassmembers class org.monogram.** {
24-
*** Companion;
25-
}
26-
-keepclasseswithmembers class org.monogram.** {
27-
kotlinx.serialization.KSerializer serializer(...);
28-
}
29-
30-
# Navigation / stores
31-
-keep class com.arkivanov.decompose.** { *; }
32-
-keep class com.arkivanov.essenty.** { *; }
33-
-keep class com.arkivanov.mvikotlin.** { *; }
34-
-dontwarn com.arkivanov.**
35-
36-
# JNA + UniFFI + native facade
373
-keep class com.sun.jna.** { *; }
384
-keep class * extends com.sun.jna.** { *; }
395
-keep class * implements com.sun.jna.** { *; }
40-
-keep class uniffi.** { *; }
41-
-keepclassmembers class uniffi.** { *; }
42-
-keep class org.monogram.mtproto.** { *; }
43-
-keep class org.monogram.network.bridge.** { *; }
446
-dontwarn java.awt.**
45-
-dontwarn android.app.Fragment
46-
47-
# Room
48-
-keep class * extends androidx.room.RoomDatabase
49-
-keep @androidx.room.Entity class *
50-
-dontwarn androidx.room.paging.**
51-
52-
# Media3 extractors are looked up reflectively.
53-
-keep class androidx.media3.** { *; }
54-
-dontwarn androidx.media3.**
55-
56-
-dontwarn androidx.compose.**
7+
-dontwarn android.app.Fragment
3.14 KB
Loading
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package org.monogram.core.database
2+
3+
import androidx.room.Room
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import androidx.test.platform.app.InstrumentationRegistry
6+
import kotlinx.coroutines.runBlocking
7+
import org.junit.Assert.assertEquals
8+
import org.junit.Assert.assertFalse
9+
import org.junit.Assert.assertNotNull
10+
import org.junit.Assert.assertNull
11+
import org.junit.Assert.assertTrue
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
import org.monogram.core.database.entity.MessageHoleEntity
15+
import org.monogram.core.database.entity.MessageTombstoneEntity
16+
import org.monogram.core.database.entity.MetaEntity
17+
import org.monogram.core.database.entity.PeerEntity
18+
import org.monogram.core.database.entity.ProfileCommonEntity
19+
import org.monogram.core.database.entity.ProfileMediaEntity
20+
import org.monogram.core.database.entity.ProfileMemberEntity
21+
import org.monogram.core.database.entity.ProfileTabsEntity
22+
import org.monogram.core.database.entity.UpdateCursorEntity
23+
import org.monogram.core.models.Chat
24+
import org.monogram.core.models.Message
25+
import org.monogram.core.models.MessageId
26+
import org.monogram.core.models.PeerId
27+
28+
@RunWith(AndroidJUnit4::class)
29+
class CacheTtlRoomTest {
30+
@Test
31+
fun pruneKeepsTheRecentPageAndLeavesCursorsAlone() = runBlocking {
32+
val context = InstrumentationRegistry.getInstrumentation().targetContext
33+
val database = Room.inMemoryDatabaseBuilder(context, MonogramDatabase::class.java)
34+
.allowMainThreadQueries()
35+
.build()
36+
val now = 1_800_000_000_000L
37+
val old = now / 1000 - CACHE_MESSAGE_TTL_SECONDS - 60
38+
val recent = now / 1000 - 3_600
39+
val warmup = OfflineWarmup(database)
40+
try {
41+
val kept = (1..121).map { id ->
42+
message(7, id, if (id == 121) recent else old)
43+
}
44+
val pending = message(7, -1, old, pending = true)
45+
val failed = message(7, 200, old, failed = true)
46+
val openChat = (1..30).map { id -> message(9, id, old) }
47+
run {
48+
database.messageDao()
49+
.upsertAll((kept + pending + failed + openChat).map { it.toEntity() })
50+
database.messageHoleDao().insert(MessageHoleEntity(7L, 1, Int.MAX_VALUE))
51+
database.messageTombstoneDao().insert(
52+
listOf(MessageTombstoneEntity(7L, 3), MessageTombstoneEntity(7L, 50)),
53+
)
54+
database.peerDao().upsertAll(
55+
listOf(PeerEntity(7L, "user", "Kept"), PeerEntity(99L, "user", "Stray")),
56+
)
57+
repeat(503) { index ->
58+
database.metaDao().upsert(MetaEntity("discussion_read:7:${index + 1}", "1"))
59+
}
60+
database.updateCursorDao().upsert(UpdateCursorEntity("updates", 4, 5, 6, 7, 0))
61+
database.updateCursorDao().upsert(
62+
UpdateCursorEntity("$KEY_CHANNEL_CURSOR_PREFIX" + "1", 1, 0, 2, 0, 9),
63+
)
64+
database.profileTabsDao().upsert(ProfileTabsEntity(1L, "{}", 0L))
65+
database.profileTabsDao().upsert(ProfileTabsEntity(2L, "{}", now))
66+
database.profileMembersDao().upsertAll(
67+
listOf(
68+
ProfileMemberEntity(1L, 5L, 0, "Old", role = "member"),
69+
ProfileMemberEntity(2L, 6L, 0, "New", role = "member"),
70+
),
71+
)
72+
database.profileMediaDao().upsertAll(
73+
listOf(
74+
ProfileMediaEntity(1L, "photo", 1, 0, 7L),
75+
ProfileMediaEntity(2L, "photo", 1, 0, 7L),
76+
),
77+
)
78+
database.profileCommonDao().upsertAll(
79+
listOf(
80+
ProfileCommonEntity(1L, 3L, 0, "Old"),
81+
ProfileCommonEntity(2L, 4L, 0, "New"),
82+
),
83+
)
84+
}
85+
warmup.upsertChats(listOf(Chat(id = PeerId(7), title = "Kept")))
86+
warmup.setOpenChat(9L)
87+
warmup.cacheHistoryPage(PeerId(7), listOf(message(7, 121, recent)))
88+
warmup.pruneExpiredCache(nowMillis = now, force = true)
89+
run {
90+
val dao = database.messageDao()
91+
assertNull(dao.byIds(7L, listOf(1)).singleOrNull())
92+
assertNotNull(dao.byIds(7L, listOf(22)).singleOrNull())
93+
assertNotNull(dao.byIds(7L, listOf(121)).singleOrNull())
94+
assertTrue(dao.byIds(7L, listOf(-1)).single().pending)
95+
assertTrue(dao.byIds(7L, listOf(200)).single().failed)
96+
assertEquals(30, dao.newestConfirmedIds(9L, 100).size)
97+
assertEquals("Kept", database.chatDao().get(7L)?.title)
98+
val account = database.updateCursorDao().get("updates")
99+
assertEquals(4, account?.pts)
100+
assertEquals(5, account?.qts)
101+
assertEquals(6, account?.date)
102+
assertEquals(7, account?.seq)
103+
val channel = database.updateCursorDao().get("$KEY_CHANNEL_CURSOR_PREFIX" + "1")
104+
assertEquals(9, channel?.channelPts)
105+
assertNull(database.profileTabsDao().get(1L))
106+
assertNotNull(database.profileTabsDao().get(2L))
107+
assertTrue(database.profileMembersDao().list(1L).isEmpty())
108+
assertEquals(1, database.profileMembersDao().list(2L).size)
109+
assertTrue(database.profileMediaDao().list(1L).isEmpty())
110+
assertEquals(1, database.profileMediaDao().list(2L).size)
111+
assertTrue(database.profileCommonDao().list(1L).isEmpty())
112+
assertEquals(1, database.profileCommonDao().list(2L).size)
113+
assertTrue(database.messageTombstoneDao().deletedIds(7L, listOf(3)).isEmpty())
114+
assertEquals(listOf(50), database.messageTombstoneDao().deletedIds(7L, listOf(50)))
115+
assertNotNull(database.peerDao().get(7L))
116+
assertNull(database.peerDao().get(99L))
117+
assertNull(database.metaDao().get("discussion_read:7:1"))
118+
assertNotNull(database.metaDao().get("discussion_read:7:503"))
119+
val holes = database.messageHoleDao().forChat(7L, MessageHoleEntity.SOURCE_HISTORY)
120+
assertTrue(holes.any { 1 in it.startId..it.endId })
121+
assertTrue(holes.none { hole -> (22..121).any { it in hole.startId..hole.endId } })
122+
assertFalse(holes.any { 121 in it.startId..it.endId })
123+
}
124+
} finally {
125+
database.close()
126+
}
127+
}
128+
129+
private fun message(
130+
chatId: Long,
131+
id: Int,
132+
date: Long,
133+
pending: Boolean = false,
134+
failed: Boolean = false,
135+
) = Message(
136+
id = MessageId(PeerId(chatId), id),
137+
senderId = null,
138+
text = null,
139+
date = date,
140+
outgoing = pending || failed,
141+
pending = pending,
142+
failed = failed,
143+
randomId = if (pending) 99L else null,
144+
)
145+
}

‎app/src/androidTest/java/org/monogram/ComposerEditingTest.kt‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,29 @@ import androidx.compose.foundation.layout.fillMaxSize
55
import androidx.compose.runtime.CompositionLocalProvider
66
import androidx.compose.runtime.remember
77
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.geometry.Offset
89
import androidx.compose.ui.platform.LocalDensity
910
import androidx.compose.ui.platform.testTag
10-
import androidx.compose.ui.geometry.Offset
1111
import androidx.compose.ui.semantics.SemanticsProperties
12-
import androidx.compose.ui.text.TextRange
1312
import androidx.compose.ui.test.assertIsDisplayed
1413
import androidx.compose.ui.test.hasSetTextAction
1514
import androidx.compose.ui.test.hasText
1615
import androidx.compose.ui.test.junit4.v2.createEmptyComposeRule
16+
import androidx.compose.ui.test.longClick
17+
import androidx.compose.ui.test.onNodeWithContentDescription
1718
import androidx.compose.ui.test.onNodeWithText
18-
import androidx.compose.ui.test.performTextInput
1919
import androidx.compose.ui.test.performClick
20-
import androidx.compose.ui.test.onNodeWithContentDescription
20+
import androidx.compose.ui.test.performTextInput
2121
import androidx.compose.ui.test.performTouchInput
22-
import androidx.compose.ui.test.longClick
22+
import androidx.compose.ui.text.TextRange
2323
import androidx.compose.ui.unit.Density
2424
import androidx.test.core.app.ActivityScenario
2525
import androidx.test.platform.app.InstrumentationRegistry
2626
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
2727
import kotlinx.coroutines.flow.emptyFlow
2828
import org.junit.After
29-
import org.junit.Assert.assertTrue
3029
import org.junit.Assert.assertEquals
30+
import org.junit.Assert.assertTrue
3131
import org.junit.Rule
3232
import org.junit.Test
3333
import org.monogram.core.common.Outcome
@@ -63,7 +63,6 @@ class ComposerEditingTest {
6363
compose.runOnIdle { root.openChatForPlayback(1) }
6464
compose.onNodeWithText("Message 1").performTouchInput { longClick() }
6565
val context = InstrumentationRegistry.getInstrumentation().targetContext
66-
compose.onNodeWithText(context.getString(org.monogram.feature.dialog.R.string.dialog_select_message)).performClick()
6766
compose.onNodeWithText("Message 2").performClick()
6867
compose.onNodeWithContentDescription(context.getString(org.monogram.feature.dialog.R.string.dialog_forward)).performClick()
6968
compose.onNodeWithText(context.getString(org.monogram.feature.chats.R.string.chats_share_recipient)).assertIsDisplayed()

‎app/src/androidTest/java/org/monogram/MediaQueueTest.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class MediaQueueTest {
4242
val jobs = (1L..20L).map { id ->
4343
async(Dispatchers.IO) {
4444
val priority = if (id > 12) MediaPriority.VISIBLE else MediaPriority.IDLE
45-
repo.ensureLocalAvatar(PeerId(id), "photo:$id", priority)
45+
repo.ensureIndexedMedia(PeerId(id), 0, "photo:$id", priority = priority)
4646
}
4747
}
4848
val results = withTimeout(8_000) { jobs.map { it.await() } }

0 commit comments

Comments
 (0)