Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ most things are toggleable in `Settings → Inugram`, with sensible opinionated
- sticker creator output sent as photo when high-quality default is on
- recyclerlistview double-tap requires same view
- dialogs list pull-to-reveal-archive glitches
- standalone TTF/OTF fonts with inflated vertical metrics don't add bogus top/bottom padding
- shared media player visual glitches
- shared media pager: fling mid-animation to chain tabs or reverse (was ignored until settled); at the edge tab the fling falls through to swipe-to-close
- attach panel: better perf, safe close before fully open
Expand Down
41 changes: 41 additions & 0 deletions patches/bugfix/proxy-list-md3-sections.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andrew <kezin636@gmail.com>
Date: Thu, 16 Jul 2026 12:32:04 +0300
Subject: Fix MD3 sections style not applying in Proxy Settings list

---
.../src/main/java/org/telegram/ui/ProxyListActivity.java | 5 -----
1 file changed, 5 deletions(-)

diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java
index 0000000..0000000 100644
--- a/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java
+++ b/TMessagesProj/src/main/java/org/telegram/ui/ProxyListActivity.java
@@ -1008,27 +1008,22 @@ public class ProxyListActivity extends BaseFragment implements NotificationCente
break;
case VIEW_TYPE_TEXT_SETTING:
view = new TextSettingsCell(mContext);
- view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite));
break;
case VIEW_TYPE_HEADER:
view = new HeaderCell(mContext);
- view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite));
break;
case VIEW_TYPE_TEXT_CHECK:
view = new TextCheckCell(mContext);
- view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite));
break;
case VIEW_TYPE_INFO:
view = new TextInfoPrivacyCell(mContext);
break;
case VIEW_TYPE_SLIDE_CHOOSER:
view = new SlideChooseView(mContext);
- view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite));
break;
case VIEW_TYPE_PROXY_DETAIL:
default:
view = new TextDetailProxyCell(mContext);
- view.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite));
break;
}
view.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT));
1 change: 1 addition & 0 deletions series
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,4 @@ bugfix/catch-shared-media-swipe.patch
bugfix/recyclerlist-ripple.patch
bugfix/popup-drag-select-high-ids.patch
feature/web-preview-spoiler.patch
bugfix/proxy-list-md3-sections.patch
41 changes: 40 additions & 1 deletion src/kotlin/helpers/dialogs/DrawerHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import desu.inugram.helpers.update.UpdateHelper
import desu.inugram.ui.drawer.DrawerAddCell
import desu.inugram.ui.drawer.DrawerLayoutAdapter
import desu.inugram.ui.drawer.DrawerProfileCell
import desu.inugram.ui.drawer.DrawerProxyCell
import desu.inugram.ui.drawer.DrawerSwipeController
import desu.inugram.ui.drawer.DrawerUserCell
import desu.inugram.ui.drawer.SideMenultItemAnimator
Expand All @@ -26,6 +27,7 @@ import org.telegram.messenger.ApplicationLoader
import org.telegram.messenger.FileLoader
import org.telegram.messenger.ImageLoader
import org.telegram.messenger.LocaleController.getString
import org.telegram.tgnet.ConnectionsManager
import org.telegram.messenger.MessagesController
import org.telegram.messenger.NotificationCenter
import org.telegram.messenger.R
Expand All @@ -49,6 +51,7 @@ import org.telegram.ui.LaunchActivity
import org.telegram.ui.LoginActivity
import org.telegram.ui.MainTabsActivity
import org.telegram.ui.ProfileActivity
import org.telegram.ui.ProxyListActivity
import org.telegram.ui.SettingsActivity
import org.telegram.ui.UpdateLayoutWrapper

Expand All @@ -59,6 +62,7 @@ object DrawerHelper {
private var sideMenu: RecyclerListView? = null
private var sideMenuContainer: FrameLayout? = null
private var themeObserver: NotificationCenter.NotificationCenterDelegate? = null
private var proxyObserver: NotificationCenter.NotificationCenterDelegate? = null
private var updateLayout: IUpdateLayout? = null
private var updateObserver: NotificationCenter.NotificationCenterDelegate? = null
private var updateObserverAccount: Int = -1
Expand Down Expand Up @@ -120,7 +124,7 @@ object DrawerHelper {
val sm = RecyclerListView(context)
sm.layoutManager = LinearLayoutManager(context)
val itemAnimator = SideMenultItemAnimator(sm)
val newAdapter = DrawerLayoutAdapter(context, itemAnimator, drawerLayoutContainer)
val newAdapter = DrawerLayoutAdapter(context, itemAnimator, drawerLayoutContainer, ::applyProxyEnabled)
adapter = newAdapter
sideMenu = sm
sm.setItemAnimator(itemAnimator)
Expand Down Expand Up @@ -157,6 +161,7 @@ object DrawerHelper {
controller.setAllowOpenDrawer(true, false)

installThemeObserver()
installProxyObserver()
installUpdateLayout(context as? Activity, container, sm)
}

Expand Down Expand Up @@ -348,6 +353,34 @@ object DrawerHelper {
NotificationCenter.getGlobalInstance().addObserver(obs, NotificationCenter.reloadInterface)
}

private fun installProxyObserver() {
if (proxyObserver != null) return
val obs = NotificationCenter.NotificationCenterDelegate { id, _, _ ->
if (id == NotificationCenter.proxySettingsChanged) {
adapter?.notifyDataSetChanged()
}
}
proxyObserver = obs
NotificationCenter.getGlobalInstance().addObserver(obs, NotificationCenter.proxySettingsChanged)
}

private fun applyProxyEnabled(enabled: Boolean) {
val proxy = if (enabled) SharedConfig.currentProxy else null
MessagesController.getGlobalMainSettings().edit()
.putBoolean("proxy_enabled", enabled && proxy != null)
.apply()
if (proxy != null) {
ConnectionsManager.setProxySettings(
true, proxy.address, proxy.port,
proxy.username, proxy.password, proxy.secret
)
} else {
ConnectionsManager.setProxySettings(false, "", 0, "", "", "")
}
NotificationCenter.getGlobalInstance()
.postNotificationName(NotificationCenter.proxySettingsChanged)
}

private fun refreshTheme() {
sideMenuContainer?.setBackgroundColor(Theme.getColor(Theme.key_chats_menuBackground))
sideMenu?.let { applySideMenuColors(it) }
Expand Down Expand Up @@ -453,6 +486,11 @@ object DrawerHelper {
close()
}

ITEM_PROXY -> {
nav.presentFragment(ProxyListActivity())
close()
}

else -> close()
}
}
Expand All @@ -465,6 +503,7 @@ object DrawerHelper {
private const val ITEM_CALLS = 10
private const val ITEM_SAVED_MESSAGES = 11
private const val ITEM_SETTINGS = 8
private const val ITEM_PROXY = DrawerLayoutAdapter.ITEM_PROXY

@JvmStatic
fun notifyDataChanged() {
Expand Down
205 changes: 205 additions & 0 deletions src/kotlin/helpers/font/FontLibrary.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.graphics.fonts.FontStyle
import android.graphics.fonts.SystemFonts
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import desu.inugram.helpers.font.FontLibrary.buildEditorRoster
import desu.inugram.helpers.font.SfntParser.Script
Expand All @@ -19,6 +20,7 @@ import org.telegram.messenger.Utilities
import org.telegram.ui.Components.Paint.PaintTypeface
import java.io.File
import java.io.FileOutputStream
import java.io.RandomAccessFile

/**
* Owns the editor **font roster** and the storage behind it: imported font families, discovered device
Expand Down Expand Up @@ -355,6 +357,208 @@ object FontLibrary {
File(sub, MANIFEST).writeText(root.toString())
}

/**
* Some Google Fonts downloads (notably Google Sans variable) carry a reasonable line box but a wildly
* oversized safety bbox/win box. Android can let those outer boxes leak into TextView top/bottom metrics,
* which shifts Telegram text. Clamp only the outer boxes to a padded line box; keep hhea/typo metrics.
*/
private fun normalizeVerticalMetrics(file: File) {
try {
RandomAccessFile(file, "rw").use { raf ->
when (val magic = raf.readInt()) {
0x74746366 -> Unit // TTC checkSumAdjustment is per-face; skip rather than risk corrupting it.

Comment on lines +368 to +370

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Either normalize TTC faces or narrow the feature scope.

The implementation explicitly skips TTC containers while the feature documentation covers imported fonts generally.

  • src/kotlin/helpers/font/FontLibrary.kt#L367-L369: traverse each TTC font directory and update affected table checksums, or explicitly reject/exclude TTC normalization.
  • FEATURES.md#L221-L221: limit the claim to standalone TTF/OTF files until TTC support exists.
📍 Affects 2 files
  • src/kotlin/helpers/font/FontLibrary.kt#L367-L369 (this comment)
  • FEATURES.md#L221-L221
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/kotlin/helpers/font/FontLibrary.kt` around lines 367 - 369, TTC
containers are currently skipped by the font normalization logic in
FontLibrary.kt around the readInt magic check; either implement traversal of
each TTC face so affected table checksums are updated, or explicitly
exclude/reject TTC files. If TTC support is not added, update FEATURES.md at
line 221 to claim normalization only for standalone TTF/OTF files; both listed
sites require corresponding changes.

Source: Coding guidelines

0x00010000, 0x4F54544F, 0x74727565 -> {
if (normalizeFaceVerticalMetrics(raf, 0)) {
Log.d(TAG, "normalizeVerticalMetrics: normalized ${file.name}")
}
}

else -> Log.d(TAG, "normalizeVerticalMetrics: unknown magic 0x${Integer.toHexString(magic)} in ${file.name}")
}
}
} catch (e: Throwable) {
FileLog.e("$TAG: normalizeVerticalMetrics: failed for ${file.name}", e)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

private fun normalizeFaceVerticalMetrics(raf: RandomAccessFile, sfntOffset: Int): Boolean {
raf.seek(sfntOffset.toLong())
val sfntVer = raf.readInt()
if (sfntVer != 0x00010000 && sfntVer != 0x4F54544F && sfntVer != 0x74727565) return false
val numTables = raf.readShort().toInt() and 0xffff
raf.skipBytes(6)

var os2: SfntTable? = null
var head: SfntTable? = null
var maxp: SfntTable? = null
var loca: SfntTable? = null
var glyf: SfntTable? = null
for (i in 0 until numTables) {
val recordOffset = raf.filePointer
val tag = raf.readInt()
raf.readInt()
val off = raf.readInt()
val length = raf.readInt()
when (tag) {
0x4F532F32 -> os2 = SfntTable(recordOffset, off, length) // OS/2
0x68656164 -> head = SfntTable(recordOffset, off, length) // head
0x6D617870 -> maxp = SfntTable(recordOffset, off, length) // maxp
0x6C6F6361 -> loca = SfntTable(recordOffset, off, length) // loca
0x676C7966 -> glyf = SfntTable(recordOffset, off, length) // glyf
}
}
val os2Table = os2 ?: return false

raf.seek(os2Table.offset.toLong() + 68)
val typoAsc = raf.readShort().toInt()
val typoDesc = raf.readShort().toInt()
val typoGap = raf.readShort().toInt()
raf.seek(os2Table.offset.toLong() + 74)
val winAsc = raf.readShort().toInt() and 0xffff
val winDesc = raf.readShort().toInt() and 0xffff
val headTable = head
var headYMin = 0
var headYMax = 0
if (headTable != null) {
raf.seek(headTable.offset.toLong() + 38)
headYMin = raf.readShort().toInt()
raf.skipBytes(2)
headYMax = raf.readShort().toInt()
}

val typoDescAbs = -typoDesc
val typoTotal = typoAsc + typoDescAbs + typoGap
val winTotal = winAsc + winDesc
if (typoAsc <= 0 || typoDescAbs <= 0 || typoTotal <= 0) return false

val shouldNormalizeWinBox = winTotal > typoTotal * 3 / 2 || winDesc > typoDescAbs * 2
val headDescAbs = -headYMin
val shouldNormalizeHeadBox = headTable != null && (headDescAbs > typoDescAbs * 2 || headYMax > typoAsc * 3 / 2)
if (!shouldNormalizeWinBox && !shouldNormalizeHeadBox) return false

val glyphExtents = glyphYExtents(raf, headTable, maxp, loca, glyf)
val minAsc = glyphExtents?.yMax ?: headYMax.coerceAtLeast(0)
val minDesc = -(glyphExtents?.yMin ?: headYMin.coerceAtMost(0))
val targetDesc = (typoDescAbs + typoTotal / 16).coerceAtMost(typoDescAbs * 2)
val targetAsc = (typoAsc + typoTotal / 10).coerceAtMost(typoAsc * 3 / 2)
val safeTargetDesc = targetDesc.coerceAtLeast(minDesc)
val safeTargetAsc = targetAsc.coerceAtLeast(minAsc)
var changed = false

if (shouldNormalizeHeadBox && headTable != null && (safeTargetDesc != headDescAbs || safeTargetAsc != headYMax)) {
raf.seek(headTable.offset.toLong() + 38)
raf.writeShort((-safeTargetDesc).coerceIn(Short.MIN_VALUE.toInt(), -1))
raf.skipBytes(2)
raf.writeShort(safeTargetAsc.coerceIn(1, 0xffff))
changed = true
}

val winTargetAsc = safeTargetAsc.coerceAtMost(winAsc)
val winTargetDesc = safeTargetDesc.coerceAtMost(winDesc)
if (shouldNormalizeWinBox && (winTargetAsc != winAsc || winTargetDesc != winDesc)) {
raf.seek(os2Table.offset.toLong() + 74)
raf.writeShort(winTargetAsc.coerceIn(1, 0xffff))
raf.writeShort(winTargetDesc.coerceIn(1, 0xffff))
updateTableChecksum(raf, os2Table)
changed = true
}
if (changed) headTable?.let { updateCheckSumAdjustment(raf, it) }
return changed
}

private data class SfntTable(val recordOffset: Long, val offset: Int, val length: Int)
private data class GlyphYExtents(val yMin: Int, val yMax: Int)

private fun glyphYExtents(
raf: RandomAccessFile,
head: SfntTable?,
maxp: SfntTable?,
loca: SfntTable?,
glyf: SfntTable?,
): GlyphYExtents? {
if (head == null || maxp == null || loca == null || glyf == null) return null
return try {
raf.seek(head.offset.toLong() + 50)
val longLoca = raf.readShort().toInt() != 0
raf.seek(maxp.offset.toLong() + 4)
val glyphCount = raf.readShort().toInt() and 0xffff
if (glyphCount <= 0) return null

fun locaOffset(index: Int): Int {
raf.seek(loca.offset.toLong() + if (longLoca) index * 4L else index * 2L)
return if (longLoca) raf.readInt() else (raf.readShort().toInt() and 0xffff) * 2
}

var yMin = Int.MAX_VALUE
var yMax = Int.MIN_VALUE
for (i in 0 until glyphCount) {
val start = locaOffset(i)
val end = locaOffset(i + 1)
if (end <= start || start < 0 || end > glyf.length) continue
raf.seek(glyf.offset.toLong() + start + 4)
val glyphYMin = raf.readShort().toInt()
raf.skipBytes(2)
val glyphYMax = raf.readShort().toInt()
if (glyphYMin < yMin) yMin = glyphYMin
if (glyphYMax > yMax) yMax = glyphYMax
}
if (yMin == Int.MAX_VALUE || yMax == Int.MIN_VALUE) null else GlyphYExtents(yMin, yMax)
} catch (_: Throwable) {
null
}
}

private fun updateTableChecksum(raf: RandomAccessFile, table: SfntTable) {
val checksum = tableChecksum(raf, table.offset.toLong(), table.length)
raf.seek(table.recordOffset + 4)
raf.writeInt(checksum)
}

private fun updateCheckSumAdjustment(raf: RandomAccessFile, head: SfntTable) {
val adjustmentOffset = head.offset.toLong() + 8
raf.seek(adjustmentOffset)
raf.writeInt(0)
val headChecksum = tableChecksum(raf, head.offset.toLong(), head.length)
raf.seek(head.recordOffset + 4)
raf.writeInt(headChecksum)

val fileSum = tableChecksum(raf, 0, raf.length().coerceAtMost(Int.MAX_VALUE.toLong()).toInt()).toLong() and 0xffffffffL
raf.seek(adjustmentOffset)
raf.writeInt((0xB1B0AFBAL - fileSum).toInt())
}

private fun tableChecksum(raf: RandomAccessFile, offset: Long, length: Int): Int {
var sum = 0L
val buffer = ByteArray(8192)
var remaining = length
raf.seek(offset)
while (remaining > 0) {
val read = minOf(buffer.size, remaining)
raf.readFully(buffer, 0, read)
var pos = 0
while (pos + 4 <= read) {
sum = (sum + ((buffer[pos].toLong() and 0xffL) shl 24) +
((buffer[pos + 1].toLong() and 0xffL) shl 16) +
((buffer[pos + 2].toLong() and 0xffL) shl 8) +
(buffer[pos + 3].toLong() and 0xffL)) and 0xffffffffL
pos += 4
}
if (pos < read) {
var word = 0L
var shift = 24
while (pos < read) {
word = word or ((buffer[pos].toLong() and 0xffL) shl shift)
shift -= 8
pos++
}
sum = (sum + word) and 0xffffffffL
}
remaining -= read
}
return sum.toInt()
}

/** Returns (roster entries or null if absent, hidden entry set). */
private fun readIndex(): Pair<MutableList<FontId>?, Set<FontId>> {
val dir = rootDir ?: return null to emptySet()
Expand Down Expand Up @@ -623,6 +827,7 @@ object FontLibrary {
tmp.delete()
return StageResult.UNPARSEABLE
}
normalizeVerticalMetrics(tmp)
if (!isLoadableBySystem(tmp)) {
tmp.delete()
return StageResult.UNSUPPORTED
Expand Down
Loading