Skip to content

Commit 1167728

Browse files
committedJul 3, 2026
feat(Twitter): Added More information on profile patch
1 parent 8ffe8ba commit 1167728

18 files changed

Lines changed: 591 additions & 38 deletions

File tree

 

‎extensions/twitter/src/main/java/app/morphe/extension/twitter/Pref.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,5 +447,9 @@ public static String getExternalDownloaderPackageName(){
447447
return Utils.getStringPref(Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME);
448448
}
449449

450+
public static boolean moreInfoOnProfile(){
451+
return Utils.getBooleanPref(Settings.MORE_INFO_ON_PROFILE) && SettingsStatus.moreInfoOnProfile;
452+
}
453+
450454
//end
451455
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (C) 2026 piko <https://github.com/crimera/piko>
3+
*
4+
* See the included NOTICE file for GPLv3 §7(b) terms that apply to this code.
5+
*/
6+
7+
package app.morphe.extension.twitter.entity;
8+
9+
import app.morphe.extension.twitter.entity.ExtMediaEntities;
10+
import app.morphe.extension.twitter.entity.TweetInfo;
11+
import app.morphe.extension.twitter.entity.Debug;
12+
13+
import java.util.*;
14+
import app.morphe.extension.crimera.PikoUtils;
15+
16+
public class TwitterUser extends Debug {
17+
private final Object obj;
18+
19+
public TwitterUser(Object obj) {
20+
super(obj);
21+
this.obj = obj;
22+
}
23+
24+
public <T> T fieldNullCheck(String fieldName, Class<T> type) throws Exception {
25+
Object value = this.getField(fieldName);
26+
27+
if (value != null) {
28+
// If the value exists, cast it to the requested type
29+
return type.cast(value);
30+
}
31+
32+
// If the value is null, determine the default based on the requested type
33+
if (type == Integer.class) {
34+
return type.cast(0);
35+
} else if (type == Long.class) {
36+
return type.cast(0L);
37+
}
38+
39+
// Default for String and all other Object types
40+
return null;
41+
}
42+
43+
public int getStatusCount() throws Exception {
44+
return fieldNullCheck("getStatusCount", Integer.class);
45+
}
46+
47+
public int getMediaCount() throws Exception {
48+
return fieldNullCheck("getMediaCount", Integer.class);
49+
}
50+
51+
public int getLikesCount() throws Exception {
52+
return fieldNullCheck("getLikesCount", Integer.class);
53+
}
54+
55+
public Integer getArticleCount() throws Exception {
56+
return fieldNullCheck("getArticleCount", Integer.class);
57+
}
58+
59+
public int getFastFollowersCount() throws Exception {
60+
return fieldNullCheck("getFastFollowersCount", Integer.class);
61+
}
62+
63+
public long getLastUpdatedAt() throws Exception {
64+
return fieldNullCheck("getLastUpdatedAt", Long.class);
65+
}
66+
67+
public long getId() throws Exception {
68+
return (long) this.getMethod("getId");
69+
}
70+
71+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright (C) 2026 piko <https://github.com/crimera/piko>
3+
*
4+
* See the included NOTICE file for GPLv3 §7(b) terms that apply to this code.
5+
*/
6+
7+
package app.morphe.extension.twitter.patches.profile;
8+
9+
import android.content.Context;
10+
import android.view.View;
11+
12+
import app.morphe.extension.shared.ResourceType;
13+
import app.morphe.extension.shared.ResourceUtils;
14+
import app.morphe.extension.crimera.PikoUtils;
15+
import static app.morphe.extension.shared.StringRef.str;
16+
17+
import app.morphe.extension.twitter.entity.Debug;
18+
import app.morphe.extension.twitter.entity.TwitterUser;
19+
import app.morphe.extension.twitter.Pref;
20+
21+
import com.twitter.ui.tweet.TweetStatView;
22+
23+
public class MoreProfileInfo {
24+
25+
private static void setTweetStatViewValue(Context context,TweetStatView tweetStatView,String name,int value){
26+
// The code will be injected in patching.
27+
}
28+
29+
private static String headerComponentViewFieldName(){
30+
return "c";
31+
}
32+
33+
private static String headerComponentContextFieldName(){
34+
return "a";
35+
}
36+
37+
private static void setTweetStatView(Context context,View rootView, String resourceName, String text, int count){
38+
int targetId = ResourceUtils.getIdentifier(ResourceType.ID, resourceName);
39+
TweetStatView tweetStatView = (TweetStatView) rootView.findViewById(targetId);
40+
41+
setTweetStatViewValue(context, tweetStatView, text, count);
42+
if(count>0)
43+
tweetStatView.setVisibility(View.VISIBLE);
44+
}
45+
46+
public static void addTweetStatView(Object headerComponent, Object TwitterUserObject){
47+
try {
48+
if(Pref.moreInfoOnProfile()) {
49+
TwitterUser twitterUserEntity = new TwitterUser(TwitterUserObject);
50+
Debug headerComponentEntity = new Debug(headerComponent);
51+
52+
View rootView = (View) headerComponentEntity.getField(headerComponentViewFieldName());
53+
Context context = (Context) headerComponentEntity.getField(headerComponentContextFieldName());
54+
55+
int count = twitterUserEntity.getFastFollowersCount();
56+
String text = str("piko_fast_follower");
57+
setTweetStatView(context, rootView, "fast_follower_stat", text, count);
58+
59+
count = twitterUserEntity.getStatusCount();
60+
text = str("profile_tab_title_posts");
61+
setTweetStatView(context, rootView, "tweet_stat", text, count);
62+
63+
count = twitterUserEntity.getArticleCount();
64+
text = str("profile_tab_title_articles");
65+
setTweetStatView(context, rootView, "article_stat", text, count);
66+
67+
count = twitterUserEntity.getMediaCount();
68+
text = str("profile_tab_title_media");
69+
setTweetStatView(context, rootView, "media_stat", text, count);
70+
71+
count = twitterUserEntity.getLikesCount();
72+
text = str("profile_tab_title_favorites");
73+
setTweetStatView(context, rootView, "likes_stat", text, count);
74+
}
75+
} catch (Exception e) {
76+
PikoUtils.logger(e);
77+
}
78+
79+
}
80+
81+
public static String addUserId(String userName, Object TwitterUserObject){
82+
try{
83+
if(Pref.moreInfoOnProfile()) {
84+
TwitterUser twitterUserEntity = new TwitterUser(TwitterUserObject);
85+
long userId = twitterUserEntity.getId();
86+
return userName + "\n#" + Long.valueOf(userId);
87+
}
88+
return userName;
89+
} catch (Exception e) {
90+
PikoUtils.logger(e);
91+
return userName;
92+
}
93+
}
94+
95+
96+
97+
}

‎extensions/twitter/src/main/java/app/morphe/extension/twitter/settings/ScreenBuilder.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,14 @@ public void buildCustomiseSection(boolean buildCategory){
740740
)
741741
);
742742

743+
addPreference(category,
744+
helper.switchPreference(
745+
str("piko_pref_customisation_more_info_on_profile"),
746+
str("piko_pref_customisation_more_info_on_profile_desc"),
747+
Settings.MORE_INFO_ON_PROFILE
748+
)
749+
);
750+
743751
addPreference(category,
744752
helper.switchPreference(
745753
str("piko_single_page_settings"),

‎extensions/twitter/src/main/java/app/morphe/extension/twitter/settings/Settings.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public class Settings {
104104
public static final StringSetting REPLY_SORTING_LAST_FILTER = new StringSetting("reply_sorting_last_filter", "Relevance");
105105
public static final StringSetting CUSTOM_SEARCH_TYPE_AHEAD = new StringSetting("customisation_search_type_ahead", "");
106106
public static final StringSetting CUSTOM_POST_FONT_SIZE = new StringSetting("customisation_post_font_size", String.valueOf(ResourceUtils.getDimension("font_size_normal")));
107+
public static final BooleanSetting MORE_INFO_ON_PROFILE = new BooleanSetting("more_info_on_profile", true);
107108

108109
public static final StringSetting LAST_CHANGELOG_VERSION = new StringSetting("last_changelog_version", "0");
109110
public static final StringSetting LAST_CHANGELOG = new StringSetting("last_changelog", "0");

‎extensions/twitter/src/main/java/app/morphe/extension/twitter/settings/SettingsStatus.java‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ public class SettingsStatus {
8686
public static boolean customFont = false;
8787
public static boolean customEmojiFont = false;
8888
public static boolean inlineDownloadButton = false;
89+
90+
public static boolean moreInfoOnProfile = false;
91+
public static void moreInfoOnProfile() {
92+
moreInfoOnProfile = true;
93+
}
94+
8995
public static void inlineDownloadButton() {
9096
inlineDownloadButton = true;
9197
}
@@ -425,7 +431,7 @@ public static boolean enablePremiumSection() {
425431
}
426432

427433
public static boolean enableCustomisationSection() {
428-
return (appIconCustomisation || notificationTabCustomisation || searchTabCustomisation || typeaheadCustomisation || exploreTabCustomisation || customPostFontSize || inlineBarCustomisation || navBarCustomisation || sideBarCustomisation || profileTabCustomisation || timelineTabCustomisation || defaultReplySortFilter);
434+
return (moreInfoOnProfile || appIconCustomisation || notificationTabCustomisation || searchTabCustomisation || typeaheadCustomisation || exploreTabCustomisation || customPostFontSize || inlineBarCustomisation || navBarCustomisation || sideBarCustomisation || profileTabCustomisation || timelineTabCustomisation || defaultReplySortFilter);
429435
}
430436
public static boolean loggingSection() {
431437
return (serverResponseLogging || serverResponseLoggingOverwriteFile);

‎extensions/twitter/src/main/java/app/morphe/extension/twitter/settings/fragments/SettingsAboutFragment.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public void onCreate(@org.jetbrains.annotations.Nullable Bundle savedInstanceSta
7070
);
7171

7272
TreeMap<String,Boolean> flags = new TreeMap();
73+
flags.put(str("piko_pref_customisation_more_info_on_profile"),SettingsStatus.moreInfoOnProfile);
7374
flags.put(str("piko_pref_external_downloader_toggle"),SettingsStatus.externalDownloader);
7475
flags.put(str("piko_title_native_share_menu"),SettingsStatus.enableNativeShareMenu);
7576
flags.put(str("piko_pref_video_download"),SettingsStatus.enableVidDownload);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.twitter.ui.components.text.legacy;
2+
3+
import android.widget.TextView;
4+
import android.util.AttributeSet;
5+
import android.content.Context;
6+
7+
public class TypefacesTextView extends TextView {
8+
9+
public TypefacesTextView(Context context){
10+
super(context);
11+
}
12+
13+
public TypefacesTextView(Context context, AttributeSet attributeSet) {
14+
super(context,attributeSet);
15+
}
16+
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.twitter.ui.tweet;
2+
3+
import android.widget.RelativeLayout;
4+
import android.util.AttributeSet;
5+
import android.content.Context;
6+
7+
public class TweetStatView extends RelativeLayout {
8+
9+
public TweetStatView(Context context, AttributeSet attributeSet) {
10+
super(context,attributeSet);
11+
}
12+
13+
public void setName(CharSequence charSequence){
14+
15+
}
16+
17+
public void setVisibility(int visibility){
18+
19+
}
20+
21+
}

‎patches/src/main/kotlin/app/crimera/patches/twitter/entity/Fingerprints.kt‎

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package app.crimera.patches.twitter.entity
88

9+
import app.crimera.patches.twitter.utils.Constants.ENTITY_DESCRIPTOR
910
import app.morphe.patcher.Fingerprint
1011
import app.morphe.patcher.InstructionLocation.MatchAfterImmediately
1112
import app.morphe.patcher.methodCall
@@ -14,7 +15,8 @@ import app.morphe.patcher.string
1415
import com.android.tools.smali.dexlib2.AccessFlags
1516
import com.android.tools.smali.dexlib2.Opcode
1617

17-
private const val ENTITY_TWEET_DEFINING_CLASS = "Lapp/morphe/extension/twitter/entity/Tweet"
18+
private const val ENTITY_TWEET_DEFINING_CLASS = "${ENTITY_DESCRIPTOR}Tweet"
19+
private const val ENTITY_EXT_MEDIA_DEFINING_CLASS = "${ENTITY_DESCRIPTOR}ExtMediaEntities"
1820

1921
// --------------- Tweet
2022
internal object TweetObjectFingerprint : Fingerprint(
@@ -64,40 +66,41 @@ internal object TweetNamesFingerprint : Fingerprint(
6466
accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL),
6567
parameters = listOf(),
6668
returnType = "Ljava/lang/String;",
67-
filters = listOf(
68-
methodCall(
69-
opcode = Opcode.INVOKE_VIRTUAL,
70-
definingClass = "this",
71-
returnType = "Ljava/lang/String;"
72-
),
73-
opcode(
74-
opcode = Opcode.MOVE_RESULT_OBJECT,
75-
location = MatchAfterImmediately()
76-
),
77-
methodCall(
78-
opcode = Opcode.INVOKE_VIRTUAL,
79-
definingClass = "this",
80-
returnType = "Ljava/lang/String;",
81-
location = MatchAfterImmediately()
82-
),
83-
opcode(
84-
opcode = Opcode.MOVE_RESULT_OBJECT,
85-
location = MatchAfterImmediately()
86-
),
87-
methodCall(
88-
opcode = Opcode.INVOKE_STATIC,
89-
parameters = listOf("Ljava/lang/String;", "Ljava/lang/String;"),
90-
returnType = "Ljava/lang/String;"
91-
),
92-
opcode(
93-
opcode = Opcode.MOVE_RESULT_OBJECT,
94-
location = MatchAfterImmediately()
69+
filters =
70+
listOf(
71+
methodCall(
72+
opcode = Opcode.INVOKE_VIRTUAL,
73+
definingClass = "this",
74+
returnType = "Ljava/lang/String;",
75+
),
76+
opcode(
77+
opcode = Opcode.MOVE_RESULT_OBJECT,
78+
location = MatchAfterImmediately(),
79+
),
80+
methodCall(
81+
opcode = Opcode.INVOKE_VIRTUAL,
82+
definingClass = "this",
83+
returnType = "Ljava/lang/String;",
84+
location = MatchAfterImmediately(),
85+
),
86+
opcode(
87+
opcode = Opcode.MOVE_RESULT_OBJECT,
88+
location = MatchAfterImmediately(),
89+
),
90+
methodCall(
91+
opcode = Opcode.INVOKE_STATIC,
92+
parameters = listOf("Ljava/lang/String;", "Ljava/lang/String;"),
93+
returnType = "Ljava/lang/String;",
94+
),
95+
opcode(
96+
opcode = Opcode.MOVE_RESULT_OBJECT,
97+
location = MatchAfterImmediately(),
98+
),
99+
opcode(
100+
opcode = Opcode.RETURN_OBJECT,
101+
location = MatchAfterImmediately(),
102+
),
95103
),
96-
opcode(
97-
opcode = Opcode.RETURN_OBJECT,
98-
location = MatchAfterImmediately()
99-
)
100-
)
101104
)
102105

103106
internal object TweetMediaEntityClassFingerprint : Fingerprint(
@@ -128,12 +131,12 @@ object MediaOptionSheetMediaListVideoDownloaderImplDownloadMethodFingerprint : F
128131
)
129132

130133
internal object ExtMediaHighResVideoFingerprint : Fingerprint(
131-
definingClass = "Lapp/morphe/extension/twitter/entity/ExtMediaEntities",
134+
definingClass = ENTITY_EXT_MEDIA_DEFINING_CLASS,
132135
name = "getHighResVideo",
133136
)
134137

135138
internal object ExtMediaGetImageFingerprint : Fingerprint(
136-
definingClass = "Lapp/morphe/extension/twitter/entity/ExtMediaEntities",
139+
definingClass = ENTITY_EXT_MEDIA_DEFINING_CLASS,
137140
name = "getImageUrl",
138141
)
139142

@@ -160,6 +163,6 @@ internal object TweetInfoObjectFingerprint : Fingerprint(
160163
)
161164

162165
internal object TweetLangFingerprint : Fingerprint(
163-
definingClass = "Lapp/morphe/extension/twitter/entity/TweetInfo;",
166+
definingClass = "${ENTITY_DESCRIPTOR}TweetInfo;",
164167
name = "getLang",
165168
)

‎patches/src/main/kotlin/app/crimera/patches/twitter/entity/decoder/Decoder.kt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package app.crimera.patches.twitter.entity.decoder
88

9+
import app.crimera.patches.twitter.entity.twitterUser.TwitterUserToStringFingerprint
910
import app.morphe.patcher.patch.bytecodePatch
1011
import kotlin.properties.Delegates
1112

@@ -15,6 +16,9 @@ var TIMELINE_ITEM_CLASS_NAME: String by Delegates.notNull()
1516
var TWEET_ITEM_CLASS_NAME: String by Delegates.notNull()
1617
private set
1718

19+
var TWITTER_USER_CLASS_NAME: String by Delegates.notNull()
20+
private set
21+
1822
val decoderEntity =
1923
bytecodePatch(
2024
description = "This patch is used hold class and field names that are commonly used",
@@ -23,5 +27,7 @@ val decoderEntity =
2327
TIMELINE_ITEM_CLASS_NAME = TimelineItemDebugDialogInfoBuilderFingerprint.classDef.type
2428

2529
TWEET_ITEM_CLASS_NAME = ContextualTweetPermaLinkBuilderFingerprint.classDef.type
30+
31+
TWITTER_USER_CLASS_NAME = TwitterUserToStringFingerprint.classDef.type
2632
}
2733
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (C) 2026 piko <https://github.com/crimera/piko>
3+
*
4+
* See the included NOTICE file for GPLv3 §7(b) terms that apply to this code.
5+
*/
6+
7+
package app.crimera.patches.twitter.entity.twitterUser
8+
9+
import app.crimera.patches.twitter.utils.Constants.ENTITY_DESCRIPTOR
10+
import app.morphe.patcher.Fingerprint
11+
12+
private const val ENTITY_CLASS = "${ENTITY_DESCRIPTOR}TwitterUser;"
13+
val STRING_LIST =
14+
listOf(
15+
", fastfollowersCount=",
16+
", statusesCount=",
17+
", mediaCount=",
18+
", favoritesCount=",
19+
", articlesCount=",
20+
", lastUpdated=",
21+
)
22+
23+
internal object GetStatusCountExtension : Fingerprint(
24+
definingClass = ENTITY_CLASS,
25+
name = "getStatusCount",
26+
)
27+
28+
internal object GetMediaCountExtension : Fingerprint(
29+
definingClass = ENTITY_CLASS,
30+
name = "getMediaCount",
31+
)
32+
33+
internal object GetLikesCountExtension : Fingerprint(
34+
definingClass = ENTITY_CLASS,
35+
name = "getLikesCount",
36+
)
37+
38+
internal object GetArticleCountExtension : Fingerprint(
39+
definingClass = ENTITY_CLASS,
40+
name = "getArticleCount",
41+
)
42+
43+
internal object GetFastFollowersCountExtension : Fingerprint(
44+
definingClass = ENTITY_CLASS,
45+
name = "getFastFollowersCount",
46+
)
47+
48+
internal object GetLastUpdatedAtExtension : Fingerprint(
49+
definingClass = ENTITY_CLASS,
50+
name = "getLastUpdatedAt",
51+
)
52+
53+
object TwitterUserToStringFingerprint : Fingerprint(
54+
name = "toString",
55+
strings = STRING_LIST,
56+
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright (C) 2026 piko <https://github.com/crimera/piko>
3+
*
4+
* See the included NOTICE file for GPLv3 §7(b) terms that apply to this code.
5+
*/
6+
7+
package app.crimera.patches.twitter.entity.twitterUser
8+
9+
import app.crimera.utils.changeFirstString
10+
import app.crimera.utils.fieldExtractor
11+
import app.morphe.patcher.Match
12+
import app.morphe.patcher.extensions.InstructionExtensions.getInstruction
13+
import app.morphe.patcher.patch.bytecodePatch
14+
import app.morphe.util.indexOfFirstInstruction
15+
import com.android.tools.smali.dexlib2.Opcode
16+
17+
val twitterUserEntity =
18+
bytecodePatch(
19+
description = "For Twitter user entity reflection",
20+
) {
21+
execute {
22+
23+
// val intList = listOf(STRING_LIST[1], STRING_LIST[2], STRING_LIST[3], STRING_LIST[4])
24+
// val intFingerprints =
25+
// listOf(
26+
// GetFastFollowersCountExtension,
27+
// GetStatusCountExtension,
28+
// GetMediaCountExtension,
29+
// GetLikesCountExtension,
30+
// )
31+
32+
// val longList = listOf(STRING_LIST[6], STRING_LIST[7])
33+
// val longFingerprints =
34+
// listOf(
35+
// ,
36+
// ,
37+
// )
38+
39+
val fingerprintList =
40+
listOf(
41+
GetFastFollowersCountExtension,
42+
GetStatusCountExtension,
43+
GetMediaCountExtension,
44+
GetLikesCountExtension,
45+
GetArticleCountExtension,
46+
GetLastUpdatedAtExtension,
47+
)
48+
49+
TwitterUserToStringFingerprint.apply {
50+
val stringMatches = stringMatches
51+
method.apply {
52+
STRING_LIST.forEach { str ->
53+
val strListIndex = STRING_LIST.indexOf(str)
54+
val strIndex = stringMatches.first { it.string == str }.index
55+
val valueInstruction = getInstruction(strIndex + 2)
56+
val fieldName = valueInstruction.fieldExtractor().name
57+
fingerprintList[strListIndex].changeFirstString(fieldName)
58+
}
59+
}
60+
}
61+
}
62+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright (C) 2026 piko <https://github.com/crimera/piko>
3+
*
4+
* See the included NOTICE file for GPLv3 §7(b) terms that apply to this code.
5+
*/
6+
7+
package app.crimera.patches.twitter.misc.moreProfileInfo
8+
9+
import app.morphe.patcher.Fingerprint
10+
import app.morphe.patches.all.misc.resources.ResourceType
11+
import app.morphe.patches.all.misc.resources.resourceLiteral
12+
13+
internal object ProfileStatViewFormerFingerprint : Fingerprint(
14+
definingClass = "Lcom/twitter/app/profiles/header/components",
15+
filters =
16+
listOf(
17+
resourceLiteral(ResourceType.ID, "stats_container"),
18+
),
19+
)
20+
21+
internal object ProfileStatViewLoaderFingerprint : Fingerprint(
22+
definingClass = "Landroidx/compose/foundation/text/input/internal/",
23+
filters =
24+
listOf(
25+
resourceLiteral(ResourceType.PLURALS, "profile_follower_count"),
26+
),
27+
)
28+
29+
internal object CheckIranFlagOnUserHeaderTextFieldsFingerprint : Fingerprint(
30+
strings = listOf("android_twemoji_iran_flag_emoji_enabled"),
31+
parameters = listOf("Landroid/widget/TextView;", "Ljava/lang/CharSequence;"),
32+
)
33+
34+
internal object SetUserNameOnUserHeaderFingerprint : Fingerprint(
35+
classFingerprint = CheckIranFlagOnUserHeaderTextFieldsFingerprint,
36+
parameters = listOf("Ljava/lang/String;"),
37+
)
38+
39+
internal object SetTweetStatViewValueExtension : Fingerprint(
40+
name = "setTweetStatViewValue",
41+
)
42+
43+
internal object HeaderComponentViewFieldNameExtension : Fingerprint(
44+
name = "headerComponentViewFieldName",
45+
)
46+
47+
internal object HeaderComponentContextFieldNameExtension : Fingerprint(
48+
name = "headerComponentContextFieldName",
49+
)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright (C) 2026 piko <https://github.com/crimera/piko>
3+
*
4+
* See the included NOTICE file for GPLv3 §7(b) terms that apply to this code.
5+
*/
6+
7+
package app.crimera.patches.twitter.misc.moreProfileInfo
8+
9+
import app.crimera.patches.twitter.entity.decoder.TWITTER_USER_CLASS_NAME
10+
import app.crimera.patches.twitter.entity.decoder.decoderEntity
11+
import app.crimera.patches.twitter.entity.twitterUser.twitterUserEntity
12+
import app.crimera.patches.twitter.misc.settings.settingsPatch
13+
import app.crimera.patches.twitter.utils.Constants.COMPATIBILITY_X
14+
import app.crimera.patches.twitter.utils.Constants.PATCHES_DESCRIPTOR
15+
import app.crimera.patches.twitter.utils.enableSettings
16+
import app.crimera.utils.changeFirstString
17+
import app.morphe.patcher.extensions.InstructionExtensions.addInstructions
18+
import app.morphe.patcher.extensions.InstructionExtensions.getInstruction
19+
import app.morphe.patcher.extensions.InstructionExtensions.instructions
20+
import app.morphe.patcher.patch.bytecodePatch
21+
import app.morphe.patches.all.misc.resources.resourceMappingPatch
22+
import app.morphe.util.getReference
23+
import app.morphe.util.indexOfFirstInstruction
24+
import app.morphe.util.registersUsed
25+
import com.android.tools.smali.dexlib2.Opcode
26+
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
27+
28+
internal const val CLASS_NAME = "${PATCHES_DESCRIPTOR}/profile/MoreProfileInfo;"
29+
30+
@Suppress("unused")
31+
val moreProfileInfoPatch =
32+
bytecodePatch(
33+
name = "More information on profile",
34+
description = "Adds more details on the profile page",
35+
) {
36+
compatibleWith(COMPATIBILITY_X)
37+
dependsOn(settingsPatch, resourceMappingPatch, moreProfileInfoResourcePatch, twitterUserEntity, decoderEntity)
38+
execute {
39+
40+
var setTweetViewStatMethodCall: MethodReference
41+
ProfileStatViewLoaderFingerprint.apply {
42+
val filterIndex = instructionMatches.first().index
43+
method.apply {
44+
45+
val methodInvokeInstruction = indexOfFirstInstruction(filterIndex, Opcode.INVOKE_STATIC)
46+
setTweetViewStatMethodCall = getInstruction(methodInvokeInstruction).getReference<MethodReference>()!!
47+
48+
val entityRegister = getInstruction(indexOfFirstInstruction(filterIndex, Opcode.IGET)).registersUsed[1]
49+
val headerComponentIGetObjectIndex =
50+
instructions.indexOfLast {
51+
it.opcode == Opcode.IGET_OBJECT &&
52+
it.location.index < filterIndex
53+
}
54+
val headerComponentInstruction = getInstruction(headerComponentIGetObjectIndex)
55+
val actualHCRegister = headerComponentInstruction.registersUsed[1]
56+
57+
val ifNezRegister = getInstruction(headerComponentIGetObjectIndex - 1).registersUsed[0]
58+
59+
addInstructions(
60+
headerComponentIGetObjectIndex,
61+
"""
62+
invoke-static {v$actualHCRegister,v$entityRegister}, $CLASS_NAME->addTweetStatView(Ljava/lang/Object;Ljava/lang/Object;)V
63+
""".trimIndent(),
64+
)
65+
}
66+
SetTweetStatViewValueExtension.method.addInstructions(
67+
0,
68+
"""
69+
invoke-static {p0,p1,p2,p3}, $setTweetViewStatMethodCall
70+
""".trimIndent(),
71+
)
72+
}
73+
74+
val profileStatFormerClass = mutableClassDefBy { it.type == ProfileStatViewFormerFingerprint.method.returnType }
75+
val fields = profileStatFormerClass.fields
76+
77+
HeaderComponentContextFieldNameExtension.changeFirstString(fields.first { it.type == "Landroid/content/Context;" }.name)
78+
HeaderComponentViewFieldNameExtension.changeFirstString(fields.first { it.type == "Landroid/view/View;" }.name)
79+
80+
val userHeaderFields = SetUserNameOnUserHeaderFingerprint.classDef.fields
81+
val profileInfoField = userHeaderFields.first { it.type.startsWith("Lcom/twitter/profiles/") }
82+
val userFieldFromProfileInfo =
83+
mutableClassDefBy { it.type == profileInfoField.type }.fields.first {
84+
it.type ==
85+
TWITTER_USER_CLASS_NAME
86+
}
87+
88+
SetUserNameOnUserHeaderFingerprint.method.apply {
89+
val userNameObjectIndex = indexOfFirstInstruction(Opcode.MOVE_RESULT_OBJECT)
90+
val userNameStringRegister = getInstruction(userNameObjectIndex).registersUsed[0]
91+
92+
addInstructions(
93+
userNameObjectIndex + 1,
94+
"""
95+
# final lines of method so hardcoding registers
96+
iget-object v1, p0, $profileInfoField
97+
iget-object v1, v1, $userFieldFromProfileInfo
98+
99+
invoke-static {p1,v1}, $CLASS_NAME->addUserId(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;
100+
move-result-object p1
101+
102+
""".trimIndent(),
103+
)
104+
}
105+
106+
enableSettings("moreInfoOnProfile")
107+
}
108+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (C) 2026 piko <https://github.com/crimera/piko>
3+
*
4+
* See the included NOTICE file for GPLv3 §7(b) terms that apply to this code.
5+
*/
6+
7+
package app.crimera.patches.twitter.misc.moreProfileInfo
8+
9+
import app.morphe.patcher.patch.resourcePatch
10+
import app.morphe.patches.all.misc.resources.resourceMappingPatch
11+
import app.morphe.util.findElementByAttributeValueOrThrow
12+
13+
internal val fieldList = listOf("fast_follower_stat", "tweet_stat", "article_stat", "media_stat", "likes_stat")
14+
15+
internal val moreProfileInfoResourcePatch =
16+
resourcePatch {
17+
dependsOn(resourceMappingPatch)
18+
execute {
19+
document("res/layout/profile_details.xml").use { editor ->
20+
val statsContainer =
21+
editor.childNodes.findElementByAttributeValueOrThrow(
22+
"android:id",
23+
"@id/stats_container",
24+
)
25+
26+
fieldList.forEach {
27+
val stat = editor.createElement("com.twitter.ui.tweet.TweetStatView")
28+
stat.setAttribute("android:id", "@+id/$it")
29+
stat.setAttribute("android:visibility", "gone")
30+
stat.setAttribute("android:layout_width", "wrap_content")
31+
stat.setAttribute("android:layout_height", "fill_parent")
32+
stat.setAttribute("style", "@style/ProfileStatView")
33+
34+
statsContainer.appendChild(stat)
35+
}
36+
}
37+
}
38+
}

‎patches/src/main/kotlin/app/crimera/patches/twitter/utils/Constants.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,6 @@ object Constants {
5656

5757
const val SSTS_DESCRIPTOR = "invoke-static {}, $ACTIVITY_SETTINGS_CLASS/SettingsStatus;"
5858
const val FSTS_DESCRIPTOR = "invoke-static {}, $INTEGRATIONS_PACKAGE/patches/FeatureSwitchPatch;"
59+
60+
const val ENTITY_DESCRIPTOR = "$INTEGRATIONS_PACKAGE/entity/"
5961
}

‎patches/src/main/resources/addresources/values/twitter/strings.xml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@
206206
<string name="piko_pref_customisation_search_type_ahead">Search suggestions to hide</string>
207207
<string name="piko_pref_customisation_change_app_icon">Change app icon</string>
208208
<string name="piko_notifications_tab_priority">Priority</string>
209+
<string name="piko_pref_customisation_more_info_on_profile">More information on profile</string>
210+
<string name="piko_pref_customisation_more_info_on_profile_desc">Adds more details on the profile page</string>
211+
<string name="piko_fast_follower">Fast follower</string>
209212

210213
<!-- Font Settings -->
211214
<string name="piko_title_font">Font style</string>

0 commit comments

Comments
 (0)
Please sign in to comment.