1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2024-12-23 16:40:32 +00:00

Migrate comments fragment to Jetpack Compose

This commit is contained in:
Isira Seneviratne 2024-06-21 16:05:45 +05:30
parent be037e0756
commit 9c52e039ee
13 changed files with 161 additions and 605 deletions

View File

@ -882,7 +882,7 @@ public final class VideoDetailFragment
if (shouldShowComments()) {
pageAdapter.addFragment(
CommentsFragment.getInstance(serviceId, url, title), COMMENTS_TAB_TAG);
CommentsFragment.getInstance(serviceId, url), COMMENTS_TAB_TAG);
tabIcons.add(R.drawable.ic_comment);
tabContentDescriptions.add(R.string.comments_tab_description);
}
@ -1014,16 +1014,15 @@ public final class VideoDetailFragment
public void scrollToComment(final CommentsInfoItem comment) {
final int commentsTabPos = pageAdapter.getItemPositionByTitle(COMMENTS_TAB_TAG);
final Fragment fragment = pageAdapter.getItem(commentsTabPos);
if (!(fragment instanceof CommentsFragment)) {
return;
}
final var fragment = pageAdapter.getItem(commentsTabPos);
// TODO: Implement the scrolling with Compose.
// unexpand the app bar only if scrolling to the comment succeeded
if (((CommentsFragment) fragment).scrollToComment(comment)) {
binding.appBarLayout.setExpanded(false, false);
binding.viewPager.setCurrentItem(commentsTabPos, false);
}
// if (fragment instanceof CommentsFragment commentsFragment &&
// commentsFragment.scrollToComment(comment)) {
// binding.appBarLayout.setExpanded(false, false);
// binding.viewPager.setCurrentItem(commentsTabPos, false);
// }
}
/*//////////////////////////////////////////////////////////////////////////

View File

@ -83,8 +83,7 @@ fun Comment(comment: CommentsInfoItem) {
.clip(CircleShape)
.clickable {
NavigationHelper.openCommentAuthorIfPresent(
context as FragmentActivity,
comment
context as FragmentActivity, comment
)
}
)
@ -140,7 +139,11 @@ fun Comment(comment: CommentsInfoItem) {
}
if (comment.replies != null) {
TextButton(onClick = { /*TODO*/ }) {
TextButton(onClick = {
NavigationHelper.openCommentRepliesFragment(
context as FragmentActivity, comment
)
}) {
Text(
text = pluralStringResource(
R.plurals.replies, comment.replyCount, comment.replyCount.toString()

View File

@ -31,17 +31,17 @@ class CommentRepliesFragment : Fragment() {
bar.setDisplayShowTitleEnabled(true)
bar.title = Localization.replyCount(activity, comment.replyCount)
return ComposeView(requireContext()).apply {
return ComposeView(activity).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val flow = remember(comment) {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
CommentRepliesSource(comment)
CommentsSource(comment.serviceId, comment.url, comment.replies)
}.flow
}
AppTheme {
CommentReplies(comment = comment, flow = flow)
CommentSection(parentComment = comment, flow = flow)
}
}
}

View File

@ -1,22 +0,0 @@
package org.schabi.newpipe.fragments.list.comments
import androidx.paging.PagingState
import androidx.paging.rxjava3.RxPagingSource
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.extractor.Page
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.util.ExtractorHelper
class CommentRepliesSource(
private val commentsInfoItem: CommentsInfoItem,
) : RxPagingSource<Page, CommentsInfoItem>() {
override fun loadSingle(params: LoadParams<Page>): Single<LoadResult<Page, CommentsInfoItem>> {
val nextPage = params.key ?: commentsInfoItem.replies
return ExtractorHelper.getMoreCommentItems(commentsInfoItem.serviceId, commentsInfoItem.url, nextPage)
.subscribeOn(Schedulers.io())
.map { LoadResult.Page(it.items, null, it.nextPage) }
}
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
}

View File

@ -15,16 +15,18 @@ import org.schabi.newpipe.extractor.stream.Description
import org.schabi.newpipe.ui.theme.AppTheme
@Composable
fun CommentReplies(
comment: CommentsInfoItem,
flow: Flow<PagingData<CommentsInfoItem>>
fun CommentSection(
flow: Flow<PagingData<CommentsInfoItem>>,
parentComment: CommentsInfoItem? = null,
) {
val replies = flow.collectAsLazyPagingItems()
LazyColumn {
item {
CommentRepliesHeader(comment = comment)
HorizontalDivider(thickness = 1.dp)
if (parentComment != null) {
item {
CommentRepliesHeader(comment = parentComment)
HorizontalDivider(thickness = 1.dp)
}
}
items(replies.itemCount) {
@ -33,6 +35,25 @@ fun CommentReplies(
}
}
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun CommentSectionPreview() {
val comment1 = CommentsInfoItem(
commentText = Description("This is a comment", Description.PLAIN_TEXT),
uploaderName = "Test",
)
val comment2 = CommentsInfoItem(
commentText = Description("This is another comment.<br>This is another line.", Description.HTML),
uploaderName = "Test 2",
)
val flow = flowOf(PagingData.from(listOf(comment1, comment2)))
AppTheme {
CommentSection(flow = flow)
}
}
@Preview(name = "Light mode", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -56,6 +77,6 @@ private fun CommentRepliesPreview() {
val flow = flowOf(PagingData.from(listOf(reply1, reply2)))
AppTheme {
CommentReplies(comment = comment, flow = flow)
CommentSection(parentComment = comment, flow = flow)
}
}

View File

@ -1,123 +1,55 @@
package org.schabi.newpipe.fragments.list.comments;
package org.schabi.newpipe.fragments.list.comments
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.paging.Pager
import androidx.paging.PagingConfig
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.NO_SERVICE_ID
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
class CommentsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val arguments = requireArguments()
val serviceId = arguments.getInt(SERVICE_ID, NO_SERVICE_ID)
val url = arguments.getString(URL)
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.comments.CommentsInfo;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.info_list.ItemViewMode;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.util.ExtractorHelper;
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val flow = remember(serviceId, url) {
Pager(PagingConfig(pageSize = 20, enablePlaceholders = false)) {
CommentsSource(serviceId, url, null)
}.flow
}
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class CommentsFragment extends BaseListInfoFragment<CommentsInfoItem, CommentsInfo> {
private final CompositeDisposable disposables = new CompositeDisposable();
private TextView emptyStateDesc;
public static CommentsFragment getInstance(final int serviceId, final String url,
final String name) {
final CommentsFragment instance = new CommentsFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}
public CommentsFragment() {
super(UserAction.REQUESTED_COMMENTS);
}
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
emptyStateDesc = rootView.findViewById(R.id.empty_state_desc);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_comments, container, false);
}
@Override
public void onDestroy() {
super.onDestroy();
disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.InfoItemsPage<CommentsInfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
}
@Override
protected Single<CommentsInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void handleResult(@NonNull final CommentsInfo result) {
super.handleResult(result);
emptyStateDesc.setText(
result.isCommentsDisabled()
? R.string.comments_are_disabled
: R.string.no_comments);
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(final String title) { }
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) { }
@Override
protected ItemViewMode getItemViewMode() {
return ItemViewMode.LIST;
}
public boolean scrollToComment(final CommentsInfoItem comment) {
final int position = infoListAdapter.getItemsList().indexOf(comment);
if (position < 0) {
return false;
AppTheme {
CommentSection(flow = flow)
}
}
}
}
itemsList.scrollToPosition(position);
return true;
companion object {
private const val SERVICE_ID = "serviceId"
private const val URL = "url"
@JvmStatic
fun getInstance(serviceId: Int, url: String?) = CommentsFragment().apply {
arguments = bundleOf(
SERVICE_ID to serviceId,
URL to url
)
}
}
}

View File

@ -0,0 +1,36 @@
package org.schabi.newpipe.fragments.list.comments
import androidx.paging.PagingState
import androidx.paging.rxjava3.RxPagingSource
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.extractor.Page
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.util.ExtractorHelper
class CommentsSource(
private val serviceId: Int,
private val url: String?,
private val repliesPage: Page?
) : RxPagingSource<Page, CommentsInfoItem>() {
override fun loadSingle(params: LoadParams<Page>): Single<LoadResult<Page, CommentsInfoItem>> {
// repliesPage is non-null only when used to load the comment replies
val nextKey = params.key ?: repliesPage
return nextKey?.let {
ExtractorHelper.getMoreCommentItems(serviceId, url, it)
.subscribeOn(Schedulers.io())
.map { LoadResult.Page(it.items, null, it.nextPage) }
} ?: ExtractorHelper.getCommentsInfo(serviceId, url, false)
.subscribeOn(Schedulers.io())
.map {
if (it.isCommentsDisabled) {
LoadResult.Invalid()
} else {
LoadResult.Page(it.relatedItems, null, it.nextPage)
}
}
}
override fun getRefreshKey(state: PagingState<Page, CommentsInfoItem>) = null
}

View File

@ -13,7 +13,6 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
@ -75,21 +74,16 @@ public class InfoItemBuilder {
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
@NonNull final InfoItem.InfoType infoType,
final boolean useMiniVariant) {
switch (infoType) {
case STREAM:
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
: new StreamInfoItemHolder(this, parent);
case CHANNEL:
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
: new ChannelInfoItemHolder(this, parent);
case PLAYLIST:
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
: new PlaylistInfoItemHolder(this, parent);
case COMMENT:
return new CommentInfoItemHolder(this, parent);
default:
throw new RuntimeException("InfoType not expected = " + infoType.name());
}
return switch (infoType) {
case STREAM -> useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
: new StreamInfoItemHolder(this, parent);
case CHANNEL -> useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
: new ChannelInfoItemHolder(this, parent);
case PLAYLIST -> useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
: new PlaylistInfoItemHolder(this, parent);
case COMMENT ->
throw new IllegalArgumentException("Comments should be rendered using Compose");
};
}
public Context getContext() {

View File

@ -21,7 +21,6 @@ import org.schabi.newpipe.info_list.holder.ChannelCardInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelGridInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistCardInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistGridInfoItemHolder;
@ -283,46 +282,32 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
Log.d(TAG, "onCreateViewHolder() called with: "
+ "parent = [" + parent + "], type = [" + type + "]");
}
switch (type) {
return switch (type) {
// #4475 and #3368
// Always create a new instance otherwise the same instance
// is sometimes reused which causes a crash
case HEADER_TYPE:
return new HFHolder(headerSupplier.get());
case FOOTER_TYPE:
return new HFHolder(PignateFooterBinding
.inflate(layoutInflater, parent, false)
.getRoot()
);
case MINI_STREAM_HOLDER_TYPE:
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
case STREAM_HOLDER_TYPE:
return new StreamInfoItemHolder(infoItemBuilder, parent);
case GRID_STREAM_HOLDER_TYPE:
return new StreamGridInfoItemHolder(infoItemBuilder, parent);
case CARD_STREAM_HOLDER_TYPE:
return new StreamCardInfoItemHolder(infoItemBuilder, parent);
case MINI_CHANNEL_HOLDER_TYPE:
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
case CHANNEL_HOLDER_TYPE:
return new ChannelInfoItemHolder(infoItemBuilder, parent);
case CARD_CHANNEL_HOLDER_TYPE:
return new ChannelCardInfoItemHolder(infoItemBuilder, parent);
case GRID_CHANNEL_HOLDER_TYPE:
return new ChannelGridInfoItemHolder(infoItemBuilder, parent);
case MINI_PLAYLIST_HOLDER_TYPE:
return new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
case PLAYLIST_HOLDER_TYPE:
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
case GRID_PLAYLIST_HOLDER_TYPE:
return new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
case CARD_PLAYLIST_HOLDER_TYPE:
return new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
case COMMENT_HOLDER_TYPE:
return new CommentInfoItemHolder(infoItemBuilder, parent);
default:
return new FallbackViewHolder(new View(parent.getContext()));
}
case HEADER_TYPE -> new HFHolder(headerSupplier.get());
case FOOTER_TYPE -> new HFHolder(PignateFooterBinding
.inflate(layoutInflater, parent, false)
.getRoot()
);
case MINI_STREAM_HOLDER_TYPE -> new StreamMiniInfoItemHolder(infoItemBuilder, parent);
case STREAM_HOLDER_TYPE -> new StreamInfoItemHolder(infoItemBuilder, parent);
case GRID_STREAM_HOLDER_TYPE -> new StreamGridInfoItemHolder(infoItemBuilder, parent);
case CARD_STREAM_HOLDER_TYPE -> new StreamCardInfoItemHolder(infoItemBuilder, parent);
case MINI_CHANNEL_HOLDER_TYPE -> new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
case CHANNEL_HOLDER_TYPE -> new ChannelInfoItemHolder(infoItemBuilder, parent);
case CARD_CHANNEL_HOLDER_TYPE -> new ChannelCardInfoItemHolder(infoItemBuilder, parent);
case GRID_CHANNEL_HOLDER_TYPE -> new ChannelGridInfoItemHolder(infoItemBuilder, parent);
case MINI_PLAYLIST_HOLDER_TYPE ->
new PlaylistMiniInfoItemHolder(infoItemBuilder, parent);
case PLAYLIST_HOLDER_TYPE -> new PlaylistInfoItemHolder(infoItemBuilder, parent);
case GRID_PLAYLIST_HOLDER_TYPE ->
new PlaylistGridInfoItemHolder(infoItemBuilder, parent);
case CARD_PLAYLIST_HOLDER_TYPE ->
new PlaylistCardInfoItemHolder(infoItemBuilder, parent);
default -> new FallbackViewHolder(new View(parent.getContext()));
};
}
@Override

View File

@ -1,208 +0,0 @@
package org.schabi.newpipe.info_list.holder;
import static org.schabi.newpipe.util.ServiceHelper.getServiceById;
import static org.schabi.newpipe.util.text.TouchUtils.getOffsetForHorizontalLine;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.text.TextEllipsizer;
public class CommentInfoItemHolder extends InfoItemHolder {
private static final int COMMENT_DEFAULT_LINES = 2;
private final int commentHorizontalPadding;
private final int commentVerticalPadding;
private final RelativeLayout itemRoot;
private final ImageView itemThumbnailView;
private final TextView itemContentView;
private final ImageView itemThumbsUpView;
private final TextView itemLikesCountView;
private final TextView itemTitleView;
private final ImageView itemHeartView;
private final ImageView itemPinnedView;
private final Button repliesButton;
@NonNull
private final TextEllipsizer textEllipsizer;
public CommentInfoItemHolder(final InfoItemBuilder infoItemBuilder,
final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_comment_item, parent);
itemRoot = itemView.findViewById(R.id.itemRoot);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
itemThumbsUpView = itemView.findViewById(R.id.detail_thumbs_up_img_view);
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
itemPinnedView = itemView.findViewById(R.id.detail_pinned_view);
repliesButton = itemView.findViewById(R.id.replies_button);
commentHorizontalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_horizontal_padding);
commentVerticalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_vertical_padding);
textEllipsizer = new TextEllipsizer(itemContentView, COMMENT_DEFAULT_LINES, null);
textEllipsizer.setStateChangeListener(isEllipsized -> {
if (Boolean.TRUE.equals(isEllipsized)) {
denyLinkFocus();
} else {
determineMovementMethod();
}
});
}
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem item)) {
return;
}
// load the author avatar
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getUploaderAvatars());
if (ImageStrategy.shouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
commentVerticalPadding, commentVerticalPadding);
} else {
itemThumbnailView.setVisibility(View.GONE);
itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding,
commentHorizontalPadding, commentVerticalPadding);
}
itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item));
// setup the top row, with pinned icon, author name and comment date
itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE);
itemTitleView.setText(Localization.concatenateStrings(item.getUploaderName(),
Localization.relativeTimeOrTextual(itemBuilder.getContext(), item.getUploadDate(),
item.getTextualUploadDate())));
// setup bottom row, with likes, heart and replies button
itemLikesCountView.setText(
Localization.likeCount(itemBuilder.getContext(), item.getLikeCount()));
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
final boolean hasReplies = item.getReplies() != null;
repliesButton.setOnClickListener(hasReplies ? v -> openCommentReplies(item) : null);
repliesButton.setVisibility(hasReplies ? View.VISIBLE : View.GONE);
repliesButton.setText(hasReplies
? Localization.replyCount(itemBuilder.getContext(), item.getReplyCount()) : "");
((RelativeLayout.LayoutParams) itemThumbsUpView.getLayoutParams()).topMargin =
hasReplies ? 0 : DeviceUtils.dpToPx(6, itemBuilder.getContext());
// setup comment content and click listeners to expand/ellipsize it
textEllipsizer.setStreamingService(getServiceById(item.getServiceId()));
textEllipsizer.setStreamUrl(item.getUrl());
textEllipsizer.setContent(item.getCommentText());
textEllipsizer.ellipsize();
//noinspection ClickableViewAccessibility
itemContentView.setOnTouchListener((v, event) -> {
final CharSequence text = itemContentView.getText();
if (text instanceof Spanned buffer) {
final int action = event.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
final int offset = getOffsetForHorizontalLine(itemContentView, event);
final var links = buffer.getSpans(offset, offset, ClickableSpan.class);
if (links.length != 0) {
if (action == MotionEvent.ACTION_UP) {
links[0].onClick(itemContentView);
}
// we handle events that intersect links, so return true
return true;
}
}
}
return false;
});
itemView.setOnClickListener(view -> {
textEllipsizer.toggle();
if (itemBuilder.getOnCommentsSelectedListener() != null) {
itemBuilder.getOnCommentsSelectedListener().selected(item);
}
});
itemView.setOnLongClickListener(view -> {
if (DeviceUtils.isTv(itemBuilder.getContext())) {
openCommentAuthor(item);
} else {
final CharSequence text = itemContentView.getText();
if (text != null) {
ShareUtils.copyToClipboard(itemBuilder.getContext(), text.toString());
}
}
return true;
});
}
private void openCommentAuthor(@NonNull final CommentsInfoItem item) {
NavigationHelper.openCommentAuthorIfPresent((FragmentActivity) itemBuilder.getContext(),
item);
}
private void openCommentReplies(@NonNull final CommentsInfoItem item) {
NavigationHelper.openCommentRepliesFragment((FragmentActivity) itemBuilder.getContext(),
item);
}
private void allowLinkFocus() {
itemContentView.setMovementMethod(LinkMovementMethod.getInstance());
}
private void denyLinkFocus() {
itemContentView.setMovementMethod(null);
}
private boolean shouldFocusLinks() {
if (itemView.isInTouchMode()) {
return false;
}
final URLSpan[] urls = itemContentView.getUrls();
return urls != null && urls.length != 0;
}
private void determineMovementMethod() {
if (shouldFocusLinks()) {
allowLinkFocus();
} else {
denyLinkFocus();
}
}
}

View File

@ -155,15 +155,6 @@ public final class ExtractorHelper {
CommentsInfo.getInfo(NewPipe.getService(serviceId), url)));
}
public static Single<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems(
final int serviceId,
final CommentsInfo info,
final Page nextPage) {
checkServiceId(serviceId);
return Single.fromCallable(() ->
CommentsInfo.getMoreItems(NewPipe.getService(serviceId), info, nextPage));
}
public static Single<InfoItemsPage<CommentsInfoItem>> getMoreCommentItems(
final int serviceId,
final String url,

View File

@ -1,71 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.schabi.newpipe.views.NewPipeRecyclerView
android:id="@+id/items_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
tools:listitem="@layout/list_comment_item" />
<ProgressBar
android:id="@+id/loading_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/empty_state_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:orientation="vertical"
android:paddingTop="85dp"
android:visibility="gone"
tools:visibility="visible">
<org.schabi.newpipe.views.NewPipeTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="10dp"
android:fontFamily="monospace"
android:text="(╯°-°)╯"
android:textSize="35sp"
tools:ignore="HardcodedText,UnusedAttribute" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/empty_state_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/empty_view_no_comments"
android:textSize="24sp" />
</LinearLayout>
<!--ERROR PANEL-->
<include
android:id="@+id/error_panel"
layout="@layout/error_panel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:visibility="gone"
tools:visibility="visible" />
<View
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_alignParentTop="true"
android:background="?attr/toolbar_shadow"
android:visibility="gone" />
</RelativeLayout>

View File

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/comments_vertical_padding">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/itemThumbnailView"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginEnd="@dimen/comment_item_avatar_right_margin"
android:focusable="false"
android:src="@drawable/placeholder_person"
app:shapeAppearance="@style/CircularImageView"
tools:ignore="RtlHardcoded" />
<ImageView
android:id="@+id/detail_pinned_view"
android:layout_width="@dimen/video_item_detail_pinned_image_width"
android:layout_height="@dimen/video_item_detail_pinned_image_height"
android:layout_alignParentTop="true"
android:layout_marginEnd="@dimen/video_item_detail_pinned_right_margin"
android:layout_toEndOf="@+id/itemThumbnailView"
android:contentDescription="@string/detail_pinned_comment_view_description"
android:src="@drawable/ic_pin" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemTitleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/detail_pinned_view"
android:ellipsize="end"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/comment_item_title_text_size"
tools:text="Author Name, Lorem ipsum • 5 months ago" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/itemCommentContentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/itemTitleView"
android:layout_marginTop="6dp"
android:layout_toEndOf="@+id/itemThumbnailView"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/comment_item_content_text_size"
tools:text="@tools:sample/lorem/random[1]" />
<ImageView
android:id="@+id/detail_thumbs_up_img_view"
android:layout_width="@dimen/video_item_detail_like_image_width"
android:layout_height="@dimen/video_item_detail_like_image_height"
android:layout_below="@id/itemCommentContentView"
android:layout_alignBottom="@+id/replies_button"
android:layout_toEndOf="@+id/itemThumbnailView"
android:contentDescription="@string/detail_likes_img_view_description"
android:src="@drawable/ic_thumb_up" />
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/detail_thumbs_up_count_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/detail_thumbs_up_img_view"
android:layout_alignBottom="@id/detail_thumbs_up_img_view"
android:layout_marginStart="@dimen/video_item_detail_like_margin"
android:layout_toEndOf="@id/detail_thumbs_up_img_view"
android:gravity="center"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="@dimen/video_item_detail_likes_text_size"
tools:text="12M" />
<ImageView
android:id="@+id/detail_heart_image_view"
android:layout_width="@dimen/video_item_detail_heart_image_size"
android:layout_height="@dimen/video_item_detail_heart_image_size"
android:layout_alignTop="@id/detail_thumbs_up_img_view"
android:layout_alignBottom="@id/detail_thumbs_up_img_view"
android:layout_marginStart="@dimen/video_item_detail_heart_margin"
android:layout_toEndOf="@+id/detail_thumbs_up_count_view"
android:contentDescription="@string/detail_heart_img_view_description"
android:src="@drawable/ic_heart" />
<Button
android:id="@+id/replies_button"
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/itemCommentContentView"
android:layout_alignParentEnd="true"
android:layout_marginStart="@dimen/video_item_detail_heart_margin"
android:minHeight="0dp"
tools:text="543 replies" />
</RelativeLayout>