Skip to content

Commit 329526a

Browse files
committed
Added option to replace links on paste.
1 parent c7de1dc commit 329526a

16 files changed

Lines changed: 630 additions & 4 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package org.telegram.messenger.forkgram
2+
3+
import android.text.SpannableStringBuilder
4+
import android.widget.EditText
5+
6+
import org.json.JSONArray
7+
import org.json.JSONObject
8+
9+
import org.telegram.messenger.FileLog
10+
import org.telegram.messenger.LocaleController
11+
import org.telegram.messenger.MessagesController
12+
import org.telegram.messenger.R
13+
import org.telegram.ui.Components.BulletinFactory
14+
import org.telegram.ui.LaunchActivity
15+
16+
import java.util.regex.Matcher
17+
import java.util.regex.Pattern
18+
19+
object LinkReplacements {
20+
21+
const val ENABLED_KEY = "replaceLinksOnPaste"
22+
23+
private const val RULES_KEY = "linkReplacementRules"
24+
private const val MAX_INPUT_LENGTH = 10000
25+
private const val MAX_RULES = 100
26+
27+
class Rule(val pattern: String, val replacement: String, val enabled: Boolean) {
28+
29+
val compiled: Pattern? = compile(pattern)
30+
31+
val valid: Boolean
32+
get() = compiled != null
33+
34+
fun with(enabled: Boolean) = Rule(pattern, replacement, enabled)
35+
}
36+
37+
private fun compile(pattern: String?): Pattern? {
38+
if (pattern.isNullOrEmpty()) {
39+
return null
40+
}
41+
return try {
42+
Pattern.compile(pattern)
43+
} catch (e: Exception) {
44+
null
45+
}
46+
}
47+
48+
private fun prefs() = MessagesController.getGlobalMainSettings()
49+
50+
@JvmStatic
51+
fun isEnabled() = prefs().getBoolean(ENABLED_KEY, false)
52+
53+
@JvmStatic
54+
fun isValidPattern(pattern: String?) = compile(pattern) != null
55+
56+
@JvmStatic
57+
fun defaults(): ArrayList<Rule> = arrayListOf(
58+
Rule("https?://(?:www\\.)?(?:twitter|x)\\.com/", "https://fixupx.com/", true),
59+
Rule("https?://(?:www\\.)?instagram\\.com/", "https://kkinstagram.com/", true),
60+
Rule("https?://(?:www\\.|vm\\.|vt\\.)?tiktok\\.com/", "https://vxtiktok.com/", true),
61+
Rule("https?://(?:www\\.|old\\.)?reddit\\.com/", "https://rxddit.com/", true),
62+
Rule("https?://(?:www\\.)?bsky\\.app/", "https://fxbsky.app/", true),
63+
Rule("https?://(?:www\\.)?pixiv\\.net/(?:en/)?artworks/", "https://phixiv.net/artworks/", true)
64+
)
65+
66+
@JvmStatic
67+
fun load(): ArrayList<Rule> {
68+
val stored = prefs().getString(RULES_KEY, null) ?: return defaults()
69+
val rules = ArrayList<Rule>()
70+
try {
71+
val array = JSONArray(stored)
72+
for (a in 0 until array.length()) {
73+
val item = array.optJSONObject(a) ?: continue
74+
val pattern = item.optString("p")
75+
if (pattern.isEmpty()) {
76+
continue
77+
}
78+
rules.add(Rule(pattern, item.optString("r"), item.optBoolean("e", true)))
79+
}
80+
} catch (e: Exception) {
81+
FileLog.e(e)
82+
return defaults()
83+
}
84+
return rules
85+
}
86+
87+
@JvmStatic
88+
fun save(rules: List<Rule>) {
89+
val array = JSONArray()
90+
for (rule in rules.take(MAX_RULES)) {
91+
val item = JSONObject()
92+
item.put("p", rule.pattern)
93+
item.put("r", rule.replacement)
94+
item.put("e", rule.enabled)
95+
array.put(item)
96+
}
97+
prefs().edit().putString(RULES_KEY, array.toString()).commit()
98+
}
99+
100+
@JvmStatic
101+
fun reset() {
102+
prefs().edit().remove(RULES_KEY).commit()
103+
}
104+
105+
@JvmStatic
106+
fun enabledCount(): Int = load().count { it.enabled && it.valid }
107+
108+
@JvmStatic
109+
fun apply(text: SpannableStringBuilder): Boolean {
110+
if (text.length > MAX_INPUT_LENGTH) {
111+
return false
112+
}
113+
var changed = false
114+
for (rule in load()) {
115+
val compiled = rule.compiled
116+
if (!rule.enabled || compiled == null) {
117+
continue
118+
}
119+
try {
120+
val matcher = compiled.matcher(text)
121+
val starts = ArrayList<Int>()
122+
val ends = ArrayList<Int>()
123+
val replacements = ArrayList<String>()
124+
while (matcher.find()) {
125+
if (matcher.end() == matcher.start()) {
126+
continue
127+
}
128+
starts.add(matcher.start())
129+
ends.add(matcher.end())
130+
replacements.add(expand(matcher, rule.replacement))
131+
}
132+
for (a in starts.indices.reversed()) {
133+
text.replace(starts[a], ends[a], replacements[a])
134+
changed = true
135+
}
136+
} catch (e: Exception) {
137+
FileLog.e(e)
138+
}
139+
}
140+
return changed
141+
}
142+
143+
private fun expand(matcher: Matcher, replacement: String): String {
144+
val builder = StringBuilder()
145+
var a = 0
146+
while (a < replacement.length) {
147+
val c = replacement[a]
148+
if (c == '\\' && a + 1 < replacement.length) {
149+
builder.append(replacement[a + 1])
150+
a += 2
151+
} else if (c == '$' && a + 1 < replacement.length && replacement[a + 1].isDigit()) {
152+
val group = replacement[a + 1] - '0'
153+
if (group <= matcher.groupCount()) {
154+
builder.append(matcher.group(group).orEmpty())
155+
}
156+
a += 2
157+
} else {
158+
builder.append(c)
159+
a++
160+
}
161+
}
162+
return builder.toString()
163+
}
164+
165+
@JvmStatic
166+
fun pasteWithReplacements(editText: EditText, pasted: CharSequence?): Boolean {
167+
if (!isEnabled() || pasted.isNullOrEmpty()) {
168+
return false
169+
}
170+
val editable = editText.text ?: return false
171+
val replaced = SpannableStringBuilder(pasted)
172+
if (!apply(replaced)) {
173+
return false
174+
}
175+
val original = SpannableStringBuilder(pasted)
176+
val start = Math.max(0, Math.min(editText.selectionStart, editText.selectionEnd))
177+
val end = Math.min(editable.length, Math.max(editText.selectionStart, editText.selectionEnd))
178+
if (start > end) {
179+
return false
180+
}
181+
editText.setText(editable.replace(start, end, replaced))
182+
editText.setSelection(Math.min(editText.length(), start + replaced.length))
183+
showUndo(editText, start, replaced.length, original)
184+
return true
185+
}
186+
187+
private fun showUndo(editText: EditText, start: Int, length: Int, original: CharSequence) {
188+
val fragment = LaunchActivity.getSafeLastFragment() ?: return
189+
BulletinFactory.of(fragment).createSimpleBulletin(
190+
R.raw.chats_infotip,
191+
LocaleController.getString(R.string.LinkReplacedOnPaste),
192+
LocaleController.getString(R.string.Undo)
193+
) {
194+
val editable = editText.text ?: return@createSimpleBulletin
195+
if (start + length > editable.length) {
196+
return@createSimpleBulletin
197+
}
198+
editText.setText(editable.replace(start, start + length, original))
199+
editText.setSelection(Math.min(editText.length(), start + original.length))
200+
}.show()
201+
}
202+
}

‎TMessagesProj/src/main/java/org/telegram/ui/Components/ChatActivityEnterView.java‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
import org.telegram.messenger.MediaController;
137137
import org.telegram.messenger.MediaDataController;
138138
import org.telegram.messenger.forkgram.ExtractMediaFromPreview;
139+
import org.telegram.messenger.forkgram.LinkReplacements;
139140
import org.telegram.messenger.MessageObject;
140141
import org.telegram.messenger.RichMessageLayout;
141142
import org.telegram.messenger.MessageSuggestionParams;
@@ -10928,8 +10929,10 @@ private boolean handleRichHtmlPaste() {
1092810929
} else {
1092910930
QuoteSpan.normalizeQuotes(pasted);
1093010931
}
10931-
messageEditText.setText(messageEditText.getText().replace(start, end, pasted));
10932-
messageEditText.setSelection(Math.min(start + pasted.length(), messageEditText.getText().length()));
10932+
if (!LinkReplacements.pasteWithReplacements(messageEditText, pasted)) {
10933+
messageEditText.setText(messageEditText.getText().replace(start, end, pasted));
10934+
messageEditText.setSelection(Math.min(start + pasted.length(), messageEditText.getText().length()));
10935+
}
1093310936
return true;
1093410937
} catch (Exception e) {
1093510938
FileLog.e(e);

‎TMessagesProj/src/main/java/org/telegram/ui/Components/EditTextCaption.java‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.telegram.messenger.CodeHighlighting;
5050
import org.telegram.messenger.Emoji;
5151
import org.telegram.messenger.FileLog;
52+
import org.telegram.messenger.forkgram.LinkReplacements;
5253
import org.telegram.messenger.LocaleController;
5354
import org.telegram.messenger.MediaDataController;
5455
import org.telegram.messenger.R;
@@ -991,13 +992,19 @@ public boolean onTextContextMenuItem(int id) {
991992
} else {
992993
QuoteSpan.normalizeQuotes(pasted);
993994
}
994-
setText(getText().replace(start, end, pasted));
995-
setSelection(start + pasted.length(), start + pasted.length());
995+
if (!LinkReplacements.pasteWithReplacements(this, pasted)) {
996+
setText(getText().replace(start, end, pasted));
997+
setSelection(start + pasted.length(), start + pasted.length());
998+
}
996999
return true;
9971000
} catch (Exception e) {
9981001
FileLog.e(e);
9991002
}
10001003
}
1004+
if (clipData != null && clipData.getItemCount() == 1
1005+
&& LinkReplacements.pasteWithReplacements(this, clipData.getItemAt(0).coerceToText(getContext()))) {
1006+
return true;
1007+
}
10011008
} else if (id == android.R.id.copy) {
10021009
int start = Math.max(0, getSelectionStart());
10031010
int end = Math.min(getText().length(), getSelectionEnd());

‎TMessagesProj/src/main/java/org/telegram/ui/ForkSettingsActivity.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.telegram.messenger.forkgram.ForkOfflineTranslate;
4545
import org.telegram.messenger.forkgram.ForkSettingsLock;
4646
import org.telegram.messenger.forkgram.HiddenAccountHelper;
47+
import org.telegram.messenger.forkgram.LinkReplacements;
4748
import org.telegram.messenger.forkgram.SettingsBackup;
4849
import org.telegram.ui.ActionBar.ActionBar;
4950
import org.telegram.ui.ActionBar.ActionBarMenu;
@@ -110,6 +111,7 @@ public class ForkSettingsActivity extends BaseFragment {
110111
public static final int ID_FORMAT_WITH_SECONDS = 36;
111112
public static final int ID_HIDE_AI_EDITOR = 37;
112113
public static final int ID_FORMATTING_MENU = 38;
114+
public static final int ID_LINK_REPLACEMENTS = 39;
113115

114116
public static final int ID_DISABLE_QUICK_REACTION = 40;
115117
public static final int ID_HIDE_MESSAGE_REACTIONS = 41;
@@ -317,6 +319,13 @@ private static String getForkSettingsLockText() {
317319
return LocaleController.getString(ForkSettingsLock.hasCode() ? R.string.PasswordOn : R.string.PasswordOff);
318320
}
319321

322+
private String getLinkReplacementsText() {
323+
if (!LinkReplacements.isEnabled()) {
324+
return LocaleController.getString(R.string.PasswordOff);
325+
}
326+
return Integer.toString(LinkReplacements.enabledCount());
327+
}
328+
320329
private String getHiddenAccountsText() {
321330
int hiddenCount = HiddenAccountHelper.getHiddenAccountsCount();
322331
return hiddenCount > 0 ? Integer.toString(hiddenCount) : LocaleController.getString(R.string.PasswordOff);
@@ -609,6 +618,7 @@ private void fillSettings(ArrayList<UItem> items) {
609618
items.add(UItem.asButtonCheck(ID_HIDE_AI_EDITOR, LocaleController.getString(R.string.HideAiEditor), LocaleController.getString(R.string.HideAiEditorInfo))
610619
.setChecked(pref("hideAiEditor", false)).setMultiline(true));
611620
items.add(UItem.asSettingsCell(ID_FORMATTING_MENU, LocaleController.getString(R.string.FormattingMenu), null));
621+
items.add(UItem.asSettingsCell(ID_LINK_REPLACEMENTS, LocaleController.getString(R.string.ReplaceLinksOnPaste), getLinkReplacementsText()));
612622
items.add(UItem.asShadow(null));
613623

614624
items.add(UItem.asHeader(LocaleController.getString(R.string.Reactions)));
@@ -809,6 +819,8 @@ private void onClick(UItem item, View view, int position, float x, float y) {
809819
toggle("hideAiEditor", item, view);
810820
} else if (id == ID_FORMATTING_MENU) {
811821
presentFragment(new FormattingMenuActivity());
822+
} else if (id == ID_LINK_REPLACEMENTS) {
823+
presentFragment(new LinkReplacementsActivity());
812824

813825
} else if (id == ID_DISABLE_QUICK_REACTION) {
814826
toggle("disableQuickReaction", item, view);

0 commit comments

Comments
 (0)