diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/comments/LazyComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/comments/LazyComment.kt new file mode 100644 index 00000000..f94d0a7d --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/comments/LazyComment.kt @@ -0,0 +1,63 @@ +package com.futo.platformplayer.api.media.models.comments + +import com.futo.platformplayer.api.media.IPlatformClient +import com.futo.platformplayer.api.media.models.PlatformAuthorLink +import com.futo.platformplayer.api.media.models.ratings.IRating +import com.futo.platformplayer.api.media.models.ratings.RatingLikes +import com.futo.platformplayer.api.media.models.ratings.RatingType +import com.futo.platformplayer.api.media.structures.IPager +import com.futo.platformplayer.logging.Logger +import kotlinx.coroutines.Deferred +import java.time.OffsetDateTime + +class LazyComment: IPlatformComment { + private var _commentDeferred: Deferred; + private var _commentLoaded: IPlatformComment? = null; + private var _commentException: Throwable? = null; + + override val contextUrl: String + get() = _commentLoaded?.contextUrl ?: ""; + override val author: PlatformAuthorLink + get() = _commentLoaded?.author ?: PlatformAuthorLink.UNKNOWN; + override val message: String + get() = _commentLoaded?.message ?: ""; + override val rating: IRating + get() = _commentLoaded?.rating ?: RatingLikes(0); + override val date: OffsetDateTime? + get() = _commentLoaded?.date ?: OffsetDateTime.MIN; + override val replyCount: Int? + get() = _commentLoaded?.replyCount ?: 0; + + val isAvailable: Boolean get() = _commentLoaded != null; + + private var _uiHandler: ((LazyComment)->Unit)? = null; + + constructor(commentDeferred: Deferred) { + _commentDeferred = commentDeferred; + _commentDeferred.invokeOnCompletion { + if(it == null) { + _commentLoaded = commentDeferred.getCompleted(); + Logger.i("LazyComment", "Resolved comment"); + } + else { + _commentException = it; + Logger.e("LazyComment", "Resolving comment failed: ${it.message}", it); + } + + _uiHandler?.invoke(this); + } + } + + fun getUnderlyingComment(): IPlatformComment? { + return _commentLoaded; + } + + fun setUIHandler(handler: (LazyComment)->Unit){ + _uiHandler = handler; + } + + override fun getReplies(client: IPlatformClient): IPager? { + return _commentLoaded?.getReplies(client); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt index bed1ba3b..e98aff0c 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt @@ -10,6 +10,7 @@ import com.futo.platformplayer.activities.PolycentricHomeActivity import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.comments.IPlatformComment +import com.futo.platformplayer.api.media.models.comments.LazyComment import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes @@ -46,6 +47,7 @@ import com.google.protobuf.ByteString import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import userpackage.Protocol @@ -53,6 +55,7 @@ import userpackage.Protocol.Reference import java.time.Instant import java.time.OffsetDateTime import java.time.ZoneOffset +import java.util.concurrent.ForkJoinPool class StatePolycentric { private data class LikeDislikeEntry(val unixMilliseconds: Long, val hasLiked: Boolean, val hasDisliked: Boolean); @@ -63,6 +66,9 @@ class StatePolycentric { private var _transientEnabled = true val enabled get() = _transientEnabled && Settings.instance.other.polycentricEnabled + private val _commentPool = ForkJoinPool(2); + private val _commentPoolDispatcher = _commentPool.asCoroutineDispatcher(); + fun load(context: Context) { if (!enabled) { return @@ -510,7 +516,7 @@ class StatePolycentric { }; } - private suspend fun mapQueryReferences(contextUrl: String, response: Protocol.QueryReferencesResponse): List { + private suspend fun mapQueryReferences(contextUrl: String, response: Protocol.QueryReferencesResponse): List { return response.itemsList.mapNotNull { val sev = SignedEvent.fromProto(it.event); val ev = sev.event; @@ -524,49 +530,53 @@ class StatePolycentric { val dislikes = it.countsList[1]; val replies = it.countsList[2]; - val profileEvents = ApiMethods.getQueryLatest( - PolycentricCache.SERVER, - ev.system.toProto(), - listOf( - ContentType.AVATAR.value, - ContentType.USERNAME.value - ) - ).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType } - .map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } }; + val scope = StateApp.instance.scopeOrNull ?: return@mapNotNull null; + return@mapNotNull LazyComment(scope.async(_commentPoolDispatcher){ + Logger.i(TAG, "Fetching comment data for [" + ev.system.key.toBase64() + "]"); + val profileEvents = ApiMethods.getQueryLatest( + PolycentricCache.SERVER, + ev.system.toProto(), + listOf( + ContentType.AVATAR.value, + ContentType.USERNAME.value + ) + ).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType } + .map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } }; - val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value }; - val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value }; - val imageBundle = if (avatarEvent != null) { - val lwwElementValue = avatarEvent.event.lwwElement?.value; - if (lwwElementValue != null) { - Protocol.ImageBundle.parseFrom(lwwElementValue) + val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value }; + val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value }; + val imageBundle = if (avatarEvent != null) { + val lwwElementValue = avatarEvent.event.lwwElement?.value; + if (lwwElementValue != null) { + Protocol.ImageBundle.parseFrom(lwwElementValue) + } else { + null + } } else { null } - } else { - null - } - val unixMilliseconds = ev.unixMilliseconds - //TODO: Don't use single hardcoded sderver here - val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER)); - val dp_25 = 25.dp(StateApp.instance.context.resources) - return@mapNotNull PolycentricPlatformComment( - contextUrl = contextUrl, - author = PlatformAuthorLink( - id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()), - name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown", - url = systemLinkUrl, - thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) }, - subscribers = null - ), - msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content, - rating = RatingLikeDislikes(likes, dislikes), - date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN, - replyCount = replies.toInt(), - eventPointer = sev.toPointer(), - parentReference = sev.event.references.getOrNull(0) - ); + val unixMilliseconds = ev.unixMilliseconds + //TODO: Don't use single hardcoded sderver here + val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER)); + val dp_25 = 25.dp(StateApp.instance.context.resources) + return@async PolycentricPlatformComment( + contextUrl = contextUrl, + author = PlatformAuthorLink( + id = PlatformID("polycentric", systemLinkUrl, null, ClaimType.POLYCENTRIC.value.toInt()), + name = nameEvent?.event?.lwwElement?.value?.decodeToString() ?: "Unknown", + url = systemLinkUrl, + thumbnail = imageBundle?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(ev.system.toProto(), img.process, listOf(PolycentricCache.SERVER)) }, + subscribers = null + ), + msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content, + rating = RatingLikeDislikes(likes, dislikes), + date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN, + replyCount = replies.toInt(), + eventPointer = sev.toPointer(), + parentReference = sev.event.references.getOrNull(0) + ); + }); } catch (e: Throwable) { return@mapNotNull null; } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt index b6e98981..fe9c6079 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.api.media.models.comments.IPlatformComment +import com.futo.platformplayer.api.media.models.comments.LazyComment import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes import com.futo.platformplayer.api.media.models.ratings.RatingLikes @@ -24,6 +25,7 @@ import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.toHumanNowDiffString import com.futo.platformplayer.toHumanNumber +import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.pills.PillButton import com.futo.platformplayer.views.pills.PillRatingLikesDislikes @@ -46,6 +48,9 @@ class CommentViewHolder : ViewHolder { private val _layoutComment: ConstraintLayout; private val _buttonDelete: FrameLayout; + private val _containerComments: ConstraintLayout; + private val _loader: LoaderView; + var onRepliesClick = Event1(); var onDelete = Event1(); var onAuthorClick = Event1(); @@ -67,6 +72,9 @@ class CommentViewHolder : ViewHolder { _pillRatingLikesDislikes = itemView.findViewById(R.id.rating); _buttonDelete = itemView.findViewById(R.id.button_delete); + _containerComments = itemView.findViewById(R.id.comment_container); + _loader = itemView.findViewById(R.id.loader); + _pillRatingLikesDislikes.onLikeDislikeUpdated.subscribe { args -> val c = comment if (c !is PolycentricPlatformComment) { @@ -123,6 +131,33 @@ class CommentViewHolder : ViewHolder { } fun bind(comment: IPlatformComment, readonly: Boolean) { + + if(comment is LazyComment){ + if(comment.isAvailable) + { + comment.getUnderlyingComment()?.let { + bind(it, readonly); + } + return; + } + else { + _loader.visibility = View.VISIBLE; + _loader.start(); + _containerComments.visibility = View.GONE; + comment.setUIHandler { + StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { + if (it.isAvailable && it == this@CommentViewHolder.comment) + bind(it, readonly); + } + } + } + } + else { + _loader.stop(); + _loader.visibility = View.GONE; + _containerComments.visibility = View.VISIBLE; + } + _creatorThumbnail.setThumbnail(comment.author.thumbnail, false); val polycentricComment = if (comment is PolycentricPlatformComment) comment else null _creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto()); diff --git a/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt b/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt index 3cd12fd7..2c983280 100644 --- a/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt +++ b/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt @@ -90,7 +90,7 @@ class PillRatingLikesDislikes : LinearLayout { setRating(rating, hasLiked, hasDisliked); } is RatingLikes -> { - setRating(rating, hasLiked, hasDisliked); + setRating(rating, hasLiked); } else -> { throw Exception("Unknown rating type"); @@ -98,6 +98,36 @@ class PillRatingLikesDislikes : LinearLayout { } } + fun setRating(rating: RatingLikeDislikes, hasLiked: Boolean = false, hasDisliked: Boolean = false) { + setLoading(false) + + _textLikes.text = rating.likes.toHumanNumber(); + _textDislikes.text = rating.dislikes.toHumanNumber(); + _textLikes.visibility = View.VISIBLE; + _textDislikes.visibility = View.VISIBLE; + _seperator.visibility = View.VISIBLE; + _iconDislikes.visibility = View.VISIBLE; + _likes = rating.likes; + _dislikes = rating.dislikes; + _hasLiked = hasLiked; + _hasDisliked = hasDisliked; + updateColors(); + } + fun setRating(rating: RatingLikes, hasLiked: Boolean = false) { + setLoading(false) + + _textLikes.text = rating.likes.toHumanNumber(); + _textLikes.visibility = View.VISIBLE; + _textDislikes.visibility = View.GONE; + _seperator.visibility = View.GONE; + _iconDislikes.visibility = View.GONE; + _likes = rating.likes; + _dislikes = 0; + _hasLiked = hasLiked; + _hasDisliked = false; + updateColors(); + } + fun like(processHandle: ProcessHandle) { if (_hasDisliked) { _dislikes--; @@ -155,34 +185,4 @@ class PillRatingLikesDislikes : LinearLayout { _iconDislikes.setColorFilter(ContextCompat.getColor(context, R.color.white)); } } - - fun setRating(rating: RatingLikeDislikes, hasLiked: Boolean = false, hasDisliked: Boolean = false) { - setLoading(false) - - _textLikes.text = rating.likes.toHumanNumber(); - _textDislikes.text = rating.dislikes.toHumanNumber(); - _textLikes.visibility = View.VISIBLE; - _textDislikes.visibility = View.VISIBLE; - _seperator.visibility = View.VISIBLE; - _iconDislikes.visibility = View.VISIBLE; - _likes = rating.likes; - _dislikes = rating.dislikes; - _hasLiked = hasLiked; - _hasDisliked = hasDisliked; - updateColors(); - } - fun setRating(rating: RatingLikes, hasLiked: Boolean = false) { - setLoading(false) - - _textLikes.text = rating.likes.toHumanNumber(); - _textLikes.visibility = View.VISIBLE; - _textDislikes.visibility = View.GONE; - _seperator.visibility = View.GONE; - _iconDislikes.visibility = View.GONE; - _likes = rating.likes; - _dislikes = 0; - _hasLiked = hasLiked; - _hasDisliked = false; - updateColors(); - } } \ No newline at end of file diff --git a/app/src/main/res/layout/list_comment.xml b/app/src/main/res/layout/list_comment.xml index ea2c861a..91a1d0a9 100644 --- a/app/src/main/res/layout/list_comment.xml +++ b/app/src/main/res/layout/list_comment.xml @@ -11,161 +11,179 @@ android:layout_marginEnd="14dp" android:orientation="vertical"> - - - - - + android:layout_marginTop="50dp" + android:layout_marginBottom="50dp" /> - - - - - + + tools:src="@drawable/placeholder_channel_thumbnail" /> + + + + + + - - - + android:layout_height="wrap_content" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:layout_marginStart="9dp" /> - - - - - + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginStart="10dp" + app:layout_constraintLeft_toRightOf="@id/image_thumbnail" + app:layout_constraintTop_toBottomOf="@id/text_body" + android:gravity="center_vertical"> - + - - + + + + + + + + - - + android:layout_weight="1" /> + + + + + \ No newline at end of file