Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
10fa745
presentation: unify chat exit actions (leave vs delete)
gdlbo Jun 18, 2026
73f0cb3
presentation: Unify chat action states and refactor operations to sus…
gdlbo Jun 18, 2026
7c15557
chat: improve auto-scroll behavior and restrict read reporting to use…
gdlbo Jun 18, 2026
cb12182
Create AGENTS.md
gdlbo Jun 19, 2026
309ab9b
chats: separate media tap actions into open and download operations
gdlbo Jun 19, 2026
52870dd
viewers: restore device settings on exit and improve drag gestures
gdlbo Jun 19, 2026
1796ebc
Implement incoming system share intent handling and unify pending att…
gdlbo Jun 19, 2026
f23b476
settings: display inline supporter stats and donation links
gdlbo Jun 19, 2026
2895852
ui: Implement custom swipe-back gestures for archive and topics
gdlbo Jun 20, 2026
4287d77
chats: Implement local hide pin message
gdlbo Jun 26, 2026
295b2f8
Update dependencies and Gradle to latest versions
gdlbo Jun 26, 2026
d304a4a
Removed unused translations
gdlbo Jun 26, 2026
7bc3091
chats: Implement channel Telegram Ads support
gdlbo Jun 26, 2026
586a427
profiles: Implement new profile, chats and groups editing ui
gdlbo Jun 26, 2026
b4e4ac8
chats: Refactor open chat logic, added more logs
gdlbo Jun 29, 2026
6d33ae6
chats: Refactor media preview size
gdlbo Jun 29, 2026
9d8a975
chats: Added selfpromo banner
gdlbo Jun 30, 2026
c52dd41
chats: Add missing test overrides
gdlbo Jun 30, 2026
6870303
chats: Better scaling of images, added uri test
gdlbo Jun 30, 2026
21ef996
chats: Rich caching local info of messages
gdlbo Jun 30, 2026
c24de94
chats: Make cached message reads thread-aware
gdlbo Jun 30, 2026
57dbd9b
ext: Consolidate and improve attachment kind inference
gdlbo Jul 1, 2026
d01b65c
chats: Fix false recompositions of rich messages
gdlbo Jul 1, 2026
0ab146a
instantview: Handle errors when loading articles
gdlbo Jul 1, 2026
3d61533
menu: Remove help button
gdlbo Jul 1, 2026
f02f445
prefs: Default background service to disabled
gdlbo Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
470 changes: 470 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ dependencies {
implementation(project(":core"))

baselineProfile(project(":baselineprofile"))

testImplementation(libs.junit)
}

tasks.withType(DependencyTask::class.java).configureEach {
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="tg"/>
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>

<activity
Expand Down
162 changes: 162 additions & 0 deletions app/src/main/java/org/monogram/app/IncomingShareIntentResolver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package org.monogram.app

import android.content.ClipData
import android.content.Intent
import android.net.Uri
import android.os.Build
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.monogram.presentation.core.util.CopiedUriFile
import org.monogram.presentation.core.util.copyUriToTempDocumentFile
import org.monogram.presentation.core.util.copyUriToTempMediaFile
import org.monogram.presentation.features.share.IncomingShareRequest
import org.monogram.presentation.features.share.PendingAttachment
import org.monogram.presentation.features.share.PendingAttachmentKind

class IncomingShareIntentResolver(
private val copyUriToAttachment: (Uri) -> PendingAttachment?
) {
constructor(context: android.content.Context) : this(
copyUriToAttachment = { uri -> copyUriToAttachment(context, uri) }
)

suspend fun resolve(intent: Intent): IncomingShareRequest? = withContext(Dispatchers.IO) {
val payload = extractIncomingSharePayload(intent) ?: return@withContext null
val attachments = payload.uris.mapNotNull(copyUriToAttachment)

IncomingShareRequest(
requestId = System.currentTimeMillis(),
text = payload.text,
attachments = attachments
).takeIf { it.text.isNotBlank() || it.attachments.isNotEmpty() }
}
}

internal data class ExtractedIncomingSharePayload(
val text: String,
val uris: List<Uri>
)

internal data class IncomingSharePayloadInput(
val action: String?,
val text: String?,
val clipDataUris: List<String> = emptyList(),
val extraStreamUris: List<String> = emptyList()
)

internal data class NormalizedIncomingSharePayload(
val text: String,
val uriStrings: List<String>
)

internal fun extractIncomingSharePayload(intent: Intent): ExtractedIncomingSharePayload? {
val extraStreamUris = when (intent.action) {
Intent.ACTION_SEND -> listOfNotNull(
intent.parcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)?.toString()
)

Intent.ACTION_SEND_MULTIPLE -> intent.parcelableArrayListExtraCompat<Uri>(Intent.EXTRA_STREAM)
?.map(Uri::toString)
.orEmpty()

else -> emptyList()
}

val normalized = normalizeIncomingSharePayload(
IncomingSharePayloadInput(
action = intent.action,
text = intent.getStringExtra(Intent.EXTRA_TEXT),
clipDataUris = intent.clipData?.collectUriStrings().orEmpty(),
extraStreamUris = extraStreamUris
)
) ?: return null

return ExtractedIncomingSharePayload(
text = normalized.text,
uris = normalized.uriStrings.map(Uri::parse)
)
}

internal fun normalizeIncomingSharePayload(
input: IncomingSharePayloadInput
): NormalizedIncomingSharePayload? {
if (input.action != Intent.ACTION_SEND && input.action != Intent.ACTION_SEND_MULTIPLE) return null

val uris = LinkedHashSet<String>()
input.clipDataUris.asSequence()
.map(String::trim)
.filter(String::isNotBlank)
.forEach(uris::add)
input.extraStreamUris.asSequence()
.map(String::trim)
.filter(String::isNotBlank)
.forEach(uris::add)

val text = input.text.orEmpty().trim()
if (text.isBlank() && uris.isEmpty()) return null

return NormalizedIncomingSharePayload(
text = text,
uriStrings = uris.toList()
)
}

internal fun copyUriToAttachment(context: android.content.Context, uri: Uri): PendingAttachment? {
val mediaCopy = context.copyUriToTempMediaFile(uri)
val kind = mediaCopy?.classifyAsMediaKind()
if (kind != null) {
return PendingAttachment(
localPath = mediaCopy.localPath,
kind = kind,
deleteAfterUse = true
)
}

val documentCopy = context.copyUriToTempDocumentFile(uri) ?: return null
return PendingAttachment(
localPath = documentCopy.localPath,
kind = PendingAttachmentKind.DOCUMENT,
deleteAfterUse = true
)
}

private fun Intent.isShareIntent(): Boolean {
return action == Intent.ACTION_SEND || action == Intent.ACTION_SEND_MULTIPLE
}

private fun ClipData.collectUriStrings(): List<String> {
return buildList {
for (index in 0 until itemCount) {
getItemAt(index)?.uri?.toString()?.let(::add)
}
}
}

@Suppress("DEPRECATION")
private inline fun <reified T : android.os.Parcelable> Intent.parcelableExtraCompat(name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelableExtra(name, T::class.java)
} else {
getParcelableExtra(name)
}
}

@Suppress("DEPRECATION")
private inline fun <reified T : android.os.Parcelable> Intent.parcelableArrayListExtraCompat(name: String): ArrayList<T>? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelableArrayListExtra(name, T::class.java)
} else {
getParcelableArrayListExtra(name)
}
}

private fun CopiedUriFile.classifyAsMediaKind(): PendingAttachmentKind? {
val mime = mimeType.orEmpty().lowercase()
val name = (displayName ?: localPath).lowercase()
return when {
mime == "image/gif" || name.endsWith(".gif") -> PendingAttachmentKind.GIF
mime.startsWith("video/") -> PendingAttachmentKind.VIDEO
mime.startsWith("image/") -> PendingAttachmentKind.PHOTO
else -> null
}
}
21 changes: 21 additions & 0 deletions app/src/main/java/org/monogram/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.CompositionLocalProvider
Expand All @@ -14,6 +15,7 @@ import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.window.layout.WindowInfoTracker
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.retainedComponent
Expand All @@ -35,6 +37,7 @@ class MainActivity : FragmentActivity() {
private lateinit var root: RootComponent
private val appPreferences: AppPreferences by inject()
private val appForegroundTracker: AppForegroundTracker by inject()
private lateinit var incomingShareIntentResolver: IncomingShareIntentResolver

@Volatile
private var keepSplashOnScreen: Boolean = true
Expand Down Expand Up @@ -64,6 +67,7 @@ class MainActivity : FragmentActivity() {
)
)
}
incomingShareIntentResolver = IncomingShareIntentResolver(this)

handleIntent(intent)

Expand Down Expand Up @@ -98,10 +102,27 @@ class MainActivity : FragmentActivity() {

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIntent(intent)
}

private fun handleIntent(intent: Intent) {
if (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) {
lifecycleScope.launchWhenStarted {
val request = incomingShareIntentResolver.resolve(intent)
if (request != null) {
root.handleIncomingShare(request)
} else {
Toast.makeText(
this@MainActivity,
"Unable to share this content",
Toast.LENGTH_SHORT
).show()
}
}
return
}

val data = intent.dataString
if (data != null) {
root.handleLink(data)
Expand Down
Loading