11package org .schabi .newpipe .info_list .holder ;
22
3+ import android .graphics .Paint ;
4+ import android .text .Layout ;
35import android .text .TextUtils ;
46import android .text .method .LinkMovementMethod ;
57import android .text .style .URLSpan ;
6- import android .text .util .Linkify ;
78import android .util .Log ;
89import android .view .View ;
910import android .view .ViewGroup ;
1011import android .widget .ImageView ;
1112import android .widget .RelativeLayout ;
1213import android .widget .TextView ;
1314
15+ import androidx .annotation .Nullable ;
1416import androidx .appcompat .app .AppCompatActivity ;
15- import androidx .core .text .util . LinkifyCompat ;
17+ import androidx .core .text .HtmlCompat ;
1618
1719import org .schabi .newpipe .R ;
1820import org .schabi .newpipe .error .ErrorUtil ;
1921import 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 ;
2025import org .schabi .newpipe .extractor .comments .CommentsInfoItem ;
26+ import org .schabi .newpipe .extractor .exceptions .ExtractionException ;
27+ import org .schabi .newpipe .extractor .stream .Description ;
2128import org .schabi .newpipe .info_list .InfoItemBuilder ;
2229import org .schabi .newpipe .local .history .HistoryRecordManager ;
23- import org .schabi .newpipe .util .text .CommentTextOnTouchListener ;
2430import org .schabi .newpipe .util .DeviceUtils ;
2531import org .schabi .newpipe .util .Localization ;
2632import org .schabi .newpipe .util .NavigationHelper ;
2733import org .schabi .newpipe .util .PicassoHelper ;
2834import 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
3342public 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}
0 commit comments