Skip to content

Commit b2c6a8c

Browse files
committed
fix: support RTL usernames in comment header
1 parent 276bf39 commit b2c6a8c

4 files changed

Lines changed: 82 additions & 55 deletions

File tree

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import androidx.annotation.NonNull;
1919
import androidx.fragment.app.FragmentActivity;
20+
import androidx.core.text.BidiFormatter;
2021

2122
import org.schabi.newpipe.R;
2223
import org.schabi.newpipe.extractor.InfoItem;
@@ -51,7 +52,7 @@ public class CommentInfoItemHolder extends InfoItemHolder {
5152
private final TextEllipsizer textEllipsizer;
5253

5354
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
54-
final ViewGroup parent) {
55+
final ViewGroup parent) {
5556
super(infoItemBuilder, R.layout.list_comment_item, parent);
5657

5758
itemRoot = itemView.findViewById(R.id.itemRoot);
@@ -81,13 +82,12 @@ public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
8182

8283
@Override
8384
public void updateFromItem(final InfoItem infoItem,
84-
final HistoryRecordManager historyRecordManager) {
85+
final HistoryRecordManager historyRecordManager) {
8586
if (!(infoItem instanceof CommentsInfoItem)) {
8687
return;
8788
}
8889
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
8990

90-
9191
// load the author avatar
9292
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
9393
if (ImageStrategy.shouldLoadImages()) {
@@ -101,14 +101,16 @@ public void updateFromItem(final InfoItem infoItem,
101101
}
102102
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
103103

104-
105104
// setup the top row, with pinned icon, author name and comment date
106105
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
107-
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(),
108-
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(),
106+
final String uploaderName = BidiFormatter.getInstance().unicodeWrap(item.getUploaderName());
107+
itemTitleView.setText(Localization.concatenateStrings(
108+
uploaderName,
109+
Localization.relativeTimeOrTextual(
110+
itemBuilder.getContext(),
111+
item.getUploadDate(),
109112
item.getTextualUploadDate())));
110113

111-
112114
// setup bottom row, with likes, heart and replies button
113115
itemLikesCountView.setText(
114116
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
@@ -119,18 +121,19 @@ public void updateFromItem(final InfoItem infoItem,
119121
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
120122
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
121123
repliesButton.setText(hasReplies
122-
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
124+
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount())
125+
: "");
123126
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
124-
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
125-
127+
hasReplies ? 0
128+
: DeviceUtils.dpToPx(6, itemBuilder.getContext());
126129

127130
// setup comment content and click listeners to expand/ellipsize it
128131
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
129132
textEllipsizer.setStreamUrl(item.getUrl());
130133
textEllipsizer.setContent(item.getCommentText());
131134
textEllipsizer.ellipsize();
132135

133-
//noinspection ClickableViewAccessibility
136+
// noinspection ClickableViewAccessibility
134137
itemContentView.setOnTouchListener((v, event) -> {
135138
final CharSequence text = itemContentView.getText();
136139
if (text instanceof Spanned buffer) {

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

Lines changed: 66 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.os.Build;
1212
import android.text.TextUtils;
1313
import android.text.format.DateUtils;
14+
import android.text.BidiFormatter;
1415
import android.util.DisplayMetrics;
1516
import android.util.Log;
1617

@@ -45,7 +46,6 @@
4546
import java.util.Objects;
4647
import java.util.stream.Collectors;
4748

48-
4949
/*
5050
* Created by chschtsch on 12/29/15.
5151
*
@@ -71,17 +71,21 @@ public final class Localization {
7171
public static final String DOT_SEPARATOR = " • ";
7272
private static PrettyTime prettyTime;
7373

74-
private Localization() { }
74+
private Localization() {
75+
}
7576

7677
@NonNull
7778
public static String concatenateStrings(final String... strings) {
7879
return concatenateStrings(DOT_SEPARATOR, Arrays.asList(strings));
7980
}
8081

82+
// Use of BidiFormater to fix text direction automatically
8183
@NonNull
8284
public static String concatenateStrings(final String delimiter, final List<String> strings) {
85+
final BidiFormatter bidi = BidiFormatter.getInstance();
8386
return strings.stream()
8487
.filter(string -> !TextUtils.isEmpty(string))
88+
.map((s) -> strings.indexOf(s) == 0 ? bidi.unicodeWrap(s) : s) // only wrap username
8589
.collect(Collectors.joining(delimiter));
8690
}
8791

@@ -123,15 +127,15 @@ public static String localizeNumber(@NonNull final Context context, final double
123127
}
124128

125129
public static String formatDate(@NonNull final Context context,
126-
@NonNull final OffsetDateTime offsetDateTime) {
130+
@NonNull final OffsetDateTime offsetDateTime) {
127131
return DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
128132
.withLocale(getAppLocale(context)).format(offsetDateTime
129133
.atZoneSameInstant(ZoneId.systemDefault()));
130134
}
131135

132136
@SuppressLint("StringFormatInvalid")
133137
public static String localizeUploadDate(@NonNull final Context context,
134-
@NonNull final OffsetDateTime offsetDateTime) {
138+
@NonNull final OffsetDateTime offsetDateTime) {
135139
return context.getString(R.string.upload_date_text, formatDate(context, offsetDateTime));
136140
}
137141

@@ -141,7 +145,7 @@ public static String localizeViewCount(@NonNull final Context context, final lon
141145
}
142146

143147
public static String localizeStreamCount(@NonNull final Context context,
144-
final long streamCount) {
148+
final long streamCount) {
145149
switch ((int) streamCount) {
146150
case (int) ListExtractor.ITEM_COUNT_UNKNOWN:
147151
return "";
@@ -156,7 +160,7 @@ public static String localizeStreamCount(@NonNull final Context context,
156160
}
157161

158162
public static String localizeStreamCountMini(@NonNull final Context context,
159-
final long streamCount) {
163+
final long streamCount) {
160164
switch ((int) streamCount) {
161165
case (int) ListExtractor.ITEM_COUNT_UNKNOWN:
162166
return "";
@@ -170,7 +174,7 @@ public static String localizeStreamCountMini(@NonNull final Context context,
170174
}
171175

172176
public static String localizeWatchingCount(@NonNull final Context context,
173-
final long watchingCount) {
177+
final long watchingCount) {
174178
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
175179
localizeNumber(context, watchingCount));
176180
}
@@ -202,7 +206,7 @@ public static String listeningCount(@NonNull final Context context, final long l
202206
}
203207

204208
public static String shortWatchingCount(@NonNull final Context context,
205-
final long watchingCount) {
209+
final long watchingCount) {
206210
return getQuantity(context, R.plurals.watching, R.string.no_one_watching, watchingCount,
207211
shortCount(context, watchingCount));
208212
}
@@ -213,7 +217,7 @@ public static String shortViewCount(@NonNull final Context context, final long v
213217
}
214218

215219
public static String shortSubscriberCount(@NonNull final Context context,
216-
final long subscriberCount) {
220+
final long subscriberCount) {
217221
return getQuantity(context, R.plurals.subscribers, R.string.no_subscribers, subscriberCount,
218222
shortCount(context, subscriberCount));
219223
}
@@ -224,7 +228,7 @@ public static String downloadCount(@NonNull final Context context, final int dow
224228
}
225229

226230
public static String deletedDownloadCount(@NonNull final Context context,
227-
final int deletedCount) {
231+
final int deletedCount) {
228232
return getQuantity(context, R.plurals.deleted_downloads_toast, 0,
229233
deletedCount, shortCount(context, deletedCount));
230234
}
@@ -235,10 +239,12 @@ public static String replyCount(@NonNull final Context context, final int replyC
235239
}
236240

237241
/**
238-
* @param context the Android context
242+
* @param context the Android context
239243
* @param likeCount the like count, possibly negative if unknown
240-
* @return if {@code likeCount} is smaller than {@code 0}, the string {@code "-"}, otherwise
241-
* the result of calling {@link #shortCount(Context, long)} on the like count
244+
* @return if {@code likeCount} is smaller than {@code 0}, the string
245+
* {@code "-"}, otherwise
246+
* the result of calling {@link #shortCount(Context, long)} on the like
247+
* count
242248
*/
243249
public static String likeCount(@NonNull final Context context, final int likeCount) {
244250
if (likeCount < 0) {
@@ -249,7 +255,8 @@ public static String likeCount(@NonNull final Context context, final int likeCou
249255
}
250256

251257
/**
252-
* Get a readable text for a duration in the format {@code hours:minutes:seconds}.
258+
* Get a readable text for a duration in the format
259+
* {@code hours:minutes:seconds}.
253260
*
254261
* @param duration the duration in seconds
255262
* @return a formatted duration String or {@code 00:00} if the duration is zero.
@@ -259,16 +266,18 @@ public static String getDurationString(final long duration) {
259266
}
260267

261268
/**
262-
* Get a readable text for a duration in the format {@code hours:minutes:seconds+}. If the given
269+
* Get a readable text for a duration in the format
270+
* {@code hours:minutes:seconds+}. If the given
263271
* duration is incomplete, a plus is appended to the duration string.
264272
*
265-
* @param duration the duration in seconds
266-
* @param isDurationComplete whether the given duration is complete or whether info is missing
273+
* @param duration the duration in seconds
274+
* @param isDurationComplete whether the given duration is complete or whether
275+
* info is missing
267276
* @param showDurationPrefix whether the duration-prefix shall be shown
268277
* @return a formatted duration String or {@code 00:00} if the duration is zero.
269278
*/
270279
public static String getDurationString(final long duration, final boolean isDurationComplete,
271-
final boolean showDurationPrefix) {
280+
final boolean showDurationPrefix) {
272281
final String output = getDurationString(duration);
273282
final String durationPrefix = showDurationPrefix ? "⏱ " : "";
274283
final String durationPostfix = isDurationComplete ? "" : "+";
@@ -278,16 +287,19 @@ public static String getDurationString(final long duration, final boolean isDura
278287
/**
279288
* Localize an amount of seconds into a human readable string.
280289
*
281-
* <p>The seconds will be converted to the closest whole time unit.
282-
* <p>For example, 60 seconds would give "1 minute", 119 would also give "1 minute".
290+
* <p>
291+
* The seconds will be converted to the closest whole time unit.
292+
* <p>
293+
* For example, 60 seconds would give "1 minute", 119 would also give "1
294+
* minute".
283295
*
284296
* @param context used to get plurals resources.
285297
* @param durationInSecs an amount of seconds.
286298
* @return duration in a human readable string.
287299
*/
288300
@NonNull
289301
public static String localizeDuration(@NonNull final Context context,
290-
final int durationInSecs) {
302+
final int durationInSecs) {
291303
if (durationInSecs < 0) {
292304
throw new IllegalArgumentException("duration can not be negative");
293305
}
@@ -313,11 +325,13 @@ public static String localizeDuration(@NonNull final Context context,
313325
/**
314326
* Get the localized name of an audio track.
315327
*
316-
* <p>Examples of results returned by this method:</p>
328+
* <p>
329+
* Examples of results returned by this method:
330+
* </p>
317331
* <ul>
318-
* <li>English (original)</li>
319-
* <li>English (descriptive)</li>
320-
* <li>Spanish (Spain) (dubbed)</li>
332+
* <li>English (original)</li>
333+
* <li>English (descriptive)</li>
334+
* <li>Spanish (Spain) (dubbed)</li>
321335
* </ul>
322336
*
323337
* @param context the context used to get the app language
@@ -343,7 +357,7 @@ public static String audioTrackName(@NonNull final Context context, final AudioS
343357

344358
@NonNull
345359
private static String audioTrackType(@NonNull final Context context,
346-
@NonNull final AudioTrackType trackType) {
360+
@NonNull final AudioTrackType trackType) {
347361
return switch (trackType) {
348362
case ORIGINAL -> context.getString(R.string.audio_track_type_original);
349363
case DUBBED -> context.getString(R.string.audio_track_type_dubbed);
@@ -352,9 +366,11 @@ private static String audioTrackType(@NonNull final Context context,
352366
};
353367
}
354368

355-
/*//////////////////////////////////////////////////////////////////////////
356-
// Pretty Time
357-
//////////////////////////////////////////////////////////////////////////*/
369+
/*
370+
* //////////////////////////////////////////////////////////////////////////
371+
* // Pretty Time
372+
* //////////////////////////////////////////////////////////////////////////
373+
*/
358374

359375
public static void initPrettyTime(@NonNull final PrettyTime time) {
360376
prettyTime = time;
@@ -371,19 +387,26 @@ public static String relativeTime(@NonNull final OffsetDateTime offsetDateTime)
371387
}
372388

373389
/**
374-
* @param context the Android context; if {@code null} then even if in debug mode and the
375-
* setting is enabled, {@code textual} will not be shown next to {@code parsed}
376-
* @param parsed the textual date or time ago parsed by NewPipeExtractor, or {@code null} if
390+
* @param context the Android context; if {@code null} then even if in debug
391+
* mode and the
392+
* setting is enabled, {@code textual} will not be shown next to
393+
* {@code parsed}
394+
* @param parsed the textual date or time ago parsed by NewPipeExtractor, or
395+
* {@code null} if
377396
* the extractor could not parse it
378-
* @param textual the original textual date or time ago string as provided by services
379-
* @return {@link #relativeTime(OffsetDateTime)} is used if {@code parsed != null}, otherwise
380-
* {@code textual} is returned. If in debug mode, {@code context != null},
381-
* {@code parsed != null} and the relevant setting is enabled, {@code textual} will
397+
* @param textual the original textual date or time ago string as provided by
398+
* services
399+
* @return {@link #relativeTime(OffsetDateTime)} is used if
400+
* {@code parsed != null}, otherwise
401+
* {@code textual} is returned. If in debug mode,
402+
* {@code context != null},
403+
* {@code parsed != null} and the relevant setting is enabled,
404+
* {@code textual} will
382405
* be appended to the returned string for debugging purposes.
383406
*/
384407
public static String relativeTimeOrTextual(@Nullable final Context context,
385-
@Nullable final DateWrapper parsed,
386-
final String textual) {
408+
@Nullable final DateWrapper parsed,
409+
final String textual) {
387410
if (parsed == null) {
388411
return textual;
389412
} else if (DEBUG && context != null && PreferenceManager
@@ -404,7 +427,7 @@ public static void assureCorrectAppLanguage(final Context c) {
404427
}
405428

406429
private static Locale getLocaleFromPrefs(@NonNull final Context context,
407-
@StringRes final int prefKey) {
430+
@StringRes final int prefKey) {
408431
final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
409432
final String defaultKey = context.getString(R.string.default_localization_key);
410433
final String languageCode = sp.getString(context.getString(prefKey), defaultKey);
@@ -421,10 +444,10 @@ private static double round(final double value) {
421444
}
422445

423446
private static String getQuantity(@NonNull final Context context,
424-
@PluralsRes final int pluralId,
425-
@StringRes final int zeroCaseStringId,
426-
final long count,
427-
final String formattedCount) {
447+
@PluralsRes final int pluralId,
448+
@StringRes final int zeroCaseStringId,
449+
final long count,
450+
final String formattedCount) {
428451
if (count == 0) {
429452
return context.getString(zeroCaseStringId);
430453
}

app/src/main/res/layout/comment_replies_header.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,4 @@
134134
app:layout_constraintStart_toStartOf="parent"
135135
app:layout_constraintTop_toBottomOf="@+id/commentContent" />
136136

137-
</androidx.constraintlayout.widget.ConstraintLayout>
137+
</androidx.constraintlayout.widget.ConstraintLayout>

app/src/main/res/layout/list_comment_item.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
android:textSize="@dimen/comment_item_title_text_size"
4545
tools:text="Author Name, Lorem ipsum • 5 months ago" />
4646

47+
4748
<org.schabi.newpipe.views.NewPipeTextView
4849
android:id="@+id/itemCommentContentView"
4950
android:layout_width="match_parent"

0 commit comments

Comments
 (0)