Added ability to click on a comment to view where the comment was placed and the ability to navigate upwards in the replies overlay by clicking the parent comment.

This commit is contained in:
Koen 2024-01-15 18:06:57 +01:00
parent 84e3373fa7
commit 6f7304f59c
12 changed files with 288 additions and 26 deletions

View file

@ -810,11 +810,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
if(_fragBotBarMenu.onBackPressed())
return;
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED &&
_fragVideoDetail.onBackPressed())
if(_fragVideoDetail.state == VideoDetailFragment.State.MAXIMIZED && _fragVideoDetail.onBackPressed())
return;
if(!fragCurrent.onBackPressed())
closeSegment();
}

View file

@ -19,8 +19,9 @@ class PolycentricPlatformComment : IPlatformComment {
val eventPointer: Pointer;
val reference: Reference;
val parentReference: Reference?;
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, replyCount: Int? = null) {
constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, parentReference: Reference?, replyCount: Int? = null) {
this.contextUrl = contextUrl;
this.author = author;
this.message = msg;
@ -29,6 +30,7 @@ class PolycentricPlatformComment : IPlatformComment {
this.replyCount = replyCount;
this.eventPointer = eventPointer;
this.reference = eventPointer.toReference();
this.parentReference = parentReference;
}
override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
@ -36,10 +38,11 @@ class PolycentricPlatformComment : IPlatformComment {
}
fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount);
return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, parentReference, replyCount);
}
companion object {
private const val TAG = "PolycentricPlatformComment"
val MAX_COMMENT_SIZE = 2000
}
}

View file

@ -123,7 +123,8 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
msg = comment,
rating = RatingLikeDislikes(0, 0),
date = OffsetDateTime.now(),
eventPointer = eventPointer
eventPointer = eventPointer,
parentReference = ref
));
dismiss();

View file

@ -117,6 +117,7 @@ class CommentsFragment : MainFragment() {
val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
holder.onDelete.subscribe(::onDelete);
holder.onRepliesClick.subscribe(::onRepliesClick);
holder.onClick.subscribe(::onClick);
return@InsertedViewAdapterWithLoader holder;
}
);
@ -200,6 +201,17 @@ class CommentsFragment : MainFragment() {
return false
}
private fun onClick(c: IPlatformComment) {
if (c !is PolycentricPlatformComment) {
return
}
val parentRef = c.parentReference
if (parentRef != null && _repliesOverlay.handleParentClick(c.contextUrl, parentRef)) {
setRepliesOverlayVisible(true, true)
}
}
private fun onRepliesClick(c: IPlatformComment) {
val replyCount = c.replyCount ?: 0;
var metadata = "";

View file

@ -169,14 +169,14 @@ class VideoDetailFragment : MainFragment {
_view!!.transitionToStart();
}
fun maximizeVideoDetail(instant: Boolean = false) {
if(_maximizeProgress > 0.9f && state != State.MAXIMIZED) {
if((_maximizeProgress > 0.9f || instant) && state != State.MAXIMIZED) {
state = State.MAXIMIZED;
onMaximized.emit();
}
_view?.let {
if(!instant)
if(!instant) {
it.transitionToEnd();
else {
} else {
it.progress = 1f;
onTransitioning.emit(true);
}

View file

@ -48,6 +48,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import userpackage.Protocol
import userpackage.Protocol.Reference
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
@ -287,7 +288,8 @@ class StatePolycentric {
rating = RatingLikeDislikes(0, 0),
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
replyCount = 0,
eventPointer = se.toPointer()
eventPointer = se.toPointer(),
parentReference = se.event.references.getOrNull(0)
))
}
@ -328,6 +330,77 @@ class StatePolycentric {
return LikesDislikesReplies(likes, dislikes, replyCount)
}
suspend fun getComment(contextUrl: String, reference: Reference): PolycentricPlatformComment {
ensureEnabled()
if (reference.referenceType != 2L) {
throw Exception("Not a pointer")
}
val pointer = Protocol.Pointer.parseFrom(reference.reference)
val events = ApiMethods.getEvents(PolycentricCache.SERVER, pointer.system, Protocol.RangesForSystem.newBuilder()
.addRangesForProcesses(Protocol.RangesForProcess.newBuilder()
.setProcess(pointer.process)
.addRanges(Protocol.Range.newBuilder()
.setLow(pointer.logicalClock)
.setHigh(pointer.logicalClock)
.build())
.build())
.build())
val sev = SignedEvent.fromProto(events.getEvents(0))
val ev = sev.event
if (ev.contentType != ContentType.POST.value) {
throw Exception("This is not a comment")
}
val post = Protocol.Post.parseFrom(ev.content);
val systemLinkUrl = ev.system.systemToURLInfoSystemLinkUrl(listOf(PolycentricCache.SERVER));
val dp_25 = 25.dp(StateApp.instance.context.resources)
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)
} else {
null
}
} else {
null
}
val ldr = getLikesDislikesReplies(reference)
return 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(ldr.likes, ldr.dislikes),
date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
replyCount = ldr.replyCount.toInt(),
eventPointer = sev.toPointer(),
parentReference = sev.event.references.getOrNull(0)
)
}
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
if (!enabled) {
return EmptyPager()
@ -453,7 +526,8 @@ class StatePolycentric {
rating = RatingLikeDislikes(likes, dislikes),
date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
replyCount = replies.toInt(),
eventPointer = sev.toPointer()
eventPointer = sev.toPointer(),
parentReference = sev.event.references.getOrNull(0)
);
} catch (e: Throwable) {
return@mapNotNull null;

View file

@ -55,6 +55,7 @@ class CommentWithReferenceViewHolder : ViewHolder {
var onRepliesClick = Event1<IPlatformComment>();
var onDelete = Event1<IPlatformComment>();
var onClick = Event1<IPlatformComment>();
var comment: IPlatformComment? = null
private set;
@ -108,6 +109,11 @@ class CommentWithReferenceViewHolder : ViewHolder {
onDelete.emit(c);
}
_layoutComment.setOnClickListener {
val c = comment ?: return@setOnClickListener;
onClick.emit(c);
}
_textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
}

View file

@ -1,6 +1,7 @@
package com.futo.platformplayer.views.overlays
import android.content.Context
import android.net.Uri
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
@ -8,11 +9,15 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.fixHtmlLinks
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.toHumanNowDiffString
@ -20,6 +25,13 @@ import com.futo.platformplayer.views.behavior.NonScrollingTextView
import com.futo.platformplayer.views.comments.AddCommentView
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.segments.CommentsList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import userpackage.Protocol
class RepliesOverlay : LinearLayout {
@ -34,7 +46,11 @@ class RepliesOverlay : LinearLayout {
private val _creatorThumbnail: CreatorThumbnail;
private val _layoutParentComment: ConstraintLayout;
private var _readonly = false;
private var _loading = true;
private var _parentComment: IPlatformComment? = null;
private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null;
private val _loaderOverlay: LoaderOverlay
private val _client = ManagedHttpClient()
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.overlay_replies, this)
@ -46,6 +62,8 @@ class RepliesOverlay : LinearLayout {
_textAuthor = findViewById(R.id.text_author)
_creatorThumbnail = findViewById(R.id.image_thumbnail)
_layoutParentComment = findViewById(R.id.layout_parent_comment)
_loaderOverlay = findViewById(R.id.loader_overlay)
setLoading(false);
_addCommentView.onCommentAdded.subscribe {
_commentsList.addComment(it);
@ -72,11 +90,21 @@ class RepliesOverlay : LinearLayout {
}
};
_layoutParentComment.setOnClickListener {
val p = _parentComment
if (p !is PolycentricPlatformComment) {
return@setOnClickListener
}
val ref = p.parentReference ?: return@setOnClickListener
handleParentClick(p.contextUrl, ref)
}
_topbar.onClose.subscribe(this, onClose::emit);
_topbar.setInfo(context.getString(R.string.Replies), "");
}
fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null) {
fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null, onParentClick: ((comment: IPlatformComment) -> Unit)? = null) {
_readonly = readonly;
if (readonly) {
_addCommentView.visibility = View.GONE;
@ -109,6 +137,136 @@ class RepliesOverlay : LinearLayout {
_topbar.setInfo(context.getString(R.string.Replies), metadata);
_commentsList.load(readonly, loader);
_onCommentAdded = onCommentAdded;
_parentComment = parentComment;
}
fun handleParentClick(contextUrl: String, ref: Protocol.Reference): Boolean {
val ctx = context
if (ctx !is MainActivity) {
return false
}
return when (ref.referenceType) {
2L -> {
setLoading(true)
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
try {
val parentComment = StatePolycentric.instance.getComment(contextUrl, ref)
val replyCount = parentComment.replyCount ?: 0;
var metadata = "";
if (replyCount > 0) {
metadata += "$replyCount " + context.getString(R.string.replies);
}
withContext(Dispatchers.Main) {
setLoading(false)
load(false, metadata, parentComment.contextUrl, parentComment.reference, parentComment,
{ StatePolycentric.instance.getCommentPager(contextUrl, ref) })
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
setLoading(false)
}
Logger.e(TAG, "Failed to load parent comment.", e)
UIDialogs.toast("Failed to load comment")
}
}
true
}
3L -> {
StateApp.instance.scopeOrNull?.launch {
try {
val url = referenceToUrl(_client, ref) ?: return@launch
withContext(Dispatchers.Main) {
ctx.handleUrl(url)
onClose.emit()
}
} catch (e: Throwable) {
Logger.i(TAG, "Failed to open ref.", e)
}
}
false
}
else -> false
}
}
private fun referenceToUrl(client: ManagedHttpClient, parentRef: Protocol.Reference): String? {
val refBytes = parentRef.reference?.toByteArray() ?: return null
val ref = refBytes.decodeToString()
try {
Uri.parse(ref)
return ref
} catch (e: Throwable) {
try {
return oldReferenceToUrl(client, ref)
} catch (f: Throwable) {
Logger.i(TAG, "Failed to handle URL.", f)
}
}
return null
}
private fun oldReferenceToUrl(client: ManagedHttpClient, reference: String): String? {
return when {
reference.startsWith("video_episode:") -> {
val response = client.get("https://content.api.nebula.app/video_episodes/$reference")
if (!response.isOk) {
throw Exception("Failed to resolve nebula video (${response.code}).")
}
val respString = response.body?.string()
val jsonElement = respString?.let { Json.parseToJsonElement(it) }
return jsonElement?.jsonObject?.get("share_url")?.jsonPrimitive?.content
}
reference.length == 11 -> "https://www.youtube.com/watch?v=$reference"
reference.length == 40 -> {
val response = client.post("https://api.na-backend.odysee.com/api/v1/proxy?m=claim_search", hashMapOf(
"Content-Type" to "application/json"
))
if (!response.isOk) {
throw Exception("Failed to resolve claim (${response.code}).")
}
val jsonElement = response.body?.string()?.let { Json.parseToJsonElement(it) }
val canonicalUrl = jsonElement?.jsonObject?.get("result")
?.jsonObject?.get("items")
?.jsonArray?.get(0)
?.jsonObject?.get("canonical_url")
?.jsonPrimitive?.content
canonicalUrl ?: throw Exception("Failed to get canonical URL.")
}
reference.startsWith("v") && (reference.length == 7 || reference.length == 6) -> "https://rumble.com/$reference"
Regex("^\\d+\$").matches(reference) -> "https://www.twitch.tv/videos/$reference"
else -> null
}
}
private fun setLoading(loading: Boolean) {
if (_loading == loading) {
return;
}
_loading = loading;
if (!loading) {
_loaderOverlay.hide()
} else {
_loaderOverlay.show()
}
}
fun cleanup() {
@ -116,4 +274,8 @@ class RepliesOverlay : LinearLayout {
_onCommentAdded = null;
_commentsList.cancel();
}
companion object {
private const val TAG = "RepliesOverlay"
}
}

View file

@ -4,7 +4,6 @@ import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.Gravity
import android.view.KeyCharacterMap.UnavailableException
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
@ -12,10 +11,8 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.R
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
@ -25,6 +22,8 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePolycentric
import com.futo.platformplayer.views.adapters.CommentViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
@ -87,8 +86,6 @@ class CommentsList : ConstraintLayout {
var onRepliesClick = Event1<IPlatformComment>();
var onCommentsLoaded = Event1<Int>();
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true);

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/layout_header"
@ -65,7 +65,8 @@
android:id="@+id/replies_overlay"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:clickable="true" />
<LinearLayout android:id="@+id/layout_not_logged_in"
android:layout_width="match_parent"

View file

@ -8,7 +8,8 @@
android:orientation="vertical"
android:id="@+id/container"
android:background="#77000000"
android:elevation="4dp">
android:elevation="4dp"
android:clickable="true">
<ImageView
android:id="@+id/loader"
android:layout_width="80dp"

View file

@ -50,7 +50,7 @@
android:textSize="14sp"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintTop_toTopOf="@id/image_thumbnail"
android:text="ShortCircuit" />
tools:text="ShortCircuit" />
<TextView
android:id="@+id/text_metadata"
@ -66,7 +66,7 @@
app:layout_constraintLeft_toRightOf="@id/text_author"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/text_author"
android:text=" • 3 years ago" />
tools:text=" • 3 years ago" />
<com.futo.platformplayer.views.behavior.NonScrollingTextView
android:id="@+id/text_body"
@ -84,7 +84,7 @@
app:layout_constraintTop_toBottomOf="@id/text_metadata"
app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
app:layout_constraintRight_toRightOf="parent"
android:text="@string/lorem_ipsum" />
tools:text="@string/lorem_ipsum" />
</androidx.constraintlayout.widget.ConstraintLayout>
@ -107,4 +107,11 @@
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="12dp" />
<com.futo.platformplayer.views.overlays.LoaderOverlay
android:id="@+id/loader_overlay"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true" />
</androidx.constraintlayout.widget.ConstraintLayout>