on main help sign in
Add an option to remove the chat list search bar
2a3b4e9Claude
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.
879 h += storiesHeight * (1f - searchAnimationProgress) * (1f - rightSlidingProgress) * (1f - progressToActionMode);
879 h += storiesHeight * (1f - searchAnimationProgress) * (1f - rightSlidingProgress) * (1f - progressToActionMode);
880 }
880 }
881 h += storiesOverscroll;
881 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
883
883
884 return (int) h;
884 return (int) h;
885 }
885 }
@@ -1057,8 +1057,8 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
1057 tabsYOffset = 0;
1057 tabsYOffset = 0;
1058 storiesYOffset = 0;
1058 storiesYOffset = 0;
1059 tabsYOffset -= Math.min(
1059 tabsYOffset -= Math.min(
1060− dp(hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0) + dp(SEARCH_FIELD_HEIGHT) + scrollYOffset,
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(SEARCH_FIELD_HEIGHT))
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
1062 );
1062 );
1063 storiesYOffset = tabsYOffset;
1063 storiesYOffset = tabsYOffset;
1064 if ((rightSlidingDialogContainer != null && rightSlidingDialogContainer.hasFragment())) {
1064 if ((rightSlidingDialogContainer != null && rightSlidingDialogContainer.hasFragment())) {
@@ -1084,14 +1084,14 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
1084 if (hasStories) {
1084 if (hasStories) {
1085 addH += dp(DialogStoriesCell.HEIGHT_IN_DP);
1085 addH += dp(DialogStoriesCell.HEIGHT_IN_DP);
1086 }
1086 }
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
1088 addH *= rightSlidingDialogContainer.openedProgress;
1088 addH *= rightSlidingDialogContainer.openedProgress;
1089
1089
1090 viewPages[0].setTranslationY(rightFragmentOffset - addH);
1090 viewPages[0].setTranslationY(rightFragmentOffset - addH);
1091 } else {
1091 } else {
1092 if (fragmentSearchField != null) {
1092 if (fragmentSearchField != null) {
1093 fragmentSearchField.setTranslationY(lerp(
1093 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
1095 -dp(SEARCH_FIELD_HEIGHT + (hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0)),
1095 -dp(SEARCH_FIELD_HEIGHT + (hasStories ? DialogStoriesCell.HEIGHT_IN_DP : 0)),
1096 searchAnimationProgress
1096 searchAnimationProgress
1097 ));
1097 ));
@@ -1183,7 +1183,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
1183 if (hasStories) {
1183 if (hasStories) {
1184 h += dp(DialogStoriesCell.HEIGHT_IN_DP);
1184 h += dp(DialogStoriesCell.HEIGHT_IN_DP);
1185 }
1185 }
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
1187 }
1187 }
1188 h += actionModeAdditionalHeight;
1188 h += actionModeAdditionalHeight;
1189 if (actionBarColorAnimator == null) {
1189 if (actionBarColorAnimator == null) {
@@ -2078,7 +2078,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
2078 t += dp(DialogStoriesCell.HEIGHT_IN_DP);
2078 t += dp(DialogStoriesCell.HEIGHT_IN_DP);
2079 }
2079 }
2080 if (!actionModeFullyShowed) {
2080 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
2082 }
2082 }
2083 additionalPadding = 0;
2083 additionalPadding = 0;
2084
2084
@@ -2366,7 +2366,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
2366 offset += dp(DialogStoriesCell.HEIGHT_IN_DP);
2366 offset += dp(DialogStoriesCell.HEIGHT_IN_DP);
2367 }
2367 }
2368 if (backward) {
2368 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
2370 // offset += canShowFilterTabsView ? dp(50) : 0;
2370 // offset += canShowFilterTabsView ? dp(50) : 0;
2371 }
2371 }
2372 if (p >= 0) {
2372 if (p >= 0) {
@@ -4224,13 +4224,13 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
4224 }
4224 }
4225 int canScrollDy = -(view.getTop() - pTop) + viewsH;
4225 int canScrollDy = -(view.getTop() - pTop) + viewsH;
4226 if (!rightSlidingDialogContainer.hasFragment() && !(actionBar != null && actionBar.isActionModeShowed())) {
4226 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
4228 }
4228 }
4229 if (hasStories && (viewPage.scroller.isRunning() || dialogStoriesCell.isExpanded()) && !rightSlidingDialogContainer.hasFragment() && !fixScrollYAfterArchiveOpened) {
4229 if (hasStories && (viewPage.scroller.isRunning() || dialogStoriesCell.isExpanded()) && !rightSlidingDialogContainer.hasFragment() && !fixScrollYAfterArchiveOpened) {
4230 canScrollDy += dp(DialogStoriesCell.HEIGHT_IN_DP);
4230 canScrollDy += dp(DialogStoriesCell.HEIGHT_IN_DP);
4231 }
4231 }
4232 if ((viewPage.scroller.isRunning() || dialogStoriesCell.isExpanded()) && !rightSlidingDialogContainer.hasFragment() && !fixScrollYAfterArchiveOpened && !(actionBar != null && actionBar.isActionModeShowed())) {
4232 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
4234 }
4234 }
4235 int positiveDy = Math.abs(dy);
4235 int positiveDy = Math.abs(dy);
4236 if (canScrollDy < positiveDy) {
4236 if (canScrollDy < positiveDy) {
@@ -5742,6 +5742,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
5742 }
5742 }
5743
5743
5744 private int getMaxScrollYOffset() {
5744 private int getMaxScrollYOffset() {
5745+ if (org.telegram.messenger.multigram.HideSearchBar.hides(this)) return getMaxScrollYOffsetWithoutSearch(); // MultiGram: the header collapses by the stories only
5745 if (hasStories) {
5746 if (hasStories) {
5746 return dp(DialogStoriesCell.HEIGHT_IN_DP) + dp(SEARCH_FIELD_HEIGHT);
5747 return dp(DialogStoriesCell.HEIGHT_IN_DP) + dp(SEARCH_FIELD_HEIGHT);
5747 } else {
5748 } else {
@@ -6583,6 +6584,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
6583 searchTabsHeight * searchAnimationProgress + tabsYOffset;
6584 searchTabsHeight * searchAnimationProgress + tabsYOffset;
6584 }
6585 }
6585 totalOffset += storiesOverscroll;
6586 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)
6586
6588
6587 float searchVisibility = 0;
6589 float searchVisibility = 0;
6588 if (fragmentSearchField != null && fragmentSearchField.getVisibility() == View.VISIBLE) {
6590 if (fragmentSearchField != null && fragmentSearchField.getVisibility() == View.VISIBLE) {
@@ -7085,6 +7087,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
7085 @Override
7087 @Override
7086 public void onResume() {
7088 public void onResume() {
7087 super.onResume();
7089 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
7088 if (dialogStoriesCell != null) {
7091 if (dialogStoriesCell != null) {
7089 dialogStoriesCell.onResume();
7092 dialogStoriesCell.onResume();
7090 }
7093 }
@@ -9108,7 +9111,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
9108 viewPages[i].listView.cancelClickRunnables(true);
9111 viewPages[i].listView.cancelClickRunnables(true);
9109 }
9112 }
9110 }
9113 }
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
9112 float finalTranslateListHeight = translateListHeight;
9115 float finalTranslateListHeight = translateListHeight;
9113 actionBarColorAnimator = ValueAnimator.ofFloat(progressToActionMode, 0);
9116 actionBarColorAnimator = ValueAnimator.ofFloat(progressToActionMode, 0);
9114 actionBarColorAnimator.addUpdateListener(valueAnimator -> {
9117 actionBarColorAnimator.addUpdateListener(valueAnimator -> {
@@ -9134,7 +9137,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
9134 invalidateScrollY = true;
9137 invalidateScrollY = true;
9135 fixScrollYAfterArchiveOpened = true;
9138 fixScrollYAfterArchiveOpened = true;
9136 fragmentView.invalidate();
9139 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
9138 viewPages[0].setTranslationY(0);
9141 viewPages[0].setTranslationY(0);
9139 for (int i = 0; i < viewPages.length; i++) {
9142 for (int i = 0; i < viewPages.length; i++) {
9140 if (viewPages[i] != null) {
9143 if (viewPages[i] != null) {
@@ -10237,7 +10240,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
10237 viewPages[i].listView.cancelClickRunnables(true);
10240 viewPages[i].listView.cancelClickRunnables(true);
10238 }
10241 }
10239 }
10242 }
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
10241 if (translateListHeight != 0) {
10244 if (translateListHeight != 0) {
10242 actionModeAdditionalHeight = (int) translateListHeight;
10245 actionModeAdditionalHeight = (int) translateListHeight;
10243 fragmentView.requestLayout();
10246 fragmentView.requestLayout();
@@ -10264,7 +10267,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
10264 actionBarColorAnimator = null;
10267 actionBarColorAnimator = null;
10265 actionModeAdditionalHeight = 0;
10268 actionModeAdditionalHeight = 0;
10266 actionModeFullyShowed = true;
10269 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
10268 viewPages[0].setTranslationY(0);
10271 viewPages[0].setTranslationY(0);
10269 for (int i = 0; i < viewPages.length; i++) {
10272 for (int i = 0; i < viewPages.length; i++) {
10270 if (viewPages[i] != null) {
10273 if (viewPages[i] != null) {
@@ -14240,7 +14243,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
14240 }
14243 }
14241
14244
14242 final int maxScrollWithoutSearch = getMaxScrollYOffsetWithoutSearch();
14245 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
14244
14247
14245 final float actionModeVisible = Math.max(progressToActionMode, animatorActionModeVisible.getFloatValue());
14248 final float actionModeVisible = Math.max(progressToActionMode, animatorActionModeVisible.getFloatValue());
14246 final float searchFieldVisible = animatorSearchVisible.getFloatValue();
14249 final float searchFieldVisible = animatorSearchVisible.getFloatValue();
@@ -14325,7 +14328,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
14325 }
14328 }
14326
14329
14327 private void checkUi_itemSearchVisibility() {
14330 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
14329 final float factor1 = animatorSearchButtonVisible.getFloatValue();
14332 final float factor1 = animatorSearchButtonVisible.getFloatValue();
14330 final float factor2 = 1f - getRightSlidingProgress();
14333 final float factor2 = 1f - getRightSlidingProgress();
14331 final float factor3 = 1f - animatorDoneButtonVisible.getFloatValue();
14334 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 {
587 .setChecked(pref("disableThumbsInDialogList", false)).setMultiline(true));
587 .setChecked(pref("disableThumbsInDialogList", false)).setMultiline(true));
588 items.add(UItem.asButtonCheck(ID_DISABLE_GLOBAL_SEARCH, LocaleController.getString(R.string.DisableGlobalSearch), LocaleController.getString(R.string.DisableGlobalSearchInfo))
588 items.add(UItem.asButtonCheck(ID_DISABLE_GLOBAL_SEARCH, LocaleController.getString(R.string.DisableGlobalSearch), LocaleController.getString(R.string.DisableGlobalSearchInfo))
589 .setChecked(pref("disableGlobalSearch", false)).setMultiline(true));
589 .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
590 items.add(UItem.asButtonCheck(ID_HIDE_CONTACTS_IN_DIALOGS, LocaleController.getString(R.string.HideContactsInDialogs), LocaleController.getString(R.string.HideContactsInDialogsInfo))
591 items.add(UItem.asButtonCheck(ID_HIDE_CONTACTS_IN_DIALOGS, LocaleController.getString(R.string.HideContactsInDialogs), LocaleController.getString(R.string.HideContactsInDialogsInfo))
591 .setChecked(pref("hideContactsInDialogs", false)).setMultiline(true));
592 .setChecked(pref("hideContactsInDialogs", false)).setMultiline(true));
592 items.add(UItem.asButtonCheck(ID_ENABLE_LAST_SEEN_DOTS, LocaleController.getString(R.string.EnableLastSeenDots), LocaleController.getString(R.string.EnableLastSeenDotsInfo))
593 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 {
759
760
760 private void onClick(UItem item, View view, int position, float x, float y) {
761 private void onClick(UItem item, View view, int position, float x, float y) {
761 final int id = item.id;
762 final int id = item.id;
763+ if (org.telegram.messenger.multigram.HideSearchBar.onSettingsClick(item, view)) return; // MultiGram: "Hide chat list search" toggle
762
764
763 if (id == ID_HIDE_SENSITIVE_DATA) {
765 if (id == ID_HIDE_SENSITIVE_DATA) {
764 toggle("hideSensitiveData", item, view);
766 toggle("hideSensitiveData", item, view);
TMessagesProj/src/main/res/values/multigram_strings.xml+2
@@ -9,4 +9,6 @@
9 <string name="MultiGramStyleShuffled">New style applied.</string>
9 <string name="MultiGramStyleShuffled">New style applied.</string>
10 <string name="MultiGramUndoFailed">Couldn\'t bring back your previous style.</string>
10 <string name="MultiGramUndoFailed">Couldn\'t bring back your previous style.</string>
11 <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>
11 <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>
12</resources>
14</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
237 placement, its privacy guards, strings and backup policy (`multigram/random-style/README.md`).
237 placement, its privacy guards, strings and backup policy (`multigram/random-style/README.md`).
238 `style_knobs_check.py`, when the tree has it: the style knobs' hooks and their placement, the settings
238 `style_knobs_check.py`, when the tree has it: the style knobs' hooks and their placement, the settings
239 they may write and their safe ranges (`multigram/style-knobs/README.md`).
239 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`).
2402. `check_style_table.py`: the format rules above, every record; theme keys must be bundled base themes
2432. `check_style_table.py`: the format rules above, every record; theme keys must be bundled base themes
241 of this tree with the right night flag. Self-tests: the parser must recognise every form of the
244 of this tree with the right night flag. Self-tests: the parser must recognise every form of the
242 out-text guard (stock, `|| id > 100`, and the palette fix's exact line) with the rest of Theme.java
245 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
23 python3 multigram/tools/style_knobs_check.py
23 python3 multigram/tools/style_knobs_check.py
24fi
24fi
25
25
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+
26if [ -f multigram/rebrand/selftest.py ]; then
32if [ -f multigram/rebrand/selftest.py ]; then
27 # Rebrand toolkit: generate two identities on a scratch copy, check the output, then --clean back to the commit.
33 # Rebrand toolkit: generate two identities on a scratch copy, check the output, then --clean back to the commit.
28 echo "== rebrand toolkit: self-test"
34 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())