on main help sign in
Add an option to remove the chat list search bar

"Hide chat list search" in Fork Client Settings > Chat list view, right after Disable Global Search and off by default, removes the 48 dp search field above the main chat list, its folders, the Archive and community lists, and the header search icon that stock shows once the bar has scrolled away. The list lays out as stock does with the bar scrolled away, minus the space the bar reserves, so the first chat sits right under the header. Search opened on purpose still works as stock: the Downloads item, tg://search links and the forum topics column's search icon show the field in the header while searching. Pickers and hashtag screens keep the stock bar. HideSearchBar latches the setting per list at first use and switches it in onResume, re-anchoring each list before onResume's notifyDataSetChanged (which skips stock's own re-anchor), so a changed setting applies when Chats is shown again without a gap or a stray Archive row, and the header icon follows through checkUi_menuItems(). Upstream edits are one-line hooks marked "MultiGram:": 19 in DialogsActivity, most of them swapping only the bar height inside the original dp(...) so the option off is exactly stock, and 2 in ForkSettingsActivity for the row. hide_search_bar_check.py checks the hooks, their placement (the icon hook on checkUi_itemSearchVisibility's factor0 line, the identical line in checkUi_searchFieldVisibility left stock), the entries that still open search, every other use of the bar height against a reviewed list, and the one owned setting; check.sh runs it. The README lists what still opens search and what is lost. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mVy1fu8zBn38wHgJqmLNz

commit2a3b4e96279aa07b6722b03fbd6096220eef3b35 authorClaude <noreply@anthropic.com> dateMon 28 Sep 2026, 17:15 (2 weeks ago) parentddcbfa3
8 files changed+856−16
TMessagesProj/src/main/java/org/telegram/messenger/multigram/HideSearchBar.java+162 TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java+19−16 TMessagesProj/src/main/java/org/telegram/ui/ForkSettingsActivity.java+2 TMessagesProj/src/main/res/values/multigram_strings.xml+2 multigram/hide-search-bar/README.md+273 multigram/tools/README.md+3 multigram/tools/check.sh+6 multigram/tools/hide_search_bar_check.py+389
TMessagesProj/src/main/java/org/telegram/messenger/multigram/HideSearchBar.javanew file+162
@@ -0,0 +1,162 @@
1+package org.telegram.messenger.multigram;
2+
3+import android.os.Bundle;
4+import android.view.View;
5+
6+import androidx.recyclerview.widget.LinearLayoutManager;
7+import androidx.recyclerview.widget.RecyclerView;
8+
9+import org.telegram.messenger.ApplicationLoader;
10+import org.telegram.messenger.FileLog;
11+import org.telegram.messenger.MessagesController;
12+import org.telegram.messenger.R;
13+import org.telegram.ui.Cells.NotificationsCheckCell;
14+import org.telegram.ui.Cells.TextCheckCell;
15+import org.telegram.ui.Components.UItem;
16+import org.telegram.ui.DialogsActivity;
17+
18+import java.util.WeakHashMap;
19+
20+/**
21+ * "Hide chat list search" (Fork Client Settings > Chat list view), the "no search" variant. While it is on, the main
22+ * chat list and the lists opened from it (archive, folders, communities) keep stock's "bar scrolled away" layout at
23+ * all times, minus the 48 dp the bar reserves, and the header search icon, which stock shows whenever the field is
24+ * invisible, stays hidden as well, so the list itself offers no way into search. Search opened another way (the
25+ * Downloads item, tg://search links, the topics column's search icon) still shows the field in the header, and
26+ * pickers and hashtag search screens keep the stock bar.
27+ *
28+ * Each chat list latches the setting at first use and changes it only in onResume (refresh), so a list never changes
29+ * shape while it is on screen. UI thread only. MultiGram strings are read with Context.getString.
30+ */
31+public final class HideSearchBar {
32+
33+ /** mainconfig key, next to Forkgram's own Chat list rows. Default false. */
34+ public static final String KEY = "multigramHideSearchBar";
35+ /** Row id in ForkSettingsActivity: > 0 (its in-screen search skips ids <= 0), clear of Forkgram's 1-100. */
36+ public static final int ROW_ID = 9101;
37+
38+ private static final class Latch {
39+ final boolean inScope; // fixed at first use: a picker may reset its delegate later
40+ boolean hidden;
41+ Latch(boolean inScope) { this.inScope = inScope; }
42+ }
43+
44+ private static final WeakHashMap<DialogsActivity, Latch> LATCHES = new WeakHashMap<>();
45+
46+ private HideSearchBar() {
47+ }
48+
49+ /** True while this chat list lays out without the bar and without the header search icon. */
50+ public static boolean hides(DialogsActivity f) {
51+ return latch(f).hidden;
52+ }
53+
54+ /** The bar's rest height in dp: the stock value, or 0 while hidden. Always called inside the original dp(...). */
55+ public static int restHeight(DialogsActivity f, int stock) {
56+ return hides(f) ? 0 : stock;
57+ }
58+
59+ /** The bar's alpha from scrolling: the stock value, or 0 while hidden (the field then shows only in search mode). */
60+ public static float restAlpha(DialogsActivity f, float stock) {
61+ return hides(f) ? 0f : stock;
62+ }
63+
64+ /**
65+ * Right after super.onResume(). Re-reads the setting. When this list's latched value changes, it re-anchors each
66+ * page's first visible chat relative to the list padding (stock's own re-anchor in DialogsRecyclerView.onMeasure
67+ * is skipped after onResume's notifyDataSetChanged), switches the latch, and returns true so the caller
68+ * re-derives its header. Returns false on the first call and whenever nothing changed.
69+ */
70+ public static boolean refresh(DialogsActivity f, DialogsActivity.ViewPage[] pages) {
71+ Latch l = LATCHES.get(f);
72+ if (l == null) {
73+ latch(f);
74+ return false;
75+ }
76+ boolean hide = l.inScope && isEnabled();
77+ if (hide == l.hidden) {
78+ return false;
79+ }
80+ if (pages != null) {
81+ for (DialogsActivity.ViewPage page : pages) {
82+ if (page != null) {
83+ keepListOffset(page.listView);
84+ }
85+ }
86+ }
87+ l.hidden = hide;
88+ return f.getFragmentView() != null;
89+ }
90+
91+ /** Same as stock's onMeasure re-anchor: keep the first visible chat's offset from the padding across the padding change. */
92+ static void keepListOffset(RecyclerView list) {
93+ if (list == null || !(list.getLayoutManager() instanceof LinearLayoutManager)) {
94+ return;
95+ }
96+ LinearLayoutManager lm = (LinearLayoutManager) list.getLayoutManager();
97+ if (lm.hasPendingScrollPosition()) {
98+ return;
99+ }
100+ int pos = lm.findFirstVisibleItemPosition();
101+ if (pos == RecyclerView.NO_POSITION) {
102+ return;
103+ }
104+ View first = lm.findViewByPosition(pos);
105+ if (first != null) {
106+ lm.scrollToPositionWithOffset(pos, first.getTop() - list.getPaddingTop());
107+ }
108+ }
109+
110+ /** The row, built like Forkgram's Chat list rows. */
111+ public static UItem settingsRow() {
112+ return UItem.asButtonCheck(ROW_ID, str(R.string.MultiGramHideSearchBar), str(R.string.MultiGramHideSearchBarInfo))
113+ .setChecked(isEnabled()).setMultiline(true);
114+ }
115+
116+ /** Same as Forkgram's private toggle(): flip, putBoolean + commit, check the cell. False for every other row. */
117+ public static boolean onSettingsClick(UItem item, View view) {
118+ if (item == null || item.id != ROW_ID) {
119+ return false;
120+ }
121+ final boolean value = !item.checked;
122+ item.checked = value;
123+ MessagesController.getGlobalMainSettings().edit().putBoolean(KEY, value).commit();
124+ if (view instanceof TextCheckCell) {
125+ ((TextCheckCell) view).setChecked(value);
126+ } else if (view instanceof NotificationsCheckCell) {
127+ ((NotificationsCheckCell) view).setChecked(value);
128+ }
129+ return true;
130+ }
131+
132+ private static Latch latch(DialogsActivity f) {
133+ Latch l = LATCHES.get(f);
134+ if (l == null) {
135+ l = new Latch(inScope(f));
136+ l.hidden = l.inScope && isEnabled();
137+ LATCHES.put(f, l);
138+ }
139+ return l;
140+ }
141+
142+ /** Main list, its folders, archive, communities, BackButtonMenu's plain list. Not pickers, not search-only screens. */
143+ static boolean inScope(DialogsActivity f) {
144+ Bundle a = f.getArguments();
145+ return f.getType() == DialogsActivity.DIALOGS_TYPE_DEFAULT && f.isMainDialogList()
146+ && (a == null || !a.getBoolean("onlySelect", false));
147+ // To keep community lists stock, add: && !f.isCommunity()
148+ }
149+
150+ static boolean isEnabled() {
151+ try {
152+ return MessagesController.getGlobalMainSettings().getBoolean(KEY, false);
153+ } catch (Throwable e) {
154+ FileLog.e(e);
155+ return false;
156+ }
157+ }
158+
159+ private static String str(int res) {
160+ return ApplicationLoader.applicationContext.getString(res);
161+ }
162+}
TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java+19−16
@@ -879,7 +879,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
879879 h += storiesHeight * (1f - searchAnimationProgress) * (1f - rightSlidingProgress) * (1f - progressToActionMode);
880880 }
881881 h += storiesOverscroll;
882− h += dp(SEARCH_FIELD_HEIGHT) * (1f - progressToActionMode) * (1f - searchAnimationProgress) * (1f - rightSlidingProgress);
882+ h += dp(org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)) * (1f - progressToActionMode) * (1f - searchAnimationProgress) * (1f - rightSlidingProgress); // MultiGram: no header band behind a hidden bar
883883
884884 return (int) h;
885885 }
@@ -1057,8 +1057,8 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
10571057 tabsYOffset = 0;
10581058 storiesYOffset = 0;
10591059 tabsYOffset -= Math.min(
1060− dp(hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + dp(SEARCH_FIELD_HEIGHT) + scrollYOffset,
1061− progressToActionMode * (dp(hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + dp(SEARCH_FIELD_HEIGHT))
1060+ dp(hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + dp(org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)) + scrollYOffset, // MultiGram: action mode lifts the tabs by the visible header only
1061+ progressToActionMode * (dp(hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + dp(org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT))) // MultiGram: same, for the full lift
10621062 );
10631063 storiesYOffset = tabsYOffset;
10641064 if ((rightSlidingDialogContainer != null && rightSlidingDialogContainer.hasFragment())) {
@@ -1084,14 +1084,14 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
10841084 if (hasStories) {
10851085 addH += dp(DialogStoriesCell.HEIGHT_IN_DP);
10861086 }
1087− addH += dp(SEARCH_FIELD_HEIGHT);
1087+ addH += dp(org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)); // MultiGram: the topics column moves the list up only by the header it covers
10881088 addH *= rightSlidingDialogContainer.openedProgress;
10891089
10901090 viewPages[0].setTranslationY(rightFragmentOffset - addH);
10911091 } else {
10921092 if (fragmentSearchField != null) {
10931093 fragmentSearchField.setTranslationY(lerp(
1094− scrollYOffset + tabsYOffset + storiesOverscroll - dp(4),
1094+ scrollYOffset + tabsYOffset + storiesOverscroll - dp(4) - dp(SEARCH_FIELD_HEIGHT - org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)), // MultiGram: a hidden bar fades in where a scrolled-away stock bar does
10951095 -dp(SEARCH_FIELD_HEIGHT + (hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0)),
10961096 searchAnimationProgress
10971097 ));
@@ -1183,7 +1183,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
11831183 if (hasStories) {
11841184 h += dp(DialogStoriesCell.HEIGHT_IN_DP);
11851185 }
1186− h += dp(SEARCH_FIELD_HEIGHT);
1186+ h += dp(org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)); // MultiGram: page measured taller by that same amount
11871187 }
11881188 h += actionModeAdditionalHeight;
11891189 if (actionBarColorAnimator == null) {
@@ -2078,7 +2078,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
20782078 t += dp(DialogStoriesCell.HEIGHT_IN_DP);
20792079 }
20802080 if (!actionModeFullyShowed) {
2081− t += dp(SEARCH_FIELD_HEIGHT);
2081+ t += dp(org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)); // MultiGram: the first chat starts right under the header
20822082 }
20832083 additionalPadding = 0;
20842084
@@ -2366,7 +2366,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
23662366 offset += dp(DialogStoriesCell.HEIGHT_IN_DP);
23672367 }
23682368 if (backward) {
2369− offset += dp(SEARCH_FIELD_HEIGHT);
2369+ offset += dp(org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)); // MultiGram: closing the topics column gives back only the header it took
23702370 // offset += canShowFilterTabsView ? dp(50) : 0;
23712371 }
23722372 if (p >= 0) {
@@ -4224,13 +4224,13 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
42244224 }
42254225 int canScrollDy = -(view.getTop() - pTop) + viewsH;
42264226 if (!rightSlidingDialogContainer.hasFragment() && !(actionBar != null && actionBar.isActionModeShowed())) {
4227− canScrollDy -= dp(SEARCH_FIELD_HEIGHT);
4227+ canScrollDy -= dp(org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)); // MultiGram: a fling stops at the first chat, not past a hidden bar
42284228 }
42294229 if (hasStories && (viewPage.scroller.isRunning() || dialogStoriesCell.isExpanded()) && !rightSlidingDialogContainer.hasFragment() && !fixScrollYAfterArchiveOpened) {
42304230 canScrollDy += dp(DialogStoriesCell.HEIGHT_IN_DP);
42314231 }
42324232 if ((viewPage.scroller.isRunning() || dialogStoriesCell.isExpanded()) && !rightSlidingDialogContainer.hasFragment() && !fixScrollYAfterArchiveOpened && !(actionBar != null && actionBar.isActionModeShowed())) {
4233− canScrollDy += dp(SEARCH_FIELD_HEIGHT);
4233+ canScrollDy += dp(org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)); // MultiGram: pairs with the line above
42344234 }
42354235 int positiveDy = Math.abs(dy);
42364236 if (canScrollDy < positiveDy) {
@@ -5742,6 +5742,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
57425742 }
57435743
57445744 private int getMaxScrollYOffset() {
5745+ if (org.telegram.messenger.multigram.HideSearchBar.hides(this)) return getMaxScrollYOffsetWithoutSearch(); // MultiGram: the header collapses by the stories only
57455746 if (hasStories) {
57465747 return dp(DialogStoriesCell.HEIGHT_IN_DP) + dp(SEARCH_FIELD_HEIGHT);
57475748 } else {
@@ -6583,6 +6584,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
65836584 searchTabsHeight * searchAnimationProgress + tabsYOffset;
65846585 }
65856586 totalOffset += storiesOverscroll;
6587+ if (org.telegram.messenger.multigram.HideSearchBar.hides(this)) totalOffset -= dp(SEARCH_FIELD_HEIGHT) * (1f - searchAnimationProgress); // MultiGram: tabs and top panels take the bar's place at rest (their layout top in ContentView.onLayout still counts it)
65866588
65876589 float searchVisibility = 0;
65886590 if (fragmentSearchField != null && fragmentSearchField.getVisibility() == View.VISIBLE) {
@@ -7085,6 +7087,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
70857087 @Override
70867088 public void onResume() {
70877089 super.onResume();
7090+ if (org.telegram.messenger.multigram.HideSearchBar.refresh(this, viewPages)) { setScrollY(Math.max(scrollYOffset, -getMaxScrollYOffset())); invalidateScrollY = true; checkUi_searchFieldVisibility(); checkUi_menuItems(); } // MultiGram: "Hide chat list search" changed while this list was paused: re-derive its header
70887091 if (dialogStoriesCell != null) {
70897092 dialogStoriesCell.onResume();
70907093 }
@@ -9108,7 +9111,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
91089111 viewPages[i].listView.cancelClickRunnables(true);
91099112 }
91109113 }
9111− translateListHeight = Math.max(0, dp((hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + SEARCH_FIELD_HEIGHT) + scrollYOffset);
9114+ translateListHeight = Math.max(0, dp((hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)) + scrollYOffset); // MultiGram: leaving action mode gives back only the header it took
91129115 float finalTranslateListHeight = translateListHeight;
91139116 actionBarColorAnimator = ValueAnimator.ofFloat(progressToActionMode, 0);
91149117 actionBarColorAnimator.addUpdateListener(valueAnimator -> {
@@ -9134,7 +9137,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
91349137 invalidateScrollY = true;
91359138 fixScrollYAfterArchiveOpened = true;
91369139 fragmentView.invalidate();
9137− scrollAdditionalOffset = -(dp((hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + SEARCH_FIELD_HEIGHT) - finalTranslateListHeight);
9140+ scrollAdditionalOffset = -(dp((hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)) - finalTranslateListHeight); // MultiGram: pairs with translateListHeight
91389141 viewPages[0].setTranslationY(0);
91399142 for (int i = 0; i < viewPages.length; i++) {
91409143 if (viewPages[i] != null) {
@@ -10237,7 +10240,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
1023710240 viewPages[i].listView.cancelClickRunnables(true);
1023810241 }
1023910242 }
10240− translateListHeight = Math.max(0, dp((hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + SEARCH_FIELD_HEIGHT) + scrollYOffset);
10243+ translateListHeight = Math.max(0, dp((hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)) + scrollYOffset); // MultiGram: action mode lifts the list by the visible header only
1024110244 if (translateListHeight != 0) {
1024210245 actionModeAdditionalHeight = (int) translateListHeight;
1024310246 fragmentView.requestLayout();
@@ -10264,7 +10267,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
1026410267 actionBarColorAnimator = null;
1026510268 actionModeAdditionalHeight = 0;
1026610269 actionModeFullyShowed = true;
10267− scrollAdditionalOffset = dp((hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + SEARCH_FIELD_HEIGHT) - finalTranslateListHeight;
10270+ scrollAdditionalOffset = dp((hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + org.telegram.messenger.multigram.HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)) - finalTranslateListHeight; // MultiGram: pairs with translateListHeight
1026810271 viewPages[0].setTranslationY(0);
1026910272 for (int i = 0; i < viewPages.length; i++) {
1027010273 if (viewPages[i] != null) {
@@ -14240,7 +14243,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
1424014243 }
1424114244
1424214245 final int maxScrollWithoutSearch = getMaxScrollYOffsetWithoutSearch();
14243− final float alphaByScrollOffset = 1f - MathUtils.clamp((-scrollYOffset - maxScrollWithoutSearch) / dp(SEARCH_FIELD_HEIGHT), 0, 1);
14246+ final float alphaByScrollOffset = org.telegram.messenger.multigram.HideSearchBar.restAlpha(this, 1f - MathUtils.clamp((-scrollYOffset - maxScrollWithoutSearch) / dp(SEARCH_FIELD_HEIGHT), 0, 1)); // MultiGram: no bar at rest; the field shows only while searching
1424414247
1424514248 final float actionModeVisible = Math.max(progressToActionMode, animatorActionModeVisible.getFloatValue());
1424614249 final float searchFieldVisible = animatorSearchVisible.getFloatValue();
@@ -14325,7 +14328,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
1432514328 }
1432614329
1432714330 private void checkUi_itemSearchVisibility() {
14328− final float factor0 = isSupportSearch() ? 1 : 0;
14331+ final float factor0 = isSupportSearch() && !org.telegram.messenger.multigram.HideSearchBar.hides(this) ? 1 : 0; // MultiGram: "no search": no header search icon either
1432914332 final float factor1 = animatorSearchButtonVisible.getFloatValue();
1433014333 final float factor2 = 1f - getRightSlidingProgress();
1433114334 final float factor3 = 1f - animatorDoneButtonVisible.getFloatValue();
TMessagesProj/src/main/java/org/telegram/ui/ForkSettingsActivity.java+2
@@ -587,6 +587,7 @@ public class ForkSettingsActivity extends BaseFragment {
587587 .setChecked(pref("disableThumbsInDialogList", false)).setMultiline(true));
588588 items.add(UItem.asButtonCheck(ID_DISABLE_GLOBAL_SEARCH, LocaleController.getString(R.string.DisableGlobalSearch), LocaleController.getString(R.string.DisableGlobalSearchInfo))
589589 .setChecked(pref("disableGlobalSearch", false)).setMultiline(true));
590+ items.add(org.telegram.messenger.multigram.HideSearchBar.settingsRow()); // MultiGram: "Hide chat list search" row, right after Disable Global Search
590591 items.add(UItem.asButtonCheck(ID_HIDE_CONTACTS_IN_DIALOGS, LocaleController.getString(R.string.HideContactsInDialogs), LocaleController.getString(R.string.HideContactsInDialogsInfo))
591592 .setChecked(pref("hideContactsInDialogs", false)).setMultiline(true));
592593 items.add(UItem.asButtonCheck(ID_ENABLE_LAST_SEEN_DOTS, LocaleController.getString(R.string.EnableLastSeenDots), LocaleController.getString(R.string.EnableLastSeenDotsInfo))
@@ -759,6 +760,7 @@ public class ForkSettingsActivity extends BaseFragment {
759760
760761 private void onClick(UItem item, View view, int position, float x, float y) {
761762 final int id = item.id;
763+ if (org.telegram.messenger.multigram.HideSearchBar.onSettingsClick(item, view)) return; // MultiGram: "Hide chat list search" toggle
762764
763765 if (id == ID_HIDE_SENSITIVE_DATA) {
764766 toggle("hideSensitiveData", item, view);
TMessagesProj/src/main/res/values/multigram_strings.xml+2
@@ -9,4 +9,6 @@
99 <string name="MultiGramStyleShuffled">New style applied.</string>
1010 <string name="MultiGramUndoFailed">Couldn\'t bring back your previous style.</string>
1111 <string name="MultiGramStyleStaysOnDevice">Your generated style stays on this device, so it can\'t be shared or turned into a cloud theme. Pick another color theme first.</string>
12+ <string name="MultiGramHideSearchBar">Hide chat list search</string>
13+ <string name="MultiGramHideSearchBarInfo">Remove the search bar and the search icon from the chat list.</string>
1214</resources>
multigram/hide-search-bar/README.mdnew file+273
@@ -0,0 +1,273 @@
1+# Hide chat list search
2+
3+Telegram 12 shows a search field (`FragmentSearchField`, 48 dp, `DialogsActivity.SEARCH_FIELD_HEIGHT`) under the
4+chat list header, and a search icon in the header once that field has scrolled away. "Hide chat list search", a
5+toggle in Fork Client Settings > Chat list view right after Forkgram's Disable Global Search (off by default),
6+removes both: the chat list lays out as stock does once the bar has scrolled away, minus the 48 dp the bar reserves,
7+so the first chat sits right under the header (or the folder tabs), and the header search icon never shows. The
8+chat list itself then offers no way into search. Forkgram's Disable Global Search is unrelated: it only drops server
9+results from search.
10+
11+This build is the **"no search" variant**, the one the owner chose. The other design, the "search icon" variant,
12+hid only the bar and let the stock header icon open search (see "Considered and not done"). The two differ by one
13+hook (21), the two strings and the check.
14+
15+The header icon needs its own hook because stock shows it whenever the bar is invisible, not only in search mode:
16+`checkUi_searchFieldVisibility` ends with `animatorSearchButtonVisible.setValue(alpha <= 0.01f, true)`, which drives
17+`checkUi_itemSearchVisibility`, and `FragmentFloatingButton.setAnimatedVisibility` makes the icon VISIBLE whenever
18+its factor is above 0. With the bar's alpha from scrolling held at 0 (hook 18), stock would swap the icon in at
19+rest; hook 21 zeroes the icon's factor instead.
20+
21+With the option off every hook returns the stock value it replaced, so the chat list is exactly stock.
22+
23+Code: `org.telegram.messenger.multigram.HideSearchBar`. Check: `multigram/tools/hide_search_bar_check.py`, which
24+`multigram/tools/check.sh` runs.
25+
26+## Scope
27+
28+In scope (no bar and no header search icon while the option is on): every `DialogsActivity` with the default
29+dialogs type, no `onlySelect` argument, no delegate and no search string (`isMainDialogList()`). That is the main
30+chat list with its folder tabs, the Archive, community lists, and the plain list `BackButtonMenu` opens.
31+
32+Out of scope (stock bar and stock search always): dialog pickers (forward, share from another app, bot share, Save
33+to Gallery exceptions and every other `onlySelect` or non-default `dialogsType` screen; every `new DialogsActivity(`
34+call site that picks a chat sets one of the two) and the `#hashtag` / link search screens, which set a search
35+string.
36+
37+The scope of a list is decided once, at its first use, because a picker may reset its delegate later
38+(`isMainDialogList()` would then turn true). To keep community lists stock, add `&& !f.isCommunity()` to
39+`HideSearchBar.inScope` (one line; the comment there says so).
40+
41+## What still opens search
42+
43+The option removes the chat list's own ways into search: the bar and the header icon. It does not block search
44+itself. These entries are left stock on purpose, and each still opens search in the chat list, with the field
45+appearing in the header while searching:
46+
47+- **The Downloads item** in the main list's header. It shows only while files are downloading or there are
48+ downloads you have not looked at yet, and it opens search on the Downloads tab.
49+- **`tg://search?query=...` links.** They open search in the chat list with the query filled in.
50+- **The search icon of the forum topics column** (a forum opened beside the chat list). It calls `performClick()`
51+ on the list's header icon, and `performClick()` runs the click listener even while the icon is hidden. That
52+ search is scoped to the forum.
53+
54+Also left stock: the music player's search by performer (a tap on the performer's name). As in stock, it does
55+nothing when the account has 10 chats or fewer. When the top screen is an in-scope list, that is an open Archive, an
56+open community list or the plain list `BackButtonMenu` opens, it opens search in that list, with the field in its
57+header. Otherwise, Chats included (there the top screen is the main tabs screen, not the list), it opens its own
58+search screen: a list with a search string, so out of scope and fully stock, bar included. Pickers and `#hashtag`
59+screens keep their stock search bar.
60+
61+Why they stay: each is something the user asked for (a download, a link, a tap on a search icon), not the chat list
62+offering search on its own. Blocking them needs about 4 more hooks in busier code (`showSearch`, `canToggleSearch`
63+while not expanded, the Downloads item, `TopicsFragment`), and blocking `showSearch` alone would leave
64+`searching = true` with no search screen. The check fails if any of the three entries above stops opening search,
65+so an update that changes one gets this section reviewed.
66+
67+## What is lost with the option on
68+
69+- **The icon's long-press shortcut to Saved Messages.** Saved Messages itself is unaffected.
70+- **The chat list route into Forkgram's hidden-account unlock** (stealth mode, no passcode: tap search, type the
71+ account's unlock code). The unlock itself is untouched: it runs in every `DialogsActivity`'s search text watcher
72+ (`HiddenAccountHelper.tryUnlockFromSearch`, gated only by `shouldUseSearchUnlock()`), so it still works from any
73+ search listed in "What still opens search", from any picker's search bar and from any `#hashtag` screen. Anyone
74+ who uses hidden accounts loses the usual way in and has to use one of those.
75+- **The promise in Hide the "All Chats" tab's info text** that chats outside folders "stay reachable through search
76+ and the archive": with both options on, the chat list offers no search to reach them.
77+
78+## Lifecycle
79+
80+| When | What happens |
81+|---|---|
82+| First use of a list (its `createView`) | The list latches the setting (and its scope) in a `WeakHashMap` keyed by the fragment. Every hook reads the latch, so a list never changes shape while it is on screen. |
83+| `onResume`, right after `super.onResume()` | `HideSearchBar.refresh` re-reads the setting. Nothing happens unless this list's latched value changes. When it does, it first re-anchors each page's first visible chat relative to the list padding, then switches the latch; the hook then clamps the header scroll to the new maximum, asks the next draw to derive it again from the list (`invalidateScrollY`), and re-derives the field's visibility and the header icons. |
84+
85+**Why refresh re-anchors the lists itself.** Stock `DialogsRecyclerView.onMeasure` keeps the first chat's offset
86+from the padding when the padding changes, but not on this path: `onResume` calls `notifyDataSetChanged` on every
87+page, after which `findViewHolderForAdapterPosition` returns null and that re-anchor is skipped, and
88+`LinearLayoutManager` then anchors on the old child's absolute top. Turning the option on with a hidden Archive
89+would then show 48 dp of the Archive row under the header; turning it off would leave the list 48 dp scrolled.
90+`keepListOffset` does what the stock re-anchor does (`scrollToPositionWithOffset(pos, top - paddingTop)`) before the
91+notify, so the layout after it uses that pending position against the new padding.
92+
93+**How the header icon follows a toggle.** Hook 13 ends with `checkUi_searchFieldVisibility(); checkUi_menuItems();`.
94+`checkUi_menuItems()` calls `checkUi_itemSearchVisibility()`, whose `factor0` (hook 21) reads the latch `refresh` has
95+just switched, and `setAnimatedVisibility` applies the result at once. Turning the option on hides the icon at
96+once. Turning it off brings the bar back at once, and the icon fades out over 350 ms: while the option was on the
97+bar's alpha was 0, so `animatorSearchButtonVisible` stands at 1, and `checkUi_searchFieldVisibility` now animates it
98+to 0. That is stock's own swap when the bar scrolls back in. Nothing else in the list makes the icon visible:
99+every other path (`checkUi_menuItems()`, the animators' `onFactorChanged`) goes through
100+`checkUi_itemSearchVisibility()` and so reads the same latch.
101+
102+A change applies the moment Chats is shown again: back, swipe back, or the Chats tab when Fork settings was opened
103+from the Settings tab; each of them resumes the list before it is drawn. On a tablet, a list that stays resumed next
104+to settings applies the change at its next resume.
105+
106+## Preferences
107+
108+mainconfig (`MessagesController.getGlobalMainSettings()`, the file Forkgram's own Chat list rows use):
109+
110+| Key | Type | Default | Meaning |
111+|---|---|---|---|
112+| `multigramHideSearchBar` | boolean | false | the option |
113+
114+Written only by the row, with `commit` like Forkgram's `toggle()`. Forkgram's settings export (`SettingsBackup`)
115+exports and imports mainconfig as a whole, so the key comes along; the Android backup agent does not back up
116+mainconfig. The row's id is 9101: above 0, so Fork settings' in-screen search finds it, and clear of Forkgram's
117+row ids (1-100). The key, the class and the strings' resource names keep the `HideSearchBar` name of the first
118+design; only the row's text changed.
119+
120+## Hooks in upstream files
121+
122+Every hook is one line marked `// MultiGram:`. The changed stock lines swap only the stock token inside the original
123+`dp(...)` for `HideSearchBar.restHeight(DialogsActivity.this, SEARCH_FIELD_HEIGHT)`, which returns it unchanged
124+while the option is off (and `dp(0) == 0` while it is on). Line numbers are those of `4780eb095b`.
125+
126+| # | File, place | Hook |
127+|---|---|---|
128+| 1 | `DialogsActivity` L881, `ContentView.getActionBarFullHeight` | header band height: `restHeight` (header band, list clip, shadow, right-pane top and swipe region follow it) |
129+| 2 | L1059, `tabsYOffset` | action mode lifts the tabs by the visible header only: `restHeight` |
130+| 3 | L1060 | same, for the full lift: `restHeight` |
131+| 4 | L1086, right pane `addH` | the topics column moves the list up only by the header it covers: `restHeight` |
132+| 5 | L1093, start term of the field's search slide | `- dp(SEARCH_FIELD_HEIGHT - restHeight(...))`: a hidden bar fades in where a scrolled-away stock bar does (end term L1094 stock) |
133+| 6 | L1185, `ContentView.onMeasure` | the page is measured taller by the same amount: `restHeight` |
134+| 7 | L2080, `DialogsRecyclerView.onMeasure` | the list's top padding: `restHeight` |
135+| 8 | L2368, `setAnimationSupportView`, `backward` branch | closing the topics column gives back only the header it took: `restHeight` |
136+| 9 | L4226, fling stop | a fling stops at the first chat, not past a hidden bar: `restHeight` |
137+| 10 | L4232 | pairs with 9: `restHeight` |
138+| 11 | after L5743, first statement of `getMaxScrollYOffset` | `if (hides(this)) return getMaxScrollYOffsetWithoutSearch();` the header collapses by the stories only |
139+| 12 | after L6584 `totalOffset += storiesOverscroll;` in `updateContextViewPosition` | `if (hides(this)) totalOffset -= dp(SEARCH_FIELD_HEIGHT) * (1f - searchAnimationProgress);` tabs and top panels take the bar's place at rest (their layout top, L1324 in `ContentView.onLayout`, still counts it) |
140+| 13 | after L7086 `super.onResume();` | `if (refresh(this, viewPages)) { setScrollY(...); invalidateScrollY = true; checkUi_searchFieldVisibility(); checkUi_menuItems(); }` |
141+| 14 | L9110, `hideActionMode` | leaving action mode gives back only the header it took: `restHeight` |
142+| 15 | L9136 | pairs with 14: `restHeight` |
143+| 16 | L10239, `showOrUpdateActionMode` | action mode lifts the list by the visible header only: `restHeight` |
144+| 17 | L10266 | pairs with 16: `restHeight` |
145+| 18 | L14236, `checkUi_searchFieldVisibility` | `alphaByScrollOffset = restAlpha(this, <stock>)`: no bar at rest, so the field shows only while searching |
146+| 19 | `ForkSettingsActivity` after L578 (Disable Global Search) | `items.add(HideSearchBar.settingsRow());` |
147+| 20 | `ForkSettingsActivity.onClick` after `final int id = item.id;` | `if (HideSearchBar.onSettingsClick(item, view)) return;` |
148+| 21 | `DialogsActivity` L14321, first line of `checkUi_itemSearchVisibility` | `factor0` also needs `!hides(this)`: no header search icon either ("no search" variant) |
149+
150+Hook 21 in full:
151+
152+```java
153+ final float factor0 = isSupportSearch() && !org.telegram.messenger.multigram.HideSearchBar.hides(this) ? 1 : 0; // MultiGram: "no search": no header search icon either
154+```
155+
156+The same stock text, `final float factor0 = isSupportSearch() ? 1 : 0;`, is also at L14241 in
157+`checkUi_searchFieldVisibility`, where it decides whether the field shows at all. That copy stays stock. The check
158+requires it exactly once in the file, as the only `factor0` of that method, and hook 21 as the only `factor0` of
159+`checkUi_itemSearchVisibility`, its first line, directly followed by the stock `factor1` line. It counts the stock
160+lines of both methods on code with comments stripped, so a stock line an update comments out does not count.
161+
162+**Deliberately not hooked** (the check keeps each bar-height line on a reviewed list, so an upstream change to any
163+use of the bar height fails the check until it is reviewed):
164+
165+- the `SEARCH_FIELD_HEIGHT = 48` declaration (L300): `javac` inlines it into 4 other classes, and it also sets how
166+ far the field travels in search;
167+- L1324, the layout top of the tabs and top panels: it also anchors the top panel in search mode, so hook 12
168+ compensates instead;
169+- L1094, the end term of the search slide; L1636;
170+- L5707, the stock "bar snap": it cannot run once the maximum scroll equals the maximum without search;
171+- L7251, the top bulletin offset: it behaves as stock does when the bar is scrolled away;
172+- L7635, the search pager entry with stories: 48 dp low during a 200 ms fade;
173+- L14377, the glass capture area: performance only;
174+- the icon's click and long-press listeners, `showSearch`, the Downloads item, `search(query, animated)` and
175+ `TopicsFragment`: the entries in "What still opens search" stay stock on purpose;
176+- `FragmentSearchField.java`.
177+
178+**State trace with the option on** (AB = action bar height, S = dp(81) with stories or 0, sy = `scrollYOffset`):
179+at rest the header band ends at AB+S and the list padding is AB+S (+ tabs and panels), tabs sit at AB+S+sy, and
180+the field and the header search icon are both GONE. Scrolling clamps sy to [-S, 0], so without stories the header
181+is fixed. A fling stops at the first chat and a hidden Archive stays hidden. When search is opened another way, the
182+field fades in where a scrolled-away stock bar does, and at `searchAnimationProgress == 1` everything matches stock,
183+because every rest term is multiplied by (1-p) or is a clamp (stock hides the icon in search mode too). Action mode
184+moves the list and tabs by S+sy (0 without stories) with no jump on either edge (hooks 14-17 pair up). The topics
185+column moves up by S times its progress and is measured with the same amount (hooks 4, 6, 8). Stories appearing or
186+disappearing stay consistent, because `getMaxScrollYOffset()` is S.
187+
188+## Considered and not done
189+
190+| Idea | Why not |
191+|---|---|
192+| The "search icon" variant: hide only the bar, and let the stock header icon (which stock shows whenever the bar is invisible) open search | The owner chose "no search". To go back: put hook 21's line back to stock; in the check, remove `H[21]` and its `LAYOUT` entry, make the stock `factor0` guard expect two copies in the file (still one, as code, in `checkUi_searchFieldVisibility`), and flip the `checkUi_itemSearchVisibility` placement check to require the stock `factor0` line and no `HideSearchBar`; reword the two strings so the title names only the bar and the info text points to the header icon; update this README. |
193+| A strict version that blocks every way into search | About 4 more hooks in busier code, and blocking `showSearch` alone leaves `searching = true` with no search screen. See "What still opens search". |
194+| Restart required | Not needed: each list latches the setting and switches it in `onResume`, before it is drawn. |
195+| An `attach` hook in `createView` to fix the scope | More upstream text in the busiest method; the first latch fixes the scope instead. |
196+| Hooking L1324, the constant or `FragmentSearchField` | L1324 also anchors the top panel in search mode; the constant is inlined into other classes and sets the search slide; the field class is shared with other screens. |
197+| Hooking L7251, L7635, L14377 | Cosmetic or performance only (see "Known limitations"). |
198+| An entry in the Settings screen's global search index (ProfileActivity) | Forkgram does not index its Chat list rows either (for example Hide the All Chats tab); the in-screen search of Fork settings finds the row. |
199+| Applying the change at once to a list visible next to settings on tablets | Needs a notification and a live re-layout of a visible list; it applies at the list's next resume. |
200+
201+Open owner decision: whether community lists are in scope (default yes; see "Scope").
202+
203+## Device test checklist
204+
205+CI only compiles and runs the checks; everything below needs a device.
206+
207+1. **Option off**, on a fresh install and again after turning it off: chat list, bar at rest and hiding on scroll,
208+ the header search icon appearing once the bar has scrolled away, search from the bar and from the icon, the
209+ icon's long-press opening Saved Messages, stories collapsing, action mode, Archive, topics column. Compare side
210+ by side with a stock Forkgram build.
211+2. **Turn it on**, then return to Chats in each of three ways: back, swipe back, and the Chats tab when settings was
212+ opened from the Settings tab.
213+ - Expected: no bar, no gap and no search icon in the header; the first chat sits directly under the header or
214+ folder tabs.
215+ - With a hidden Archive: no Archive strip shows.
216+ - Turn it off and return: the bar is back at rest, with the first chat under it; the search icon may fade out
217+ briefly, as it does in stock when the bar scrolls back in.
218+ - Repeat while scrolled halfway down the list.
219+3. **No search from the list** (option on):
220+ - No search icon at rest, while scrolled, at the bottom of a long list, after a fling, in every folder, in the
221+ Archive and in a community list.
222+ - Nothing in the header or the list opens search.
223+ - After leaving action mode, after closing the topics column and after coming back from a chat: still no icon.
224+4. **No stories**:
225+ - The header never moves.
226+ - The shadow appears when scrolled and goes at the top.
227+ - A fling to the top with a hidden Archive stops at the first chat.
228+ - Pull to reveal or open the Archive, with Open Archive on Pulldown both on and off.
229+5. **With stories**:
230+ - Collapse, expand, half-way snap, fling.
231+ - Collapsed story avatars sit next to the remaining header items, with no gap where the icon was, and are
232+ clipped before those items.
233+ - Stories appearing or disappearing causes no jump.
234+6. **Folder tabs** (also with Hide the All Chats tab and each folder tab style): switching folders by swiping; top
235+ panels (proxy, requests, suggestions) sit right under the header and move into place when search opens from
236+ Downloads or a link.
237+7. **Action mode** with and without stories, list at the top and scrolled: no jump entering or leaving; pin and
238+ archive several chats.
239+8. **Forum topics column**: it opens under the action bar and closes without a jump; its search icon still opens
240+ search scoped to the forum, the field appears in the header, and closing search brings back the list with no
241+ bar and no icon.
242+9. **Archive**, opened by pull and by the row: no bar and no icon; Hide Stories in Archive on and off. With the
243+ Archive open, the music player's search by performer shows the field in the Archive's header, with the Archive
244+ chip.
245+10. **A community list**: no bar and no icon. With the community list open, the music player's search by performer
246+ shows the field in the community list's header. (Stock instead if the owner excludes communities.)
247+11. **Pickers keep the stock bar and search**: forward, share from another app, bot share, Save to Gallery
248+ exceptions. A #hashtag screen keeps its stock search layout.
249+12. **What still opens search** (option on). Each time the field fades in in the header row, results show, and
250+ closing search (x or back) returns to the list with no bar and no icon:
251+ - the Downloads header item, while a file downloads (lands on the Downloads tab);
252+ - a `tg://search?query=` link, with Chats open and with the app in the background;
253+ - the forum topics column's search icon (item 8);
254+ - the music player's search by performer, with more than 10 chats: from Chats it opens its own search screen
255+ (stock layout, with its bar); from an open Archive or community list, see items 9 and 10; with 10 chats or
256+ fewer it does nothing, as in stock;
257+ - hidden-account unlock (stealth mode, no passcode): typing the code in one of these searches unlocks the
258+ account.
259+13. **Screens and themes**: tablet or unfolded device, landscape, split screen, liquid glass on and off, dark theme,
260+ a long custom title (it may use the room the icon took).
261+14. **Fork settings**:
262+ - The row "Hide chat list search" sits right after Disable Global Search, and the in-screen settings search
263+ finds it.
264+ - Export settings, turn the option off, import, restart: the option is on again.
265+
266+## Known limitations
267+
268+- Turning the option off: the header search icon fades out over 350 ms when Chats shows again (stock's own swap).
269+- Top bulletins keep stock's 48 dp margin.
270+- With stories, search results slide in from 48 dp below the list top.
271+- The glass capture area is 48 dp taller than needed.
272+- On tablets, a list visible beside settings applies the change at its next resume.
273+- The strings are English only.
multigram/tools/README.md+3
@@ -237,6 +237,9 @@ represented (288-387 entries per range and theme); bubbles 1/2/3/4 colours about
237237 placement, its privacy guards, strings and backup policy (`multigram/random-style/README.md`).
238238 `style_knobs_check.py`, when the tree has it: the style knobs' hooks and their placement, the settings
239239 they may write and their safe ranges (`multigram/style-knobs/README.md`).
240+ `hide_search_bar_check.py`, when the tree has it: the "Hide chat list search" option's hooks (bar
241+ and header icon) and their placement, the ways into search it leaves stock, every use of the bar
242+ height in DialogsActivity reviewed, its one setting and row (`multigram/hide-search-bar/README.md`).
2402432. `check_style_table.py`: the format rules above, every record; theme keys must be bundled base themes
241244 of this tree with the right night flag. Self-tests: the parser must recognise every form of the
242245 out-text guard (stock, `|| id > 100`, and the palette fix's exact line) with the rest of Theme.java
multigram/tools/check.sh+6
@@ -23,6 +23,12 @@ if [ -f multigram/tools/style_knobs_check.py ]; then
2323 python3 multigram/tools/style_knobs_check.py
2424fi
2525
26+if [ -f multigram/tools/hide_search_bar_check.py ]; then
27+ # "Hide chat list search": hooks and their placement, every bar-height use reviewed, one owned setting.
28+ echo "== hide search bar: hooks, reviewed bar-height uses and owned setting"
29+ python3 multigram/tools/hide_search_bar_check.py
30+fi
31+
2632if [ -f multigram/rebrand/selftest.py ]; then
2733 # Rebrand toolkit: generate two identities on a scratch copy, check the output, then --clean back to the commit.
2834 echo "== rebrand toolkit: self-test"
multigram/tools/hide_search_bar_check.pynew file+389
@@ -0,0 +1,389 @@
1+#!/usr/bin/env python3
2+"""Check the MultiGram "Hide chat list search" option (multigram/hide-search-bar/README.md) in this source tree.
3+
4+Run from anywhere:
5+
6+ python3 multigram/tools/hide_search_bar_check.py
7+ python3 multigram/tools/hide_search_bar_check.py --base forkgram # also: every line added to the hooked
8+ # upstream files since REV is marked
9+
10+What it checks:
11+ * Every hook in DialogsActivity and ForkSettingsActivity is present exactly once, is one line marked
12+ "MultiGram:", and keeps the stock token inside the original dp(...) (HideSearchBar.restHeight returns it while
13+ the option is off). Every line of those files that names HideSearchBar carries the marker.
14+ * Update guard: every other line of DialogsActivity that uses SEARCH_FIELD_HEIGHT is one of the reviewed stock
15+ lines, with the reviewed count. A new use in a Forkgram or Telegram update (or a stock line a hook replaced
16+ coming back) fails here, so it gets reviewed before it ships.
17+ * Placement: the max-scroll hook opens getMaxScrollYOffset; the tabs hook directly follows the stories overscroll
18+ in updateContextViewPosition, before the tabs are placed; the refresh hook directly follows super.onResume()
19+ (before onResume's notifyDataSetChanged); the alpha hook feeds the field's visibility and the stock search icon
20+ swap in checkUi_searchFieldVisibility, whose own factor0 line stays stock and is its only factor0; the icon hook
21+ (the "no search" variant) is the factor0 line that opens checkUi_itemSearchVisibility, the only other copy of
22+ that stock line (the stock lines of both methods are counted on code with comments stripped);
23+ the entries left stock on purpose (the icon's click listener, which the topics column's search icon calls, the
24+ Downloads item and tg://search links) still open search; the action mode hooks pair up in hideActionMode and
25+ showOrUpdateActionMode; every DialogsActivity hook but the alpha hook sits right next to a stock neighbour
26+ line (the two fling stop hooks and the page height hook also inside their stock if blocks), so moving one
27+ within its method fails; the settings row directly follows Disable Global Search and the click hook opens
28+ onClick.
29+ * HideSearchBar.java: one owned mainconfig key, written in one place and read with default false; a row id clear
30+ of Forkgram's ids; restHeight and restAlpha pass the stock value through; strings through Context.getString;
31+ FragmentSearchField is not touched; only the two hooked upstream files name the class.
32+
33+Standard library only. Exit status 0 when everything matches, 1 otherwise.
34+"""
35+import argparse
36+import os
37+import re
38+import subprocess
39+import sys
40+
41+ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
42+MAIN = "TMessagesProj/src/main/"
43+JAVA = MAIN + "java/org/telegram"
44+MG = JAVA + "/messenger/multigram/"
45+DA = JAVA + "/ui/DialogsActivity.java"
46+FORK = JAVA + "/ui/ForkSettingsActivity.java"
47+FIELD = JAVA + "/ui/Components/FragmentSearchField.java"
48+TOPICS = JAVA + "/ui/TopicsFragment.java"
49+CLS = MG + "HideSearchBar.java"
50+STRINGS = MAIN + "res/values/multigram_strings.xml"
51+SETTINGS_BACKUP = JAVA + "/messenger/forkgram/SettingsBackup.kt"
52+KEY = "multigramHideSearchBar"
53+STRING_NAMES = ["MultiGramHideSearchBar", "MultiGramHideSearchBarInfo"]
54+
55+P = r"org\.telegram\.messenger\.multigram\.HideSearchBar\."
56+M = r" // MultiGram: "
57+RH = P + r"restHeight\(DialogsActivity\.this, SEARCH_FIELD_HEIGHT\)"
58+ST = r"\(hasStories \? DialogStoriesCell\.HEIGHT_IN_DP : 0\)"
59+
60+# Hook bodies (the code of the line, without its indentation and marker), named by the spec's numbers.
61+H = {
62+ 1: r"h \+= dp\(" + RH + r"\) \* \(1f - progressToActionMode\) \* \(1f - searchAnimationProgress\) \* \(1f - rightSlidingProgress\);",
63+ 2: r"dp" + ST + r" \+ dp\(" + RH + r"\) \+ scrollYOffset,",
64+ 3: r"progressToActionMode \* \(dp" + ST + r" \+ dp\(" + RH + r"\)\)",
65+ 4: r"addH \+= dp\(" + RH + r"\);",
66+ 5: r"scrollYOffset \+ tabsYOffset \+ storiesOverscroll - dp\(4\) - dp\(SEARCH_FIELD_HEIGHT - " + RH + r"\),",
67+ 6: r"h \+= dp\(" + RH + r"\);",
68+ 7: r"t \+= dp\(" + RH + r"\);",
69+ 8: r"offset \+= dp\(" + RH + r"\);",
70+ 9: r"canScrollDy -= dp\(" + RH + r"\);",
71+ 10: r"canScrollDy \+= dp\(" + RH + r"\);",
72+ 11: r"if \(" + P + r"hides\(this\)\) return getMaxScrollYOffsetWithoutSearch\(\);",
73+ 12: r"if \(" + P + r"hides\(this\)\) totalOffset -= dp\(SEARCH_FIELD_HEIGHT\) \* \(1f - searchAnimationProgress\);",
74+ 13: r"if \(" + P + r"refresh\(this, viewPages\)\) \{ setScrollY\(Math\.max\(scrollYOffset, -getMaxScrollYOffset\(\)\)\); "
75+ r"invalidateScrollY = true; checkUi_searchFieldVisibility\(\); checkUi_menuItems\(\); \}",
76+ 14: r"translateListHeight = Math\.max\(0, dp\(" + ST + r" \+ " + RH + r"\) \+ scrollYOffset\);",
77+ 15: r"scrollAdditionalOffset = -\(dp\(" + ST + r" \+ " + RH + r"\) - finalTranslateListHeight\);",
78+ 16: r"translateListHeight = Math\.max\(0, dp\(" + ST + r" \+ " + RH + r"\) \+ scrollYOffset\);",
79+ 17: r"scrollAdditionalOffset = dp\(" + ST + r" \+ " + RH + r"\) - finalTranslateListHeight;",
80+ 18: r"final float alphaByScrollOffset = " + P + r"restAlpha\(this, 1f - MathUtils\.clamp\(\(-scrollYOffset - "
81+ r"maxScrollWithoutSearch\) / dp\(SEARCH_FIELD_HEIGHT\), 0, 1\)\);",
82+ 19: r"items\.add\(" + P + r"settingsRow\(\)\);",
83+ 20: r"if \(" + P + r"onSettingsClick\(item, view\)\) return;",
84+ 21: r"final float factor0 = isSupportSearch\(\) && !" + P + r"hides\(this\) \? 1 : 0;",
85+}
86+
87+# (file, hook number, indentation). Each hook is one line ending in a "MultiGram:" comment, present exactly once.
88+LAYOUT = [
89+ (DA, 1, 12), (DA, 2, 16), (DA, 3, 16), (DA, 4, 16), (DA, 5, 24), (DA, 6, 24), (DA, 7, 16), (DA, 8, 24),
90+ (DA, 9, 36), (DA, 10, 36), (DA, 11, 8), (DA, 12, 8), (DA, 13, 8), (DA, 14, 8), (DA, 15, 16), (DA, 16, 12),
91+ (DA, 17, 20), (DA, 18, 8), (DA, 21, 8),
92+ (FORK, 19, 8), (FORK, 20, 8),
93+]
94+HOOKS = [(rel, r"^" + " " * indent + H[n] + M, 1) for rel, n, indent in LAYOUT]
95+UPSTREAM = [DA, FORK]
96+
97+# Update guard, which also covers the replaced stock lines: every DialogsActivity line matching \bSEARCH_FIELD_HEIGHT\b
98+# and lacking the marker must be exactly one of these (stripped text), with exactly these counts.
99+REVIEWED = {
100+ "public static final int SEARCH_FIELD_HEIGHT = 48;": 1,
101+ "-dp(SEARCH_FIELD_HEIGHT + (hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0)),": 1,
102+ "childTop += dp(SEARCH_FIELD_HEIGHT);": 2,
103+ "// childTop -= dp(SEARCH_FIELD_HEIGHT);": 1,
104+ "return -lerp(dp(4), dp(SEARCH_FIELD_HEIGHT), animatorSearchVisible.getFloatValue());": 1,
105+ "int h = dp(SEARCH_FIELD_HEIGHT);": 1,
106+ "return dp(DialogStoriesCell.HEIGHT_IN_DP) + dp(SEARCH_FIELD_HEIGHT);": 1,
107+ "return dp(SEARCH_FIELD_HEIGHT);": 1,
108+ "(dp(SEARCH_FIELD_HEIGHT))": 1,
109+ "float translationY = dp(DialogStoriesCell.HEIGHT_IN_DP) + scrollYOffset + dp(SEARCH_FIELD_HEIGHT);": 1,
110+ "+ dp(DialogsActivity.SEARCH_FIELD_HEIGHT)": 1,
111+}
112+
113+errors = []
114+
115+
116+def fail(msg):
117+ errors.append(msg)
118+
119+
120+def read(rel):
121+ with open(os.path.join(ROOT, rel), encoding="utf-8") as f:
122+ return f.read()
123+
124+
125+def strip_comments(src):
126+ src = re.sub(r"/\*.*?\*/", "", src, flags=re.S)
127+ return re.sub(r"//[^\n]*", "", src)
128+
129+
130+def method_body(src, signature):
131+ """Source of the method whose declaration matches signature (a regex, re.M), up to its closing brace."""
132+ m = re.search(signature, src, re.M)
133+ if not m:
134+ return None
135+ i = src.index("{", m.end() - 1)
136+ depth = 0
137+ for j in range(i, len(src)):
138+ if src[j] == "{":
139+ depth += 1
140+ elif src[j] == "}":
141+ depth -= 1
142+ if depth == 0:
143+ return src[i:j + 1]
144+ return None
145+
146+
147+def hook(n):
148+ """Hook n as it sits in a line: its code, marker and comment, up to the end of the line (no indentation)."""
149+ return H[n] + M + r"[^\n]*"
150+
151+
152+def java_sources():
153+ for dirpath, dirnames, files in os.walk(ROOT):
154+ dirnames[:] = [d for d in dirnames if d not in (".git", "build", ".gradle")]
155+ for f in files:
156+ if f.endswith((".java", ".kt")):
157+ full = os.path.join(dirpath, f)
158+ with open(full, encoding="utf-8", errors="replace") as fh:
159+ yield os.path.relpath(full, ROOT), fh.read()
160+
161+
162+def check_hooks():
163+ for rel, pat, count in HOOKS:
164+ n = len(re.findall(pat, read(rel), re.M))
165+ if n != count:
166+ fail("hook %r found %d times in %s (expected %d)" % (pat, n, rel, count))
167+ for rel in UPSTREAM:
168+ for line in read(rel).split("\n"):
169+ if "HideSearchBar" in line and "// MultiGram: " not in line:
170+ fail("%s: line without a \"// MultiGram: \" marker: %s" % (rel, line.strip()))
171+ seen = {}
172+ for line in read(DA).split("\n"):
173+ if re.search(r"\bSEARCH_FIELD_HEIGHT\b", line) and "// MultiGram: " not in line:
174+ text = line.strip()
175+ if text not in REVIEWED:
176+ fail("%s: unreviewed use of the bar height (a new upstream use, or a stock line a hook replaced): %s"
177+ % (DA, text))
178+ seen[text] = seen.get(text, 0) + 1
179+ for text, count in REVIEWED.items():
180+ if seen.get(text, 0) != count:
181+ fail("%s: reviewed line %r found %d times (expected %d); review the bar height uses again"
182+ % (DA, text, seen.get(text, 0), count))
183+
184+
185+def need(rel, cond, msg):
186+ if not cond:
187+ fail("%s: %s" % (rel, msg))
188+
189+
190+def check_placement():
191+ src = read(DA)
192+
193+ body = method_body(src, r"^ private int getMaxScrollYOffset\(\) \{")
194+ need(DA, body is not None and re.match(r"\{\s*\n\s*" + hook(11), body),
195+ "the max-scroll hook must be the first statement of getMaxScrollYOffset")
196+
197+ body = method_body(src, r"^ private void updateContextViewPosition\(\) \{")
198+ m = body and re.search(r"totalOffset \+= storiesOverscroll;\s*\n\s*" + hook(12), body)
199+ need(DA, m, "the tabs hook must directly follow \"totalOffset += storiesOverscroll;\" in updateContextViewPosition")
200+ if m:
201+ for later in ["float fadeViewT = totalOffset;", "filterTabsView.setTranslationY("]:
202+ i = body.find(later)
203+ need(DA, i > m.end(), "the tabs hook must come before %r in updateContextViewPosition" % later)
204+
205+ body = method_body(src, r"^ public void onResume\(\) \{")
206+ need(DA, body is not None and re.search(r"super\.onResume\(\);\s*\n\s*" + hook(13), body),
207+ "the refresh hook must directly follow super.onResume() in onResume")
208+ if body is not None:
209+ i, j = body.find("HideSearchBar.refresh("), body.find(".notifyDataSetChanged()")
210+ need(DA, 0 <= i < j, "the refresh hook must run before onResume's notifyDataSetChanged")
211+
212+ # The stock lines of the two checkUi methods are counted on code with comments stripped, so a stock line that an
213+ # update comments out (Telegram often leaves old code commented out here) does not count as present.
214+ body = method_body(src, r"^ private void checkUi_searchFieldVisibility\(\) \{") or ""
215+ code = strip_comments(body)
216+ need(DA, re.search(hook(18), body), "checkUi_searchFieldVisibility must contain %r" % hook(18))
217+ for text in ["fragmentSearchField.setVisibility(alpha > 0 ? View.VISIBLE : View.GONE);",
218+ "animatorSearchButtonVisible.setValue(alpha <= 0.01f, true);"]:
219+ need(DA, code.count(text) == 1, "checkUi_searchFieldVisibility must contain %r once, as code" % text)
220+
221+ stock_factor0 = "final float factor0 = isSupportSearch() ? 1 : 0;"
222+ need(DA, src.count(stock_factor0) == 1 and code.count(stock_factor0) == 1
223+ and code.count("final float factor0 =") == 1
224+ and code.count("final float alpha = factor0 * factor1 * factor2;") == 1,
225+ "%r must remain exactly once, stock and not commented out, as the only factor0 of "
226+ "checkUi_searchFieldVisibility, feeding its stock alpha line (the icon hook replaces the other copy, in "
227+ "checkUi_itemSearchVisibility)" % stock_factor0)
228+ need(DA, body.count("HideSearchBar") == 1, "checkUi_searchFieldVisibility must hold the alpha hook only")
229+
230+ body = method_body(src, r"^ private void checkUi_itemSearchVisibility\(\) \{") or ""
231+ code = strip_comments(body)
232+ need(DA, re.match(r"\{\s*\n\s*" + hook(21) + r"\s*\n\s*"
233+ + re.escape("final float factor1 = animatorSearchButtonVisible.getFloatValue();"), body)
234+ and code.count("final float factor0 =") == 1,
235+ "the icon hook (\"no search\" variant) must be the only factor0 line of checkUi_itemSearchVisibility, "
236+ "opening it, directly followed by the stock factor1 line")
237+ for text in ["final float factor1 = animatorSearchButtonVisible.getFloatValue();",
238+ "final float factor = factor0 * factor1 * factor2 * factor3;",
239+ "FragmentFloatingButton.setAnimatedVisibility(searchItem, factor);"]:
240+ need(DA, code.count(text) == 1,
241+ "checkUi_itemSearchVisibility must contain %r once, as code (the icon hook feeds it)" % text)
242+ need(DA, body.count("HideSearchBar") == 1, "checkUi_itemSearchVisibility must hold the icon hook only")
243+
244+ # Entries left stock on purpose (README: "What still opens search"). Each still opens search with the field.
245+ need(DA, re.search(r"searchItem\.setOnClickListener\(v -> \{\s*\n\s*showSearch\(true, false, true\);", src),
246+ "the hidden header icon's listener must still open search (the topics column's search icon calls it)")
247+ need(TOPICS, "parentDialogsActivity.searchItem.performClick();" in read(TOPICS),
248+ "the topics column's search icon no longer clicks the list's header icon; review the README's "
249+ "\"What still opens search\"")
250+ need(DA, re.search(r"\} else if \(id == 3\) \{\s*\n\s*showSearch\(true, true, true\);", src),
251+ "the Downloads item must still open search (onItemClick id 3 -> showSearch)")
252+ need(DA, re.search(r"public void search\(String query, boolean animated\) \{\s*\n\s*showSearch\(true, false, animated\);",
253+ src),
254+ "tg://search links must still open search (search(query, animated) -> showSearch)")
255+
256+ body = method_body(src, r"^ private void hideActionMode\(boolean animateCheck\) \{") or ""
257+ need(DA, re.search(hook(14), body) and re.search(hook(15), body), "hideActionMode must contain hooks 14 and 15")
258+ body = method_body(src, r"^ private void showOrUpdateActionMode\(long dialogId, View cell\) \{") or ""
259+ need(DA, re.search(hook(16), body) and re.search(hook(17), body),
260+ "showOrUpdateActionMode must contain hooks 16 and 17")
261+ body = method_body(src, r"^ public int getActionBarFullHeight\(\) \{") or ""
262+ need(DA, re.search(re.escape("h += storiesOverscroll;") + r"\s*\n\s*" + hook(1), body),
263+ "the header band hook must directly follow \"h += storiesOverscroll;\" in getActionBarFullHeight")
264+
265+ need(DA, re.search(hook(5) + r"\s*\n\s*" + re.escape("-dp(SEARCH_FIELD_HEIGHT + (hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0)),")
266+ + r"\s*\n\s*searchAnimationProgress", src),
267+ "the search slide hook must be the start term of the field's lerp (end term and progress stock)")
268+ need(DA, re.search(re.escape("tabsYOffset -= Math.min(") + r"\s*\n\s*" + hook(2) + r"\s*\n\s*" + hook(3), src),
269+ "the two tab lift hooks must directly follow \"tabsYOffset -= Math.min(\"")
270+ need(DA, re.search(hook(4) + r"\s*\n\s*" + re.escape("addH *= rightSlidingDialogContainer.openedProgress;"), src),
271+ "the topics column hook must directly precede \"addH *= rightSlidingDialogContainer.openedProgress;\"")
272+ need(DA, re.search(re.escape("if (!actionModeFullyShowed) {") + r"\s*\n\s*" + hook(7), src),
273+ "the list padding hook must directly follow \"if (!actionModeFullyShowed) {\"")
274+ need(DA, re.search(re.escape("if (backward) {") + r"\s*\n\s*" + hook(8), src),
275+ "the topics column close hook must directly follow \"if (backward) {\"")
276+ need(DA, re.search(re.escape("h += dp(DialogStoriesCell.HEIGHT_IN_DP);") + r"\s*\n\s*\}\s*\n\s*" + hook(6)
277+ + r"\s*\n\s*\}\s*\n\s*" + re.escape("h += actionModeAdditionalHeight;"), src),
278+ "the page height hook must be the last statement of \"if (rightSlidingDialogContainer.hasFragment()) {\" "
279+ "in ContentView.onMeasure, after the stories block")
280+ need(DA, re.search(re.escape("int canScrollDy = -(view.getTop() - pTop) + viewsH;") + r"\s*\n\s*"
281+ + re.escape("if (!rightSlidingDialogContainer.hasFragment() && !(actionBar != null && "
282+ "actionBar.isActionModeShowed())) {") + r"\s*\n\s*" + hook(9) + r"\s*\n\s*\}", src),
283+ "the fling stop hook must be the only statement of the stock if that directly follows "
284+ "\"int canScrollDy = ...\"")
285+ need(DA, re.search(re.escape("if ((viewPage.scroller.isRunning() || dialogStoriesCell.isExpanded()) && "
286+ "!rightSlidingDialogContainer.hasFragment() && !fixScrollYAfterArchiveOpened && "
287+ "!(actionBar != null && actionBar.isActionModeShowed())) {") + r"\s*\n\s*" + hook(10)
288+ + r"\s*\n\s*\}\s*\n\s*" + re.escape("int positiveDy = Math.abs(dy);"), src),
289+ "the fling stop pair hook must be the only statement of the stock if that directly precedes "
290+ "\"int positiveDy = Math.abs(dy);\"")
291+ for n, nxt in [(14, "float finalTranslateListHeight = translateListHeight;"),
292+ (15, "viewPages[0].setTranslationY(0);"),
293+ (16, "if (translateListHeight != 0) {"),
294+ (17, "viewPages[0].setTranslationY(0);")]:
295+ need(DA, re.search(hook(n) + r"\s*\n\s*" + re.escape(nxt), src),
296+ "action mode hook %d must directly precede %r" % (n, nxt))
297+
298+ src = read(FORK)
299+ body = method_body(src, r"^ private void fillSettings\(ArrayList<UItem> items\) \{") or ""
300+ need(FORK, re.search(re.escape('.setChecked(pref("disableGlobalSearch", false)).setMultiline(true));') + r"\s*\n\s*"
301+ + hook(19), body),
302+ "the \"Hide chat list search\" row must directly follow Disable Global Search in fillSettings")
303+ body = method_body(src, r"^ private void onClick\(UItem item, View view, int position, float x, float y\) \{")
304+ need(FORK, body is not None and re.match(r"\{\s*\n\s*final int id = item\.id;\s*\n\s*" + hook(20), body),
305+ "the click hook must directly follow \"final int id = item.id;\" at the start of onClick")
306+
307+
308+def check_class():
309+ raw = read(CLS)
310+ code = strip_comments(raw)
311+ need(CLS, re.search(r'static final String KEY = "%s";' % KEY, code), "KEY must be \"%s\"" % KEY)
312+ for rel, src in java_sources():
313+ if rel != CLS and '"%s"' % KEY in src:
314+ fail("%s names the key \"%s\" (only HideSearchBar may)" % (rel, KEY))
315+ if "/multigram/" not in rel and rel not in UPSTREAM and "multigram.HideSearchBar" in src:
316+ fail("%s names multigram.HideSearchBar (only DialogsActivity and ForkSettingsActivity may)" % rel)
317+ need(CLS, len(re.findall(r"\.putBoolean\(KEY, ", code)) == 1, "exactly one .putBoolean(KEY, ...) expected")
318+ puts = re.findall(r"\.put(?:Boolean|Int|Long|Float|String|StringSet)\(", code)
319+ need(CLS, len(puts) == 1, "writes preferences other than its one key: %s" % puts)
320+ need(CLS, not re.search(r"\b(remove|clear)\(", code), "must not remove or clear preferences")
321+ reads = re.findall(r"getBoolean\(KEY, (\w+)\)", code)
322+ need(CLS, reads and all(r == "false" for r in reads), "every getBoolean(KEY, ...) must default to false")
323+
324+ m = re.search(r"static final int ROW_ID = (\d+);", code)
325+ need(CLS, m, "ROW_ID not found")
326+ if m:
327+ row = int(m.group(1))
328+ fork = strip_comments(read(FORK))
329+ ids = {int(v) for v in re.findall(r"\bID_\w+ = (\d+);", fork)}
330+ ids |= {int(v) for v in re.findall(r"\bMENU_SEARCH = (\d+);", fork)}
331+ need(CLS, row > 100 and row not in ids,
332+ "ROW_ID %d must be above 100 and clear of ForkSettingsActivity's ids" % row)
333+
334+ for name, ret in [("restHeight", r"return hides\(f\) \? 0 : stock;"), ("restAlpha", r"return hides\(f\) \? 0f : stock;")]:
335+ body = method_body(code, r"public static \w+ %s\(DialogsActivity f, \w+ stock\) \{" % name) or ""
336+ need(CLS, re.fullmatch(r"\{\s*" + ret + r"\s*\}", body), "%s must be exactly \"%s\"" % (name, ret.replace("\\", "")))
337+ need(CLS, "LocaleController" not in code, "MultiGram strings are read with Context.getString, not LocaleController")
338+
339+ xml = read(STRINGS)
340+ for n in STRING_NAMES:
341+ need(STRINGS, '<string name="%s">' % n in xml, "string %s is missing" % n)
342+ need(CLS, "R.string.%s)" % n in code, "does not use R.string.%s" % n)
343+ need(FIELD, "multigram" not in read(FIELD).lower(), "FragmentSearchField must stay stock")
344+
345+ kt = read(SETTINGS_BACKUP)
346+ m = re.search(r"ALLOWED_PREFS\s*=\s*listOf\((.*?)\)", kt, re.S)
347+ need(SETTINGS_BACKUP, m and '"mainconfig"' in m.group(1),
348+ "Forkgram's settings export no longer carries mainconfig; update the README's Preferences section")
349+
350+
351+def check_diff(base):
352+ """Every non-blank line added to the hooked upstream files since base carries the marker, or directly follows
353+ an added line that is a "// MultiGram:" comment (the Rebrand hooks' two-line form)."""
354+ for rel in UPSTREAM:
355+ out = subprocess.run(["git", "-C", ROOT, "diff", "-U0", base, "--", rel], capture_output=True, text=True)
356+ if out.returncode != 0:
357+ fail("git diff %s -- %s failed: %s" % (base, rel, out.stderr.strip()))
358+ continue
359+ prev = None
360+ for line in out.stdout.split("\n"):
361+ if line.startswith("@@") or line.startswith("---") or line.startswith("+++"):
362+ prev = None
363+ continue
364+ if not line.startswith("+"):
365+ prev = None
366+ continue
367+ text = line[1:].strip()
368+ if text and "MultiGram:" not in line and not (prev is not None and prev.startswith("// MultiGram:")):
369+ fail("%s: added line without a \"MultiGram:\" marker: %s" % (rel, text))
370+ prev = text
371+
372+
373+def main():
374+ ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
375+ ap.add_argument("--base", help="git revision of the unpatched Forkgram snapshot (optional diff check)")
376+ args = ap.parse_args()
377+ check_hooks()
378+ check_placement()
379+ check_class()
380+ if args.base:
381+ check_diff(args.base)
382+ for e in errors:
383+ print("hide-search-bar: " + e)
384+ print("hide-search-bar: %s" % ("FAILED, %d problem(s)" % len(errors) if errors else "OK"))
385+ return 1 if errors else 0
386+
387+
388+if __name__ == "__main__":
389+ sys.exit(main())