diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
index 01f2f48b12fe2868525b280e6b2b5a1bd982f701..01ef456a5119b136f97966253f47a11a3f6c0dd2 100644
--- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
+++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
@@ -1,6 +1,5 @@
 package com.futo.platformplayer
 
-import android.app.Activity
 import android.app.NotificationManager
 import android.content.ContentResolver
 import android.content.Context
@@ -68,7 +67,7 @@ class UISlideOverlays {
             return menu;
         }
 
-        fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup) {
+        fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
             val items = arrayListOf<View>();
 
             val originalNotif = subscription.doNotifications;
@@ -77,15 +76,13 @@ class UISlideOverlays {
             val originalVideo = subscription.doFetchVideos;
             val originalPosts = subscription.doFetchPosts;
 
+            val menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, listOf());
+
             StateApp.instance.scopeOrNull?.launch(Dispatchers.IO){
                 val plugin = StatePlatform.instance.getChannelClient(subscription.channel.url);
                 val capabilities = plugin.getChannelCapabilities();
 
                 withContext(Dispatchers.Main) {
-
-                    var menu: SlideUpMenuOverlay? = null;
-
-
                     items.addAll(listOf(
                         SlideUpMenuItem(container.context, R.drawable.ic_notifications, "Notifications", "", "notifications", {
                             subscription.doNotifications = menu?.selectOption(null, "notifications", true, true) ?: subscription.doNotifications;
@@ -119,7 +116,7 @@ class UISlideOverlays {
                         }, false)*/
                         ).filterNotNull());
 
-                    menu = SlideUpMenuOverlay(container.context, container, "Subscription Settings", null, true, items);
+                    menu.setItems(items);
 
                     if(subscription.doNotifications)
                         menu.selectOption(null, "notifications", true, true);
@@ -174,6 +171,8 @@ class UISlideOverlays {
                     menu.show();
                 }
             }
+
+            return menu;
         }
 
         fun showAddToGroupOverlay(channel: IPlatformVideo, container: ViewGroup) {
@@ -718,6 +717,13 @@ class UISlideOverlays {
             );
 
             val playlistItems = arrayListOf<SlideUpMenuItem>();
+            playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, container.context.getString(R.string.new_playlist), container.context.getString(R.string.add_to_new_playlist), "add_to_new_playlist", {
+                showCreatePlaylistOverlay(container) {
+                    val playlist = Playlist(it, arrayListOf(SerializedPlatformVideo.fromVideo(video)));
+                    StatePlaylists.instance.createOrUpdatePlaylist(playlist);
+                };
+            }, false))
+
             for (playlist in allPlaylists) {
                 playlistItems.add(SlideUpMenuItem(container.context, R.drawable.ic_playlist_add, playlist.name, "${playlist.videos.size} " + container.context.getString(R.string.videos), "",
                     {
diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
index c37f882a787e45fde140d6065a73e0fb4d0d1218..b1f74e13a5d6991e5f2a5d6182940aa369089584 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
@@ -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();
     }
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt
index 90a65e00094ec12acf9ebedfe7d19e8e6fa8d155..c45bdfebd981f83e4aad37c68e1343f269338f41 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt
@@ -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
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt
index f369f4814b25d26600b4f77d98e7333e833825ed..580bd5085c3cec5a5cf97fa3a88442f472de6779 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt
@@ -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();
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt
index c81ac8deafc16a83c57487722cd104034d15d07f..b2f548b7575ddfcf1655f4e4ab7b525709f4eef4 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt
@@ -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 = "";
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
index 487832333ec0270d9e2a25cbce0587fbba056591..ddb21815b60003af498106164127caa9cb193f65 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
@@ -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);
             }
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
index 82d644b5778e1753249a04bce5c5c273a4397670..78f14b56b0a4e73937a0e35e45576128175f9c0f 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
@@ -373,7 +373,7 @@ class VideoDetailView : ConstraintLayout {
 
 
         _buttonSubscribe.onSubscribed.subscribe {
-            UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
+            _slideUpOverlay = UISlideOverlays.showSubscriptionOptionsOverlay(it, _overlayContainer);
         };
 
         _container_content_liveChat.onRaidNow.subscribe {
@@ -2359,7 +2359,7 @@ class VideoDetailView : ConstraintLayout {
         }
         else if(isOverlayed) {
             _playerProgress.layoutParams = _playerProgress.layoutParams.apply {
-                (this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -6f, resources.displayMetrics).toInt();
+                (this as MarginLayoutParams).bottomMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, -2f, resources.displayMetrics).toInt();
             };
             _playerProgress.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
         }
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 984c6b4f2f4583e512a7a6a5eea393640b3efdb7..b81b08c0ffc6e30d863af9784b59fc99957f1692 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt
@@ -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;
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt
index f1fc07dac3eabf6b255f398dad1dbe59171c921b..6b40bbb22098578893fbdf6017b05e5df2e94557 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt
@@ -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);
     }
 
diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
index 109538339a37f2c939acd9d9093345981d8b5746..7f7d2429953096c7905d0c9eb78b3f33d51be06a 100644
--- a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
@@ -85,6 +85,11 @@ class GestureControlView : LinearLayout {
     private val _layoutControlsZoom: FrameLayout
     private val _textZoom: TextView
     private var _isZooming = false
+    private var _isPanning = false
+    private var _isZoomPanEnabled = false
+    private var _surfaceView: View? = null
+    private var _layoutIndicatorFill: FrameLayout;
+    private var _layoutIndicatorFit: FrameLayout;
 
     private val _gestureController: GestureDetectorCompat;
 
@@ -113,20 +118,16 @@ class GestureControlView : LinearLayout {
         _textZoom = findViewById(R.id.text_zoom)
         _progressBrightness = findViewById(R.id.progress_brightness);
         _layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen);
+        _layoutIndicatorFill = findViewById(R.id.layout_indicator_fill);
+        _layoutIndicatorFit = findViewById(R.id.layout_indicator_fit);
 
         _scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
             override fun onScale(detector: ScaleGestureDetector): Boolean {
-                if (!_isFullScreen || !Settings.instance.gestureControls.zoom) {
+                if (!_isZoomPanEnabled || !_isFullScreen || !Settings.instance.gestureControls.zoom) {
                     return false
                 }
 
-                var newScaleFactor = (_scaleFactor * detector.scaleFactor).coerceAtLeast(1.0f).coerceAtMost(5.0f)
-
-                //Make original zoom sticky
-                if (newScaleFactor - 1.0f < 0.01f) {
-                    newScaleFactor = 1.0f
-                }
-
+                val newScaleFactor = (_scaleFactor * detector.scaleFactor).coerceAtLeast(1.0f).coerceAtMost(10.0f)
                 val scaleFactorChange = newScaleFactor / _scaleFactor
                 _scaleFactor = newScaleFactor
                 onZoom.emit(_scaleFactor)
@@ -149,6 +150,9 @@ class GestureControlView : LinearLayout {
                 _layoutControlsZoom.visibility = View.VISIBLE
                 _textZoom.text = "${String.format("%.1f", _scaleFactor)}x"
                 _isZooming = true
+
+                updateSnappingVisibility()
+
                 return true
             }
         })
@@ -164,7 +168,7 @@ class GestureControlView : LinearLayout {
 
                 Logger.i(TAG, "p0.pointerCount: " + p0.pointerCount)
 
-                if (p1.pointerCount == 1) {
+                if (!_isPanning && p1.pointerCount == 1) {
                     val minDistance = Math.min(width, height)
                     if (_isFullScreen && _adjustingBrightness) {
                         val adjustAmount = (distanceY * 2) / minDistance;
@@ -201,8 +205,10 @@ class GestureControlView : LinearLayout {
                             }
                         }
                     }
-                } else if (_isFullScreen && !_isZooming && Settings.instance.gestureControls.pan) {
+                } else if (_isZoomPanEnabled && _isFullScreen && !_isZooming && Settings.instance.gestureControls.pan) {
+                    _isPanning = true
                     stopAllGestures()
+                    updateSnappingVisibility()
                     pan(_translationX - distanceX, _translationY - distanceY)
                 }
 
@@ -244,6 +250,39 @@ class GestureControlView : LinearLayout {
         isClickable = true
     }
 
+    fun updateSnappingVisibility() {
+        if (willSnapFill()) {
+            _layoutIndicatorFill.visibility = View.VISIBLE
+            _layoutIndicatorFit.visibility = View.GONE
+        } else if (willSnapFit()) {
+            _layoutIndicatorFill.visibility = View.GONE
+            _layoutIndicatorFit.visibility = View.VISIBLE
+
+            _surfaceView?.let {
+                val lp = _layoutIndicatorFit.layoutParams
+                lp.width = it.width
+                lp.height = it.height
+                _layoutIndicatorFit.layoutParams = lp
+            }
+        } else {
+            _layoutIndicatorFill.visibility = View.GONE
+            _layoutIndicatorFit.visibility = View.GONE
+        }
+    }
+
+    fun setZoomPanEnabled(view: View) {
+        _isZoomPanEnabled = true
+        _surfaceView = view
+    }
+
+    fun resetZoomPan() {
+        _scaleFactor = 1.0f
+        onZoom.emit(_scaleFactor)
+        _translationX = 0f
+        _translationY = 0f
+        onPan.emit(_translationX, _translationY)
+    }
+
     private fun pan(translationX: Float, translationY: Float) {
         val xc = width / 2.0f
         val yc = height / 2.0f
@@ -305,9 +344,29 @@ class GestureControlView : LinearLayout {
             stopAdjustingFullscreenDown();
         }
 
-        if (_isZooming && ev.action == MotionEvent.ACTION_UP) {
+        if ((_isPanning || _isZooming) && ev.action == MotionEvent.ACTION_UP) {
+            val surfaceView = _surfaceView
+            if (surfaceView != null && willSnapFill()) {
+                _scaleFactor = calculateZoomScaleFactor()
+                onZoom.emit(_scaleFactor)
+
+                _translationX = 0f
+                _translationY = 0f
+                onPan.emit(_translationX, _translationY)
+            } else if (willSnapFit()) {
+                _scaleFactor = 1f
+                onZoom.emit(_scaleFactor)
+
+                _translationX = 0f
+                _translationY = 0f
+                onPan.emit(_translationX, _translationY)
+            }
+
             _layoutControlsZoom.visibility = View.GONE
+            _layoutIndicatorFill.visibility = View.GONE
+            _layoutIndicatorFit.visibility = View.GONE
             _isZooming = false
+            _isPanning = false
         }
 
         startHideJobIfNecessary();
@@ -317,6 +376,35 @@ class GestureControlView : LinearLayout {
         return true;
     }
 
+    private fun calculateZoomScaleFactor(): Float {
+        val w = _surfaceView?.width?.toFloat() ?: return 1.0f;
+        val h = _surfaceView?.height?.toFloat() ?: return 1.0f;
+        if (w == 0.0f || h == 0.0f) {
+            return 1.0f;
+        }
+
+        return Math.max(width / w, height / h)
+    }
+
+    private val _snapTranslationTolerance = 0.04f;
+    private val _snapZoomTolerance = 0.04f;
+
+    private fun willSnapFill(): Boolean {
+        val surfaceView = _surfaceView
+        if (surfaceView != null) {
+            val zoomScaleFactor = calculateZoomScaleFactor()
+            if (Math.abs(_scaleFactor - zoomScaleFactor) < _snapZoomTolerance && Math.abs(_translationX) / width < _snapTranslationTolerance && Math.abs(_translationY) / height < _snapTranslationTolerance) {
+                return true
+            }
+        }
+
+        return false
+    }
+
+    private fun willSnapFit(): Boolean {
+        return Math.abs(_scaleFactor - 1.0f) < _snapZoomTolerance && Math.abs(_translationX) / width < _snapTranslationTolerance && Math.abs(_translationY) / height < _snapTranslationTolerance
+    }
+
     fun cancelHideJob() {
         _jobHideControls?.cancel();
         _jobHideControls = null;
@@ -646,11 +734,7 @@ class GestureControlView : LinearLayout {
     }
 
     fun setFullscreen(isFullScreen: Boolean) {
-        _scaleFactor = 1.0f
-        onZoom.emit(_scaleFactor)
-        _translationX = 0f
-        _translationY = 0f
-        onPan.emit(_translationX, _translationY)
+        resetZoomPan()
 
         if (isFullScreen) {
             val c = context
diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt
index 4d2a7fb9810804ddfc43178722b226cc9983c7c3..19c5dd226dc898209909f25ce4e926b009a279ed 100644
--- a/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt
@@ -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"
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt b/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt
index e02205b7e4dcbfd5381c4d1f63f07d8c830b4ad4..57732acb5c0f8e926f97aded71619430a63d5154 100644
--- a/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt
@@ -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);
 
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
index a84dcef69f7bf01091839169f30b3df3ce6be2e4..df3a24fb7657a485b29d75256b329457a5614719 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
@@ -272,6 +272,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
             _videoView.scaleY = it
         }
 
+        gestureControl.setZoomPanEnabled(_videoView.videoSurfaceView!!)
+
         if(!isInEditMode) {
             _videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
             val player = StatePlayer.instance.getPlayerOrCreate(context);
@@ -600,6 +602,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
     }
 
     override fun onVideoSizeChanged(videoSize: VideoSize) {
+        gestureControl.resetZoomPan()
         _lastSourceFit = null;
         if(isFullScreen)
             fillHeight();
@@ -763,4 +766,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
     fun setGestureSoundFactor(soundFactor: Float) {
         gestureControl.setSoundFactor(soundFactor);
     }
+
+    override fun onSurfaceSizeChanged(width: Int, height: Int) {
+        gestureControl.resetZoomPan()
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
index d60a3f8e6ab0f4153ad60bbcd13ff54a3a6d8517..3179e2829de0d0ba5ba32fe13e14f4d037aaad53 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
@@ -104,6 +104,11 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
             super.onPlaybackSuppressionReasonChanged(playbackSuppressionReason)
         }
 
+        override fun onSurfaceSizeChanged(width: Int, height: Int) {
+            super.onSurfaceSizeChanged(width, height)
+            this@FutoVideoPlayerBase.onSurfaceSizeChanged(width, height);
+        }
+
         override fun onIsPlayingChanged(isPlaying: Boolean) {
             super.onIsPlayingChanged(isPlaying);
             this@FutoVideoPlayerBase.onIsPlayingChanged(isPlaying);
@@ -592,6 +597,10 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
         exoPlayer?.setVolume(volume);
     }
 
+    protected open fun onSurfaceSizeChanged(width: Int, height: Int) {
+
+    }
+
     @Suppress("DEPRECATION")
     protected open fun onPlayerError(error: PlaybackException) {
         Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");
diff --git a/app/src/main/res/drawable/background_primary_border.xml b/app/src/main/res/drawable/background_primary_border.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4b155c58889e5dbb291615af26f487bc92604258
--- /dev/null
+++ b/app/src/main/res/drawable/background_primary_border.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <stroke android:color="#992D63ED" android:width="5dp" />
+    <padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
+</shape>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_comments.xml b/app/src/main/res/layout/fragment_comments.xml
index 0e49ff194e0e6ad63014ba117741a70bb07e6b5f..29fe4c0444e8fb87cc10dad5e3a94278984b3b0f 100644
--- a/app/src/main/res/layout/fragment_comments.xml
+++ b/app/src/main/res/layout/fragment_comments.xml
@@ -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"
diff --git a/app/src/main/res/layout/overlay_loader.xml b/app/src/main/res/layout/overlay_loader.xml
index c16a6bda29f7aaf0255bed9fbdfbf56f45983494..dafc34c6ab2019c8e4f17f1332c6aceb534a5f2e 100644
--- a/app/src/main/res/layout/overlay_loader.xml
+++ b/app/src/main/res/layout/overlay_loader.xml
@@ -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"
diff --git a/app/src/main/res/layout/overlay_replies.xml b/app/src/main/res/layout/overlay_replies.xml
index c683b38a3a0cd2e502ecbab3c9b0b7a8e1620178..51bb4b4c770bd23a53b177c44f7e33482fb3e70f 100644
--- a/app/src/main/res/layout/overlay_replies.xml
+++ b/app/src/main/res/layout/overlay_replies.xml
@@ -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>
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_gesture_controls.xml b/app/src/main/res/layout/view_gesture_controls.xml
index a358a60aec4013c0b73a5d3f5a566cc0e2accdec..ea175d3148eaecaebc6648893099e9eb6a13813b 100644
--- a/app/src/main/res/layout/view_gesture_controls.xml
+++ b/app/src/main/res/layout/view_gesture_controls.xml
@@ -177,4 +177,22 @@
             android:textSize="16dp"/>
     </FrameLayout>
 
+    <FrameLayout
+        android:id="@+id/layout_indicator_fill"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/background_primary_border"
+        android:visibility="gone" />
+
+    <FrameLayout
+        android:id="@+id/layout_indicator_fit"
+        android:layout_width="100dp"
+        android:layout_height="100dp"
+        android:background="@drawable/background_primary_border"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:visibility="gone"/>
+
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file