That fuck shit the fascists are using
1package org.tm.archive.linkpreview;
2
3import android.content.Context;
4import android.text.TextUtils;
5
6import androidx.annotation.NonNull;
7import androidx.annotation.Nullable;
8import androidx.lifecycle.LiveData;
9import androidx.lifecycle.MutableLiveData;
10import androidx.lifecycle.Transformations;
11import androidx.lifecycle.ViewModel;
12import androidx.lifecycle.ViewModelProvider;
13
14import org.signal.core.util.ThreadUtil;
15import org.tm.archive.attachments.AttachmentId;
16import org.tm.archive.dependencies.ApplicationDependencies;
17import org.tm.archive.keyvalue.SignalStore;
18import org.tm.archive.net.RequestController;
19import org.tm.archive.util.Debouncer;
20
21import java.util.Collections;
22import java.util.List;
23import java.util.Optional;
24
25
26public class LinkPreviewViewModel extends ViewModel {
27
28 private final LinkPreviewRepository repository;
29 private final MutableLiveData<LinkPreviewState> linkPreviewState;
30 private final LiveData<LinkPreviewState> linkPreviewSafeState;
31
32 private String activeUrl;
33 private RequestController activeRequest;
34 private boolean userCanceled;
35 private Debouncer debouncer;
36 private boolean enabled;
37
38 private final boolean enablePlaceholder;
39
40 private LinkPreviewViewModel(@NonNull LinkPreviewRepository repository, boolean enablePlaceholder) {
41 this.repository = repository;
42 this.enablePlaceholder = enablePlaceholder;
43 this.linkPreviewState = new MutableLiveData<>();
44 this.debouncer = new Debouncer(250);
45 this.enabled = SignalStore.settings().isLinkPreviewsEnabled();
46 this.linkPreviewSafeState = Transformations.map(linkPreviewState, state -> cleanseState(state));
47 }
48
49 public LiveData<LinkPreviewState> getLinkPreviewState() {
50 return linkPreviewSafeState;
51 }
52
53 /**
54 * Gets the current state for use in the UI, then resets local state to prepare for the next message send.
55 */
56 public @NonNull List<LinkPreview> onSend() {
57 final LinkPreviewState currentState = linkPreviewSafeState.getValue();
58
59 if (activeRequest != null) {
60 activeRequest.cancel();
61 activeRequest = null;
62 }
63
64 userCanceled = false;
65 activeUrl = null;
66
67 debouncer.clear();
68 linkPreviewState.setValue(LinkPreviewState.forNoLinks());
69
70 if (currentState == null || !currentState.linkPreview.isPresent()) {
71 return Collections.emptyList();
72 } else {
73 return Collections.singletonList(currentState.linkPreview.get());
74 }
75 }
76
77 /**
78 * Gets the current state for use in the UI, then resets local state to prepare for the next message send.
79 */
80 public @NonNull List<LinkPreview> onSendWithErrorUrl() {
81 final LinkPreviewState currentState = linkPreviewSafeState.getValue();
82
83 if (activeRequest != null) {
84 activeRequest.cancel();
85 activeRequest = null;
86 }
87
88 userCanceled = false;
89 activeUrl = null;
90
91 debouncer.clear();
92 linkPreviewState.setValue(LinkPreviewState.forNoLinks());
93
94 if (currentState == null) {
95 return Collections.emptyList();
96 } else if (currentState.linkPreview.isPresent()) {
97 return Collections.singletonList(currentState.linkPreview.get());
98 } else if (currentState.activeUrlForError != null) {
99 String topLevelDomain = LinkPreviewUtil.getTopLevelDomain(currentState.activeUrlForError);
100 AttachmentId attachmentId = null;
101
102 return Collections.singletonList(new LinkPreview(currentState.activeUrlForError,
103 topLevelDomain != null ? topLevelDomain : currentState.activeUrlForError,
104 null,
105 -1L,
106 attachmentId));
107 } else {
108 return Collections.emptyList();
109 }
110 }
111
112 public void onTextChanged(@NonNull Context context, @NonNull String text, int cursorStart, int cursorEnd) {
113 if (!enabled && !enablePlaceholder) return;
114
115 debouncer.publish(() -> {
116 if (TextUtils.isEmpty(text)) {
117 userCanceled = false;
118 }
119
120 if (userCanceled) {
121 return;
122 }
123
124 Optional<Link> link = LinkPreviewUtil.findValidPreviewUrls(text)
125 .findFirst();
126
127 if (link.isPresent() && link.get().getUrl().equals(activeUrl)) {
128 return;
129 }
130
131 if (activeRequest != null) {
132 activeRequest.cancel();
133 activeRequest = null;
134 }
135
136 if (!link.isPresent() || !isCursorPositionValid(text, link.get(), cursorStart, cursorEnd)) {
137 activeUrl = null;
138 linkPreviewState.setValue(LinkPreviewState.forNoLinks());
139 return;
140 }
141
142 linkPreviewState.setValue(LinkPreviewState.forLoading());
143
144 activeUrl = link.get().getUrl();
145 activeRequest = enabled ? performRequest(activeUrl) : createPlaceholder(activeUrl);
146 });
147 }
148
149 public void onUserCancel() {
150 if (activeRequest != null) {
151 activeRequest.cancel();
152 activeRequest = null;
153 }
154
155 userCanceled = true;
156 activeUrl = null;
157
158 debouncer.clear();
159 linkPreviewState.setValue(LinkPreviewState.forNoLinks());
160 }
161
162 public void onTransportChanged(boolean isSms) {
163 enabled = SignalStore.settings().isLinkPreviewsEnabled() && !isSms;
164
165 if (!enabled) {
166 onUserCancel();
167 }
168 }
169
170 public void onEnabled() {
171 userCanceled = false;
172 enabled = SignalStore.settings().isLinkPreviewsEnabled();
173 }
174
175 @Override
176 protected void onCleared() {
177 if (activeRequest != null) {
178 activeRequest.cancel();
179 }
180
181 debouncer.clear();
182 }
183
184 private boolean isCursorPositionValid(@NonNull String text, @NonNull Link link, int cursorStart, int cursorEnd) {
185 if (cursorStart != cursorEnd) {
186 return true;
187 }
188
189 if (text.endsWith(link.getUrl()) && cursorStart == link.getPosition() + link.getUrl().length()) {
190 return true;
191 }
192
193 return cursorStart < link.getPosition() || cursorStart > link.getPosition() + link.getUrl().length();
194 }
195
196 private @Nullable RequestController createPlaceholder(String url) {
197 ThreadUtil.runOnMain(() -> {
198 if (!userCanceled) {
199 if (activeUrl != null && activeUrl.equals(url)) {
200 linkPreviewState.setValue(LinkPreviewState.forLinksWithNoPreview(url, LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE));
201 } else {
202 linkPreviewState.setValue(LinkPreviewState.forNoLinks());
203 }
204 }
205
206 activeRequest = null;
207 });
208
209 return null;
210 }
211
212 private @Nullable RequestController performRequest(String url) {
213 return repository.getLinkPreview(ApplicationDependencies.getApplication(), url, new LinkPreviewRepository.Callback() {
214 @Override
215 public void onSuccess(@NonNull LinkPreview linkPreview) {
216 ThreadUtil.runOnMain(() -> {
217 if (!userCanceled) {
218 if (activeUrl != null && activeUrl.equals(linkPreview.getUrl())) {
219 linkPreviewState.setValue(LinkPreviewState.forPreview(linkPreview));
220 } else {
221 linkPreviewState.setValue(LinkPreviewState.forNoLinks());
222 }
223 }
224 activeRequest = null;
225 });
226 }
227
228 @Override
229 public void onError(@NonNull LinkPreviewRepository.Error error) {
230 ThreadUtil.runOnMain(() -> {
231 if (!userCanceled) {
232 if (activeUrl != null) {
233 linkPreviewState.setValue(LinkPreviewState.forLinksWithNoPreview(activeUrl, error));
234 } else {
235 linkPreviewState.setValue(LinkPreviewState.forNoLinks());
236 }
237 }
238 activeRequest = null;
239 });
240 }
241 });
242 }
243
244 private @NonNull LinkPreviewState cleanseState(@NonNull LinkPreviewState state) {
245 if (enabled) {
246 return state;
247 }
248
249 if (enablePlaceholder) {
250 return state.linkPreview
251 .map(linkPreview -> LinkPreviewState.forLinksWithNoPreview(linkPreview.getUrl(), LinkPreviewRepository.Error.PREVIEW_NOT_AVAILABLE))
252 .orElse(state);
253 }
254
255 return LinkPreviewState.forNoLinks();
256 }
257
258 public static class Factory extends ViewModelProvider.NewInstanceFactory {
259
260 private final LinkPreviewRepository repository;
261 private final boolean enablePlaceholder;
262
263 public Factory(@NonNull LinkPreviewRepository repository) {
264 this(repository, false);
265 }
266
267 public Factory(@NonNull LinkPreviewRepository repository, boolean enablePlaceholder) {
268 this.repository = repository;
269 this.enablePlaceholder = enablePlaceholder;
270 }
271
272 @Override
273 public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
274 return modelClass.cast(new LinkPreviewViewModel(repository, enablePlaceholder));
275 }
276 }
277}