Inugram is a patchset, not a fork. worktree/ is a stock Telegram checkout with
stgit patches applied on top. Fork code lives in src/kotlin/src/res (symlinked
into the worktree). patches/ and series are export targets, not source of truth.
FEATURES.md is the user-facing list of fork features/bugfixes. Keep it in sync —
when adding, removing or meaningfully changing a patch, update FEATURES.md in
the same change.
- Edit
worktree/directly. Never hand-editpatches/*.patchorseries— they regenerate from stgit. - Do not run
stgorgityourself unless explicitly asked. Read-onlystg top/stg showis fine. NEVER runstg export. - Stock patches stay tiny. Only wiring/hooks/guards. Real logic goes in
src/kotlin. A patch touching onlysrc/**is usually wrong. - Default off = stock-identical. Every behavior change gated behind an
InuConfig.*.getValue()check. Verify every call site is gated. - Check if stock already does it before implementing a toggle (e.g. Lite Mode often has it). Tell the user, don't silently re-implement.
- Confirm bug repro in unpatched worktree before treating a visual/behavior issue as a patch regression.
- No renames in stock. No removing stock imports (except
desu.inugram.*). - Prefer data-layer patches over UI-layer — one hook in a controller beats fifteen hooks in views.
- Never touch
TLRPC.java— auto-generated, rebasing changes there is hell. - Never touch stock DB schema or
LAST_DB_VERSION— fork state goes ininu_*tables /inu_kvviaInuDatabaseHelper. - No LSP, no local build. Don't try to compile.
- Debug logs use
android.util.Log.d, notFileLog. - Prefer non-
_solaricons when an alternative exists.
Format: group__name → patches/<group>/<name>.patch. Commit subject = plain human sentence (Allow editing by double tapping a message).
| group | when |
|---|---|
bugfix |
fixes an upstream bug |
feature |
adds user-facing capability (qol, ui tweak, customization) |
debloat |
hides/disables stock behavior behind a toggle |
hooks |
thin stock hooks for fork code to attach to; no user-visible change alone |
misc |
build, branding, infra |
debloat vs feature: only removes/toggles off stock → debloat. Adds new capability → feature. visual__, ui__, etc. are not valid groups.
Propose a patch name (and comment) for every newly made patch — don't touch stgit yourself.
public void doSomething() {
if (desu.inugram.InuConfig.MY_TOGGLE.getValue()) {
MyHelper.handle(this);
return;
}
// ...stock code unchanged...
}- Guard goes before stock, early-returns when fork takes over.
- For mode-dependent behavior, prefer an
if/elsewrapper with no re-indentation of the stock branch — keeps rebases trivial. - When extending behavior rather than replacing it, run fork logic after the stock block. Don't rewrite stock.
- When figuring out stock code history/regressions, make sure to run git inside the
worktree/dir. Root dir is just the fork code, it DOES NOT track stock code.
privatefield/method needed from fork? Change topublic. That is the whole patch.- Adding a new field/method/overload to a stock class? Prefix
inu_(Java fields too:inu_addTab,inu_internalType, etc.). - Prefer exposing over adding. Adding to a base class is especially rebase-fragile — look for an existing extension point first.
- <~5–7 lines of logic → inline in the patch.
- Bigger → extract to a Kotlin helper.
- Helper reads
InuConfigitself; don't pass config values as parameters. - Helper references stock constants directly (make them
publicif needed). - One helper per feature area (e.g.
FolderHelperowns icons + DB + layout + drawing).
- Bugfix in a specific stock class → write the fix inline in that Java class.
EditTextBoldCursorbugs get fixed inEditTextBoldCursor.java. Don't detour through a Kotlin helper just to keep the patch "clean". - Non-trivial feature logic → Kotlin helper.
- Pure config toggle with no Java wiring → don't write a stock patch at all.
Paths under worktree/TMessagesProj/src/main/java/. Line counts approximate.
Files >2k lines: never Read top-to-bottom. rg for the exact symbol, then Read with offset + small limit.
| file | ~lines | owns |
|---|---|---|
org/telegram/ui/ChatActivity.java |
46k | chat screen |
org/telegram/ui/Cells/ChatMessageCell.java |
29k | message bubble |
org/telegram/ui/PhotoViewer.java |
24k | photo/video viewer + preview for ChatAttachAlert |
org/telegram/messenger/MessagesController.java |
24k | messages domain state |
org/telegram/ui/ProfileActivity.java |
17k | profile screen |
org/telegram/ui/Components/ChatActivityEnterView.java |
15k | message input — voice, attach, text |
org/telegram/ui/DialogsActivity.java |
14k | main page / dialogs list |
org/telegram/ui/Components/SharedMediaLayout.java |
13k | profile shared-media player |
org/telegram/messenger/MediaDataController.java |
10k | stickers, reactions, recent data |
org/telegram/ui/LoginActivity.java |
10k | login flow |
org/telegram/ui/LaunchActivity.java |
9k | root activity |
org/telegram/ui/Components/ChatAttachAlert.java |
7k | attachments panel |
org/telegram/ui/Cells/DialogCell.java |
6k | single dialog row |
org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java |
5k | attach panel photo grid |
org/telegram/messenger/LocaleController.java |
4.5k | i18n |
org/telegram/ui/Components/ReactionsContainerLayout.java |
2.6k | reactions bar in message menu |
org/telegram/ui/Components/FilterTabsView.java |
2k | folder tabs strip in DialogsActivity |
org/telegram/messenger/SharedConfig.java |
2k | stock prefs |
org/telegram/ui/Components/Reactions/ReactionsLayoutInBubble.java |
1.9k | inline reaction chips on messages |
org/telegram/ui/Components/EditTextBoldCursor.java |
1.3k | text input base (used by ~every input) |
org/telegram/ui/MainTabsActivity.java |
1k | main bottom tabs |
org/telegram/ui/Components/glass/GlassTabView.java |
0.6k | liquid-glass tab rendering |
org/telegram/messenger/LiteMode.java |
0.4k | perf flag presets |
When adding to a hotspot, check patches/hooks/ first — it likely already exposes the surface you need.
Standalone hook patches expose surfaces (menu builders, callbacks, public field promotions, inu_* helpers) that multiple features consume. Intentionally no user-visible effect on their own.
| patch | what it exposes |
|---|---|
admin-logs.patch |
hooks inside admin logs activity |
app-loader.patch |
custom ApplicationLoaderImpl instead of stock |
chat-activity.patch |
various ChatActivity hooks — message menu (ChatHelper.addMenuItems/processMenuOption), undoView, replyingMessageObject etc. |
icon-replacement.patch |
custom resource loader for icon replacement |
internal-web-app.patch |
WebViewRequestProps.inu_internalType + WebAppHelper.getInternalBotName for internal bot web sheets |
loginactivity.patch |
hooks inside LoginActivity |
messagescontroller.patch |
access MessagesController instances as they're created |
notifications-controller.patch |
hooks inside NotificationsController |
photo-viewer-menu.patch |
PhotoViewerHelper.{addMenuItems,updateMenuItems,resetMenuItems,handleMenuClick} + inu_getCurrentPhotoFile; exposes containerView, menuItem, showDownloadAlert |
popup-swipeback.patch |
foreground translation + unified touch coords on swipeback popup |
profile-menu.patch |
ProfileHelper.addMenuItems + ProfileHelper.handleMenuClick |
universal-recycler.patch |
extra features in UniversalRecyclerView used by settings pages |
When to add a hooks/ patch vs a normal patch:
- New stock surface that >1 future patch will wire into →
hooks/. - One-off wiring for a single feature → keep inside the
feature//debloat/patch. - Rule of 3: if 3+ existing patches touch roughly the same stock surface, consolidate.
- A
hooks/patch must be functionally a no-op with its consumers stubbed out.
Conventions: expose the minimum, promote private → public over duplicating data, inu_ prefix on new fields, entry point is always a call to desu.inugram.helpers.XxxHelper.* — never inline logic.
Live in src/kotlin/helpers/. Sub-packages by feature area: chat/, dialogs/, menu/, translate/, search/, media/, font/, update/, cloud/, security/, theme/, profile/, icons/, maps/, notifications/. Cross-cutting / standalone ones stay flat.
Naming (don't mass-rename):
*Helper= feature-coordinator singleton*Config=InuConfig.Itemsubclass / data model*Utils/*Parser/*Drawable/*Resources= concrete type or algorithm
Common entry-point helpers: ChatHelper (chat features), ProfileHelper (profile menu), PhotoViewerHelper (photo viewer), FolderHelper (folder tabs), MainTabsHelper (bottom tabs), MonetHelper (theming), NonIslandHelper (non-island UI gating), InuDatabaseHelper (fork DB), InuUtils (id generation etc.).
Before creating a new helper, check whether an existing one owns the area.
src/kotlin/InuHooks.kt. Generic lifecycle dispatch only — feature-specific code goes on its own helper.
Currently exposed (update this table when adding):
| method | called from | purpose |
|---|---|---|
init(Context) |
ApplicationLoader.onCreate |
bootstrap InuConfig, fonts, crash reporter, etc. |
onResume(LaunchActivity) |
LaunchActivity.onResume |
monet refresh, crash sheet |
onUpdate(TLObject?, Int) |
update dispatch | fork LoginHelper hook |
onDeepLink(LaunchActivity, Intent?) |
deeplink handling | passcode + settings deeplinks |
onAuthSuccess(Int) |
login flow | clear per-account passcode |
onMessagesControllerCreated(MessagesController, Int) |
MessagesController.<init> |
per-account setup (maps provider; registers the didReceiveNewMessages → onNewMessage observer) |
onNewMessage(TLRPC.Message, Int) |
didReceiveNewMessages observer |
generic new-message dispatch (all arrival paths incl. difference catch-up); fans out to UpdateHelper etc. |
syncDoubleTapDelay() |
fork + init |
propagate DOUBLE_TAP_DELAY into stock gesture detectors |
syncAnimationSpeed() |
fork + init |
propagate ANIMATION_SPEED into stock animators |
syncChatInputRowHeight() |
fork + init |
propagate classic-ui input row height/padding into ChatActivityEnterView statics |
getCurrentAppIconLicense() |
About page | current launcher icon's license string |
New hook → @JvmStatic fun on InuHooks, one-line call site in the patch, update this table.
@JvmField val HIDE_STORIES = BoolItem("hide_stories", false)- Always
@JvmFieldso Java sees a field, notgetHIDE_STORIES(). - Types:
BoolItem,IntItem,FloatItem,StringItem. SubclassItem<T>for anything else (enums — seeFoldersDisplayModeItem,FormattingPopupConfig). BoolItemhas.toggle().- From Java:
InuConfig.HIDE_STORIES.getValue()— never.value(@JvmFieldexposes the wrapper, not its inner value). - Pref key = snake_case of the field name; default is the second arg. SharedPreferences name:
inugram. Loaded once fromInuHooks.init.
- Stock schema and
LAST_DB_VERSIONare off-limits. - Fork versioning lives in
inu_kv, managed byInuDatabaseHelper. - Fork tables:
inu_*prefix, created/migrated inInuDatabaseHelper.migrate(). - Populate fork fields by hooking stock load/save calls (see
patches/feature/folders-display-mode.patch) — don't edit stock SQL.
- Extend
desu.inugram.ui.settings.SettingsPageActivity(wrapsUniversalFragmentwith edge-to-edge + insets +showRestartBulletin()). Register pages inInuSettingsActivity. - Prefer adding to an existing page:
AppearanceSettingsActivity— general appearanceChatsSettingsActivity— chat-related appearance (bubbles, menus)MessagesSettingsActivity— message bubble / inline reactions / sticker sizeDialogsSettingsActivity— dialogs list (main page) appearanceAnnoyancesSettingsActivity— removes annoying stock stuff (only when user explicitly asks)BehaviorSettingsActivity— general behavior
- Any toggle needing a restart → call
showRestartBulletin()in the click handler (verify restart is actually needed). - Custom cells:
SliderCell,ExpandableBoolGroup,RadioDialogBuilder,StickerSizePreviewMessagesCell.
desu.inugram.SearchRegistrywires fork pages into stock settings search (ProfileActivity.SearchAdapter) and routestg://settings/inu/<slug>deeplinks.- Each searchable
*SettingsActivitydeclares a@JvmField val PAGE = SearchRegistry.Page(...)in its companion: pageslug, title res, icon res, factory, list ofSearchRegistry.Entry(slug, titleRes, itemId)— one per searchableUItem.itemIdreuses the page'sInuUtils.generateId()constant (also used as theUItem.id). - Register in
SearchRegistry.pages. Slugs are persistent identity (deeplinks + recents), globally unique — uniqueness asserted at first access. Renaming a slug is a breaking change. - Row highlight on open:
SettingsPageActivity.withHighlight(itemId)+ existingonTransitionAnimationEndhook. No extra wiring per page.
src/res/values/strings_inu.xml. All keys prefixedInu(InuHideStories).- Subtitle/info strings: same key +
Infosuffix (InuHideStoriesInfo). - Access:
LocaleController.getString(R.string.InuXxx).
src/res/drawable/(density-independent),src/res/drawable-xxhdpi/(bitmaps),src/res/assets/.- New asset dir → add path to
scripts/config.ts→forkSyncFiles. - Icons: lucide pre-bundled; selection list in
scripts/config.ts→ICON_SELECTION. Tabler pack preferred for visual consistency.
src/res/assets/monet_{light,dark,amoled}.attheme — stock attheme format, values resolved by
MonetHelper.getColor (hooked into Theme.getThemeFileValues by feature/monet-theme.patch).
- Values are palette tones (
a1_600,n1_50), M3 semantic tokens (monet_surface_container_light), custom names (monetGreen), or raw ints. - Modifiers:
(a=)alpha %,(s=)blend→white %,(l=)blend→black %,(t=)absolute HCT tone,(c=)HCT chroma multiplier % (0–400, relative so monochrome palettes stay gray). Comma-separated:monet_secondary_container_light(t=90,c=75). - Debug hot reload:
pnpm run push-theme [light|dark|amoled] [--watch] [--clear] [-s <serial>]— adb-pushes the asset to the app's external files dir and broadcastsdesu.inugram.RELOAD_THEME. Debug builds only (getThemeOverrideFileis a no-op otherwise); the app must be running.
.value(Kotlin) →.getValue()from Java.- Kotlin
object→InuXxx.INSTANCE.method()from Java unless@JvmStatic. - For hooks called from stock Java, default to
@JvmStatic fun foo(...)on a Kotlinobject— cleanest call site. - Inside stgit patches, the
worktree/prefix is omitted from paths. LayoutHelper.createLinear/createFramemargin args are dp either way (both int and float overloads pass throughAndroidUtilities.dp(...)). But Kotlin won't auto-promoteInt → Float, and several overloads exist only in the Float variant — notably the 6-argcreateLinear(w, h, l, t, r, b). Write12fnot12for margins or you'll hit "actual type is Int, but Float was expected".
Don't overuse @JvmStatic, only add it if the method/field is actually accessed from Java.
- Running
stg/git. Don't. Read-onlystg top/stg showonly. - Hand-editing
patches/*.patch. They're exports. Editworktree/; user re-exports. - Oversized stock patches. Logic beyond a guard + helper call → move to Kotlin.
- Helper for 2–5 lines. Inline it. Only extract when >5–7 lines or genuinely reused.
- Replacing stock behavior instead of running after it. Stock stays intact; fork logic runs before (early return) or after, gated by config.
- Routing a trivial set through a helper method. If the patch just assigns a field based on config, assign in-place at the stock call site.
- Modifying stock base classes. Look for an existing extension hook first (stock often has setup hooks for themed things). Base-class edits rebase poorly.
- Writing Kotlin helpers for what must be a Java fix. Bug in
EditTextCaption→ fix it inEditTextCaption.java. Don't detour. - Ungated fork behavior. Default-off must equal stock. Verify every call site.
- Java using
.value. It's.getValue(). Kotlin.valueis a property;@JvmFieldonly exposes the wrapper. - Forgetting
inu_prefix when adding fields/methods/overloads to stock classes. Including Java fields. - Re-indenting stock to wrap it in an
if. Kills rebases. Use early returns, add-after-stock, or keep indentation the same.
You never run these unless explicitly asked — documented so you can answer questions / suggest commands.
# create a new patch
stg new feature__my-patch -m 'Allow editing by double tapping a message'
# ...edit worktree/...
stg refresh
pnpm run export
# modify existing patch in-place
# ...edit worktree/...
stg refresh -p feature__my-patch # --index for staged-only
# modify existing patch, floating to top (preferred for non-trivial changes)
stg float feature__my-patch
# ...edit...
stg refresh
pnpm run exportpnpm run export rewrites patches/ + series from the stack. User runs it.
If user asks "which patch am I on" → stg top.
When adding a new InuHooks method, settings page, or shared hooks/ patch — update this file. Tribal knowledge rots.