Skip to content

Commit cd12503

Browse files
authored
Merge pull request TeamNewPipe#9631 from TeamNewPipe/update-npe
Update NewPipeExtractor and properly linkify comments
2 parents 1e724eb + 6e73c48 commit cd12503

8 files changed

Lines changed: 301 additions & 216 deletions

File tree

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ dependencies {
187187
// name and the commit hash with the commit hash of the (pushed) commit you want to test
188188
// This works thanks to JitPack: https://jitpack.io/
189189
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
190-
implementation 'com.github.TeamNewPipe:NewPipeExtractor:2211a24b6934a8a8cdf5547ea1b52daa4cb5de6c'
190+
implementation 'com.github.TeamNewPipe:NewPipeExtractor:ff94e9f30bc5d7831734cc85ecebe7d30ac9c040'
191191
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
192192

193193
/** Checkstyle **/

app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
55
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
66
import static org.schabi.newpipe.util.Localization.getAppLocale;
7+
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
78

89
import android.os.Bundle;
910
import android.view.LayoutInflater;
@@ -112,7 +113,10 @@ private void enableDescriptionSelection() {
112113

113114
private void disableDescriptionSelection() {
114115
// show description content again, otherwise some links are not clickable
115-
loadDescriptionContent();
116+
TextLinkifier.fromDescription(binding.detailDescriptionView,
117+
streamInfo.getDescription(), HtmlCompat.FROM_HTML_MODE_LEGACY,
118+
streamInfo.getService(), streamInfo.getUrl(),
119+
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
116120

117121
binding.detailDescriptionNoteView.setVisibility(View.GONE);
118122
binding.detailDescriptionView.setTextIsSelectable(false);
@@ -123,27 +127,6 @@ private void disableDescriptionSelection() {
123127
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
124128
}
125129

126-
private void loadDescriptionContent() {
127-
final Description description = streamInfo.getDescription();
128-
switch (description.getType()) {
129-
case Description.HTML:
130-
TextLinkifier.createLinksFromHtmlBlock(binding.detailDescriptionView,
131-
description.getContent(), HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo,
132-
descriptionDisposables);
133-
break;
134-
case Description.MARKDOWN:
135-
TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView,
136-
description.getContent(), streamInfo, descriptionDisposables);
137-
break;
138-
case Description.PLAIN_TEXT:
139-
default:
140-
TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView,
141-
description.getContent(), streamInfo, descriptionDisposables);
142-
break;
143-
}
144-
}
145-
146-
147130
private void setupMetadata(final LayoutInflater inflater,
148131
final LinearLayout layout) {
149132
addMetadataItem(inflater, layout, false, R.string.metadata_category,
@@ -193,8 +176,8 @@ private void addMetadataItem(final LayoutInflater inflater,
193176
});
194177

195178
if (linkifyContent) {
196-
TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content,
197-
null, descriptionDisposables);
179+
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
180+
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
198181
} else {
199182
itemBinding.metadataContentView.setText(content);
200183
}

app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java

Lines changed: 94 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,66 @@
11
package org.schabi.newpipe.info_list.holder;
22

3+
import android.graphics.Paint;
4+
import android.text.Layout;
35
import android.text.TextUtils;
46
import android.text.method.LinkMovementMethod;
57
import android.text.style.URLSpan;
6-
import android.text.util.Linkify;
78
import android.util.Log;
89
import android.view.View;
910
import android.view.ViewGroup;
1011
import android.widget.ImageView;
1112
import android.widget.RelativeLayout;
1213
import android.widget.TextView;
1314

15+
import androidx.annotation.Nullable;
1416
import androidx.appcompat.app.AppCompatActivity;
15-
import androidx.core.text.util.LinkifyCompat;
17+
import androidx.core.text.HtmlCompat;
1618

1719
import org.schabi.newpipe.R;
1820
import org.schabi.newpipe.error.ErrorUtil;
1921
import org.schabi.newpipe.extractor.InfoItem;
22+
import org.schabi.newpipe.extractor.NewPipe;
23+
import org.schabi.newpipe.extractor.ServiceList;
24+
import org.schabi.newpipe.extractor.StreamingService;
2025
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
26+
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
27+
import org.schabi.newpipe.extractor.stream.Description;
2128
import org.schabi.newpipe.info_list.InfoItemBuilder;
2229
import org.schabi.newpipe.local.history.HistoryRecordManager;
23-
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
2430
import org.schabi.newpipe.util.DeviceUtils;
2531
import org.schabi.newpipe.util.Localization;
2632
import org.schabi.newpipe.util.NavigationHelper;
2733
import org.schabi.newpipe.util.PicassoHelper;
2834
import org.schabi.newpipe.util.external_communication.ShareUtils;
29-
import org.schabi.newpipe.util.text.TimestampExtractor;
35+
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
36+
import org.schabi.newpipe.util.text.TextLinkifier;
3037

31-
import java.util.Objects;
38+
import java.util.function.Consumer;
39+
40+
import io.reactivex.rxjava3.disposables.CompositeDisposable;
3241

3342
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
3443
private static final String TAG = "CommentsMiniIIHolder";
44+
private static final String ELLIPSIS = "…";
3545

3646
private static final int COMMENT_DEFAULT_LINES = 2;
3747
private static final int COMMENT_EXPANDED_LINES = 1000;
3848

3949
private final int commentHorizontalPadding;
4050
private final int commentVerticalPadding;
4151

52+
private final Paint paintAtContentSize;
53+
private final float ellipsisWidthPx;
54+
4255
private final RelativeLayout itemRoot;
4356
private final ImageView itemThumbnailView;
4457
private final TextView itemContentView;
4558
private final TextView itemLikesCountView;
4659
private final TextView itemPublishedTime;
4760

48-
private String commentText;
61+
private final CompositeDisposable disposables = new CompositeDisposable();
62+
private Description commentText;
63+
private StreamingService streamService;
4964
private String streamUrl;
5065

5166
CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, final int layoutId,
@@ -62,6 +77,10 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
6277
.getResources().getDimension(R.dimen.comments_horizontal_padding);
6378
commentVerticalPadding = (int) infoItemBuilder.getContext()
6479
.getResources().getDimension(R.dimen.comments_vertical_padding);
80+
81+
paintAtContentSize = new Paint();
82+
paintAtContentSize.setTextSize(itemContentView.getTextSize());
83+
ellipsisWidthPx = paintAtContentSize.measureText(ELLIPSIS);
6584
}
6685

6786
public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder,
@@ -91,18 +110,20 @@ public void updateFromItem(final InfoItem infoItem,
91110

92111
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
93112

113+
try {
114+
streamService = NewPipe.getService(item.getServiceId());
115+
} catch (final ExtractionException e) {
116+
// should never happen
117+
ErrorUtil.showUiErrorSnackbar(itemBuilder.getContext(), "Getting StreamingService", e);
118+
Log.w(TAG, "Cannot obtain service from comment service id, defaulting to YouTube", e);
119+
streamService = ServiceList.YouTube;
120+
}
94121
streamUrl = item.getUrl();
95-
96-
itemContentView.setLines(COMMENT_DEFAULT_LINES);
97122
commentText = item.getCommentText();
98-
itemContentView.setText(commentText, TextView.BufferType.SPANNABLE);
99-
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
123+
ellipsize();
100124

101-
if (itemContentView.getLineCount() == 0) {
102-
itemContentView.post(this::ellipsize);
103-
} else {
104-
ellipsize();
105-
}
125+
//noinspection ClickableViewAccessibility
126+
itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE);
106127

107128
if (item.getLikeCount() >= 0) {
108129
itemLikesCountView.setText(
@@ -132,7 +153,8 @@ public void updateFromItem(final InfoItem infoItem,
132153
if (DeviceUtils.isTv(itemBuilder.getContext())) {
133154
openCommentAuthor(item);
134155
} else {
135-
ShareUtils.copyToClipboard(itemBuilder.getContext(), commentText);
156+
ShareUtils.copyToClipboard(itemBuilder.getContext(),
157+
itemContentView.getText().toString());
136158
}
137159
return true;
138160
});
@@ -172,7 +194,7 @@ private boolean shouldFocusLinks() {
172194
return urls != null && urls.length != 0;
173195
}
174196

175-
private void determineLinkFocus() {
197+
private void determineMovementMethod() {
176198
if (shouldFocusLinks()) {
177199
allowLinkFocus();
178200
} else {
@@ -181,63 +203,73 @@ private void determineLinkFocus() {
181203
}
182204

183205
private void ellipsize() {
184-
boolean hasEllipsis = false;
185-
186-
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
187-
final int endOfLastLine = itemContentView
188-
.getLayout()
189-
.getLineEnd(COMMENT_DEFAULT_LINES - 1);
190-
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2);
191-
if (end == -1) {
192-
end = Math.max(endOfLastLine - 2, 0);
193-
}
194-
final String newVal = itemContentView.getText().subSequence(0, end) + " …";
195-
itemContentView.setText(newVal);
196-
hasEllipsis = true;
197-
}
206+
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
207+
linkifyCommentContentView(v -> {
208+
boolean hasEllipsis = false;
198209

199-
linkify();
210+
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
211+
// Note that converting to String removes spans (i.e. links), but that's something
212+
// we actually want since when the text is ellipsized we want all clicks on the
213+
// comment to expand the comment, not to open links.
214+
final String text = itemContentView.getText().toString();
215+
216+
final Layout layout = itemContentView.getLayout();
217+
final float lineWidth = layout.getLineWidth(COMMENT_DEFAULT_LINES - 1);
218+
final float layoutWidth = layout.getWidth();
219+
final int lineStart = layout.getLineStart(COMMENT_DEFAULT_LINES - 1);
220+
final int lineEnd = layout.getLineEnd(COMMENT_DEFAULT_LINES - 1);
221+
222+
// remove characters up until there is enough space for the ellipsis
223+
// (also summing 2 more pixels, just to be sure to avoid float rounding errors)
224+
int end = lineEnd;
225+
float removedCharactersWidth = 0.0f;
226+
while (lineWidth - removedCharactersWidth + ellipsisWidthPx + 2.0f > layoutWidth
227+
&& end >= lineStart) {
228+
end -= 1;
229+
// recalculate each time to account for ligatures or other similar things
230+
removedCharactersWidth = paintAtContentSize.measureText(
231+
text.substring(end, lineEnd));
232+
}
233+
234+
// remove trailing spaces and newlines
235+
while (end > 0 && Character.isWhitespace(text.charAt(end - 1))) {
236+
end -= 1;
237+
}
238+
239+
final String newVal = text.substring(0, end) + ELLIPSIS;
240+
itemContentView.setText(newVal);
241+
hasEllipsis = true;
242+
}
200243

201-
if (hasEllipsis) {
202-
denyLinkFocus();
203-
} else {
204-
determineLinkFocus();
205-
}
244+
itemContentView.setMaxLines(COMMENT_DEFAULT_LINES);
245+
if (hasEllipsis) {
246+
denyLinkFocus();
247+
} else {
248+
determineMovementMethod();
249+
}
250+
});
206251
}
207252

208253
private void toggleEllipsize() {
209-
if (itemContentView.getText().toString().equals(commentText)) {
210-
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
211-
ellipsize();
212-
}
213-
} else {
254+
final CharSequence text = itemContentView.getText();
255+
if (text.charAt(text.length() - 1) == ELLIPSIS.charAt(0)) {
214256
expand();
257+
} else if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
258+
ellipsize();
215259
}
216260
}
217261

218262
private void expand() {
219263
itemContentView.setMaxLines(COMMENT_EXPANDED_LINES);
220-
itemContentView.setText(commentText);
221-
linkify();
222-
determineLinkFocus();
264+
linkifyCommentContentView(v -> determineMovementMethod());
223265
}
224266

225-
private void linkify() {
226-
LinkifyCompat.addLinks(itemContentView, Linkify.WEB_URLS);
227-
LinkifyCompat.addLinks(itemContentView, TimestampExtractor.TIMESTAMPS_PATTERN, null, null,
228-
(match, url) -> {
229-
try {
230-
final var timestampMatch = TimestampExtractor
231-
.getTimestampFromMatcher(match, commentText);
232-
if (timestampMatch == null) {
233-
return url;
234-
}
235-
return streamUrl + url.replace(Objects.requireNonNull(match.group(0)),
236-
"#timestamp=" + timestampMatch.seconds());
237-
} catch (final Exception ex) {
238-
Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex);
239-
return url;
240-
}
241-
});
267+
private void linkifyCommentContentView(@Nullable final Consumer<TextView> onCompletion) {
268+
disposables.clear();
269+
if (commentText != null) {
270+
TextLinkifier.fromDescription(itemContentView, commentText,
271+
HtmlCompat.FROM_HTML_MODE_LEGACY, streamService, streamUrl, disposables,
272+
onCompletion);
273+
}
242274
}
243275
}

app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.schabi.newpipe.util;
2121

2222
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
23+
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
2324

2425
import android.content.Context;
2526
import android.util.Log;
@@ -319,8 +320,9 @@ public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInf
319320
}
320321

321322
metaInfoSeparator.setVisibility(View.VISIBLE);
322-
TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView, stringBuilder.toString(),
323-
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, disposables);
323+
TextLinkifier.fromHtml(metaInfoTextView, stringBuilder.toString(),
324+
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, null, disposables,
325+
SET_LINK_MOVEMENT_METHOD);
324326
}
325327
}
326328

app/src/main/java/org/schabi/newpipe/util/text/CommentTextOnTouchListener.java

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,37 @@
22

33
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
44

5-
import android.text.Selection;
6-
import android.text.Spannable;
5+
import android.annotation.SuppressLint;
76
import android.text.Spanned;
87
import android.text.style.ClickableSpan;
9-
import android.text.style.URLSpan;
108
import android.view.MotionEvent;
119
import android.view.View;
1210
import android.widget.TextView;
1311

14-
import org.schabi.newpipe.util.external_communication.ShareUtils;
15-
16-
import io.reactivex.rxjava3.disposables.CompositeDisposable;
17-
1812
public class CommentTextOnTouchListener implements View.OnTouchListener {
1913
public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener();
2014

15+
@SuppressLint("ClickableViewAccessibility")
2116
@Override
2217
public boolean onTouch(final View v, final MotionEvent event) {
2318
if (!(v instanceof TextView)) {
2419
return false;
2520
}
2621
final TextView widget = (TextView) v;
27-
final Object text = widget.getText();
22+
final CharSequence text = widget.getText();
2823
if (text instanceof Spanned) {
29-
final Spannable buffer = (Spannable) text;
30-
24+
final Spanned buffer = (Spanned) text;
3125
final int action = event.getAction();
3226

3327
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
3428
final int offset = getOffsetForHorizontalLine(widget, event);
35-
final ClickableSpan[] link = buffer.getSpans(offset, offset, ClickableSpan.class);
29+
final ClickableSpan[] links = buffer.getSpans(offset, offset, ClickableSpan.class);
3630

37-
if (link.length != 0) {
31+
if (links.length != 0) {
3832
if (action == MotionEvent.ACTION_UP) {
39-
if (link[0] instanceof URLSpan) {
40-
final String url = ((URLSpan) link[0]).getURL();
41-
if (!InternalUrlsHandler.handleUrlCommentsTimestamp(
42-
new CompositeDisposable(), v.getContext(), url)) {
43-
ShareUtils.openUrlInBrowser(v.getContext(), url, false);
44-
}
45-
}
46-
} else if (action == MotionEvent.ACTION_DOWN) {
47-
Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
48-
buffer.getSpanEnd(link[0]));
33+
links[0].onClick(widget);
4934
}
35+
// we handle events that intersect links, so return true
5036
return true;
5137
}
5238
}

0 commit comments

Comments
 (0)