From 73a067a0c5bc281e630f7f027bd7ddc6d4e6263e Mon Sep 17 00:00:00 2001
From: mrilyew <99399973+mrilyew@users.noreply.github.com>
Date: Fri, 22 Nov 2024 16:31:07 +0300
Subject: [PATCH] feat: ajax infinite scrolling (#1141)

* rewrite

* allow comments scroll

* rework to up button

* add posts scrolling function and ability to disabl

* cloudflare bypass (do not uncomment)
---
 Web/Models/Repositories/Posts.php             |  4 +-
 Web/Presenters/NotesPresenter.php             |  9 +-
 Web/Presenters/OpenVKPresenter.php            |  5 ++
 Web/Presenters/SearchPresenter.php            |  1 +
 Web/Presenters/templates/@layout.xml          | 10 ++-
 Web/Presenters/templates/@listView.xml        |  4 +-
 Web/Presenters/templates/Audio/List.xml       | 10 +--
 Web/Presenters/templates/Audio/Playlist.xml   |  4 +-
 Web/Presenters/templates/Gifts/Pick.xml       |  4 +-
 Web/Presenters/templates/Group/Suggested.xml  |  4 +-
 Web/Presenters/templates/Messenger/Index.xml  |  4 +-
 Web/Presenters/templates/Notes/List.xml       | 15 +++-
 .../templates/Notification/Feed.xml           |  4 +-
 Web/Presenters/templates/Photos/Album.xml     |  4 +-
 Web/Presenters/templates/Search/Index.xml     | 58 +++++++++----
 Web/Presenters/templates/User/Groups.xml      |  5 +-
 Web/Presenters/templates/Wall/Feed.xml        | 14 ++--
 Web/Presenters/templates/Wall/Wall.xml        | 10 +--
 .../templates/components/comments.xml         | 10 +--
 .../templates/components/paginator.xml        |  2 +-
 Web/Presenters/templates/components/wall.xml  | 10 +--
 Web/static/css/main.css                       | 29 ++++++-
 Web/static/js/al_comments.js                  |  6 +-
 Web/static/js/al_feed.js                      | 19 ++++-
 Web/static/js/al_mentions.js                  |  3 +-
 Web/static/js/al_music.js                     | 29 +++++++
 Web/static/js/al_suggestions.js               |  4 +-
 Web/static/js/al_wall.js                      | 83 ++++++++++++++++++-
 Web/static/js/openvk.cls.js                   |  4 +-
 Web/static/js/scroll.js                       | 31 +++++--
 locales/en.strings                            |  1 +
 locales/ru.strings                            |  1 +
 32 files changed, 306 insertions(+), 95 deletions(-)

diff --git a/Web/Models/Repositories/Posts.php b/Web/Models/Repositories/Posts.php
index 36082f24..da1f9c8d 100644
--- a/Web/Models/Repositories/Posts.php
+++ b/Web/Models/Repositories/Posts.php
@@ -53,9 +53,9 @@ class Posts
                     $offset--;
                 }
             }
-        } else if(!is_null($offset)) {
+        } /*else if(!is_null($offset)) {
             $offset--;
-        }
+        }*/
         
         $sel = $this->posts->where([
             "wall"      => $user,
diff --git a/Web/Presenters/NotesPresenter.php b/Web/Presenters/NotesPresenter.php
index 67105fe3..37475013 100644
--- a/Web/Presenters/NotesPresenter.php
+++ b/Web/Presenters/NotesPresenter.php
@@ -22,15 +22,10 @@ final class NotesPresenter extends OpenVKPresenter
         if(!$user->getPrivacyPermission('notes.read', $this->user->identity ?? NULL))
             $this->flashFail("err", tr("forbidden"), tr("forbidden_comment"));
         
-        $this->template->notes = $this->notes->getUserNotes($user, (int)($this->queryParam("p") ?? 1));
+        $this->template->page  = (int)($this->queryParam("p") ?? 1);
+        $this->template->notes = $this->notes->getUserNotes($user, $this->template->page);
         $this->template->count = $this->notes->getUserNotesCount($user);
         $this->template->owner = $user;
-        $this->template->paginatorConf = (object) [
-            "count"   => $this->template->count,
-            "page"    => $this->queryParam("p") ?? 1,
-            "amount"  => NULL,
-            "perPage" => OPENVK_DEFAULT_PER_PAGE,
-        ];
     }
     
     function renderView(int $owner, int $note_id): void
diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php
index 0e861a7b..6df21bd2 100644
--- a/Web/Presenters/OpenVKPresenter.php
+++ b/Web/Presenters/OpenVKPresenter.php
@@ -283,6 +283,11 @@ abstract class OpenVKPresenter extends SimplePresenter
             }
         }
 
+        /*if($this->queryParam('al') == '1') {
+            $this->assertNoCSRF();
+            header('Content-Type: text/plain; charset=UTF-8');
+        }*/
+
         parent::onStartup();
     }
     
diff --git a/Web/Presenters/SearchPresenter.php b/Web/Presenters/SearchPresenter.php
index a7f3f151..9e16450e 100644
--- a/Web/Presenters/SearchPresenter.php
+++ b/Web/Presenters/SearchPresenter.php
@@ -125,5 +125,6 @@ final class SearchPresenter extends OpenVKPresenter
         ];
         $this->template->extendedPaginatorConf = clone $this->template->paginatorConf;
         $this->template->extendedPaginatorConf->space = 11;
+        $this->template->paginatorConf->atTop = true;
     }
 }
diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml
index d7a15912..0e514bb8 100644
--- a/Web/Presenters/templates/@layout.xml
+++ b/Web/Presenters/templates/@layout.xml
@@ -87,7 +87,14 @@
         {/if}
 
         <div class="toTop">
-            ⬆ {_to_top}
+            <div id='to_up'>
+                <svg id="to_up_icon" viewBox="0 0 10 6"><polygon points="0 6 5 0 10 6 0 6"/></svg>
+                <span>{_to_top}</span>
+            </div>
+            
+            <div id='to_back'>
+                <svg id="to_back_icon" viewBox="0 0 10 6"><polygon points="0 0 5 6 10 0 0 0"/></svg>
+            </div>
         </div>
 
         <div class="layout">
@@ -384,6 +391,7 @@
         {script "js/al_polls.js"}
         {script "js/al_suggestions.js"}
         {script "js/al_navigation.js"}
+        {script "js/al_comments.js"}
 
         {ifset $thisUser}
             {script "js/al_notifs.js"}
diff --git a/Web/Presenters/templates/@listView.xml b/Web/Presenters/templates/@listView.xml
index 4b2dd9ff..f97d0da5 100644
--- a/Web/Presenters/templates/@listView.xml
+++ b/Web/Presenters/templates/@listView.xml
@@ -19,7 +19,7 @@
             {ifset specpage}
                 {include specpage, x => $dat}
             {else}
-                <div class="container_gray">
+                <div class="container_gray {ifset noscroll}no_scroll_container{else}scroll_container{/ifset}">
                     {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
 
                     {ifset top}
@@ -27,7 +27,7 @@
                     {/ifset}
 
                     {if sizeof($data) > 0}
-                        <div class="content" n:foreach="$data as $dat">
+                        <div class="scroll_node content" n:foreach="$data as $dat">
                             <table>
                                 <tbody n:attr="id => is_null($table_body_id) ? NULL : $table_body_id">
                                     <tr>
diff --git a/Web/Presenters/templates/Audio/List.xml b/Web/Presenters/templates/Audio/List.xml
index b67bb208..32d2e26b 100644
--- a/Web/Presenters/templates/Audio/List.xml
+++ b/Web/Presenters/templates/Audio/List.xml
@@ -64,8 +64,8 @@
             <div n:if="$audiosCount <= 0" style='height: 50%;'>
                 {include "../components/content_error.xml", description => $ownerId > 0 ? ($ownerId == $thisUser->getId() ? tr("no_audios_thisuser") : tr("no_audios_user")) : tr("no_audios_club")}
             </div>
-            <div n:if="$audiosCount > 0" class="infContainer">
-                <div class="infObj" n:foreach="$audios as $audio">
+            <div n:if="$audiosCount > 0" class="scroll_container infContainer">
+                <div class="scroll_node infObj" n:foreach="$audios as $audio">
                     {include "player.xml", audio => $audio, club => $club}
                 </div>
             </div>
@@ -86,10 +86,10 @@
                 {include "../components/content_error.xml", description => $ownerId > 0 ? ($ownerId == $thisUser->getId() ? tr("no_playlists_thisuser") : tr("no_playlists_user")) : tr("no_playlists_club")}
             </div>
 
-            <div class="infContainer playlistContainer" n:if="$playlistsCount > 0">
-                {foreach $playlists as $playlist}
+            <div class="scroll_container infContainer playlistContainer" n:if="$playlistsCount > 0">
+                <div class='scroll_node' n:foreach='$playlists as $playlist'>
                     {include 'playlistListView.xml', playlist => $playlist}
-                {/foreach}
+                </div>
             </div>
 
             <div>
diff --git a/Web/Presenters/templates/Audio/Playlist.xml b/Web/Presenters/templates/Audio/Playlist.xml
index 4139fa2f..7f8c7348 100644
--- a/Web/Presenters/templates/Audio/Playlist.xml
+++ b/Web/Presenters/templates/Audio/Playlist.xml
@@ -67,11 +67,11 @@
                     <hr style="color: #f7f7f7;">
                 </div>
             </div>
-            <div class="audiosContainer infContainer" style="margin-top: 14px;">
+            <div class="audiosContainer scroll_container infContainer" style="margin-top: 14px;">
                 {if $count < 1}
                     {_empty_playlist}
                 {else}  
-                    <div class="infObj" n:foreach="$audios as $audio">
+                    <div class="scroll_node" n:foreach="$audios as $audio">
                         {include "player.xml", audio => $audio}
                     </div>
 
diff --git a/Web/Presenters/templates/Gifts/Pick.xml b/Web/Presenters/templates/Gifts/Pick.xml
index dad30839..cb01b944 100644
--- a/Web/Presenters/templates/Gifts/Pick.xml
+++ b/Web/Presenters/templates/Gifts/Pick.xml
@@ -12,8 +12,8 @@
 {/block}
 
 {block content}
-    <div class="gift_grid">
-        <div n:foreach="$gifts as $gift" n:class="gift_sel, !$gift->canUse($thisUser) ? disabled" data-gift="{$gift->getId()}">
+    <div class="gift_grid scroll_container">
+        <div n:foreach="$gifts as $gift" n:class="scroll_node, gift_sel, !$gift->canUse($thisUser) ? disabled" data-gift="{$gift->getId()}">
             <img class="gift_pic" src="{$gift->getImage(2)}" alt="{_gift}" loading=lazy />
 
             <strong class="gift_price">
diff --git a/Web/Presenters/templates/Group/Suggested.xml b/Web/Presenters/templates/Group/Suggested.xml
index 1c882675..1a07e0a8 100644
--- a/Web/Presenters/templates/Group/Suggested.xml
+++ b/Web/Presenters/templates/Group/Suggested.xml
@@ -12,9 +12,9 @@
         {include "../components/error.xml", title => "", description => $type == "my" ? tr("no_suggested_posts_by_you") : tr("no_suggested_posts_by_people")}
     {else}
         <h4 id="cound">{if $type == "my"}{tr("suggested_posts_in_group_by_you", $count)}{else}{tr("suggested_posts_in_group", $count)}{/if}</h4>
-        <div id="postz" class="infContainer">
+        <div id="postz" class="infContainer scroll_container">
             {var $microblog = $thisUser->hasMicroblogEnabled()}
-            <div class="infObj" n:foreach="$posts as $post">
+            <div class="infObj scroll_node" n:foreach="$posts as $post">
                 {if $microblog}
                     {include "../components/post/microblogpost.xml", post => $post, commentSection => false, suggestion => true, forceNoCommentsLink => true, forceNoPinLink => true, forceNoLike => true, forceNoShareLink => true, forceNoDeleteLink => false}
                 {else}
diff --git a/Web/Presenters/templates/Messenger/Index.xml b/Web/Presenters/templates/Messenger/Index.xml
index 5c62bf9f..b045c06f 100644
--- a/Web/Presenters/templates/Messenger/Index.xml
+++ b/Web/Presenters/templates/Messenger/Index.xml
@@ -17,9 +17,9 @@
     </div>
 
     {if sizeof($corresps) > 0}
-        <div class="crp-list">
+        <div class="crp-list scroll_container">
             <div n:foreach="$corresps as $coresp"
-                 class="crp-entry"
+                 class="scroll_node crp-entry"
                  onmousedown="window.location.href = {$coresp->getURL()};" >
                 {var $recipient = $coresp->getCorrespondents()[1]}
                 {var $lastMsg   = $coresp->getPreviewMessage()}
diff --git a/Web/Presenters/templates/Notes/List.xml b/Web/Presenters/templates/Notes/List.xml
index 7f2fcf7c..3189a146 100644
--- a/Web/Presenters/templates/Notes/List.xml
+++ b/Web/Presenters/templates/Notes/List.xml
@@ -1,6 +1,5 @@
 {extends "../@listView.xml"}
 {var $iterator = iterator_to_array($notes)}
-{var $page     = $paginatorConf->page}
 
 {block title}{_notes}{/block}
 
@@ -60,12 +59,12 @@
         }
     </style>
 
-    <div class="container_gray" style="background: white; border-top: none;">
+    <div class="container_gray scroll_container" style="background: white; border-top: none;">
 
         {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
         {if sizeof($data) > 0}
 
-        <div n:foreach="$data as $dat">
+        <div class='scroll_node' n:foreach="$data as $dat">
             <div class="profile_thumb">
                 <a href="{$owner->getURL()}">
                 <img src="{$owner->getAvatarUrl('miniscule')}" style="width: 50px;">
@@ -106,7 +105,15 @@
                 </div>
             </article>    
         </div>      
-
+        
+        {include "../components/paginator.xml", conf => (object) [
+            "page"     => $page,
+            "count"    => $count,
+            "amount"   => sizeof($data),
+            "perPage"  => 10,
+            "atBottom" => true,
+        ]}
+        
         {else}
             {if isset($thisUser) && $thisUser->getId() == $owner->getId()}
 
diff --git a/Web/Presenters/templates/Notification/Feed.xml b/Web/Presenters/templates/Notification/Feed.xml
index 04d3a25d..f0470d6c 100644
--- a/Web/Presenters/templates/Notification/Feed.xml
+++ b/Web/Presenters/templates/Notification/Feed.xml
@@ -21,8 +21,8 @@
 </div>
 {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
 {if sizeof($data) > 0}
-<div>
-    <table class="post post-divider" border="0" style="font-size: 11px;" n:foreach="$data as $dat">
+<div n:class="$mode !== 'new' ? scroll_container">
+    <table class="scroll_node post post-divider" border="0" style="font-size: 11px;" n:foreach="$data as $dat">
         <tbody>
             <tr>
                 {var $sxModel = $dat->getModel(1)}
diff --git a/Web/Presenters/templates/Photos/Album.xml b/Web/Presenters/templates/Photos/Album.xml
index e8157dd1..fa779e11 100644
--- a/Web/Presenters/templates/Photos/Album.xml
+++ b/Web/Presenters/templates/Photos/Album.xml
@@ -30,10 +30,10 @@
     {/if}
     <br/><br/>
     {if $album->getPhotosCount() > 0}
-        <div class="container_gray album-flex">
+        <div class="container_gray scroll_container album-flex">
             {foreach $photos as $photo}
                 {php if($photo->isDeleted()) continue; }
-                <div class="album-photo">
+                <div class="album-photo scroll_node">
                     <a
                     n:if="!is_null($thisUser) && $album->canBeModifiedBy($thisUser)"
                     href="/album{$album->getPrettyId()}/remove_photo/{$photo->getId()}" class="album-photo--delete">
diff --git a/Web/Presenters/templates/Search/Index.xml b/Web/Presenters/templates/Search/Index.xml
index 6cbf1036..a1d49b1c 100644
--- a/Web/Presenters/templates/Search/Index.xml
+++ b/Web/Presenters/templates/Search/Index.xml
@@ -29,10 +29,10 @@
                 </div>
 
                 <div class='page_wrap_content' id='search_page'>
-                    <div n:class='page_wrap_content_main, $section == "audios" && $count > 0 ? audios_padding'>
+                    <div n:class='page_wrap_content_main, scroll_container, $section == "audios" && $count > 0 ? audios_padding'>
                         {if $count > 0}
                             {if $section === 'users'}
-                                <div class='search_content content def_row_content' n:foreach="$data as $dat">
+                                <div class='scroll_node search_content content def_row_content' n:foreach="$data as $dat">
                                     <table>
                                         <tbody>
                                             <tr>
@@ -125,10 +125,14 @@
                                 </div>
 
                                 <script n:if='$count > 0 && !empty($query)'>
-                                    highlightText({$query}, '.page_wrap_content_main', ['text'])
+                                    function __scrollHook(page) {
+                                        highlightText({$query}, '.page_wrap_content_main', ['text'])
+                                    }
+
+                                    __scrollHook()
                                 </script>
                             {elseif $section === 'groups'}
-                                <div class='search_content content def_row_content' n:foreach="$data as $dat">
+                                <div class='scroll_node search_content content def_row_content' n:foreach="$data as $dat">
                                     <table>
                                         <tbody>
                                             <tr>
@@ -180,10 +184,14 @@
                                 </div>
 
                                 <script n:if='$count > 0 && !empty($query)'>
-                                    highlightText({$query}, '.page_wrap_content_main', ['text', "td[data-highlight='_clubDesc']"])
+                                    function __scrollHook(page) {
+                                        highlightText({$query}, '.page_wrap_content_main', ['text', "td[data-highlight='_clubDesc']"])
+                                    }
+
+                                    __scrollHook()
                                 </script>
                             {elseif $section === 'apps'}
-                                <div class='search_content content def_row_content' n:foreach="$data as $dat">
+                                <div class='scroll_node search_content content def_row_content' n:foreach="$data as $dat">
                                     <table>
                                         <tbody>
                                             <tr>
@@ -210,10 +218,14 @@
                                 </div>
 
                                 <script n:if='$count > 0 && !empty($query)'>
-                                    highlightText({$query}, '.page_wrap_content_main', ['text', "span[data-highlight='_appDesc']"])
+                                    function __scrollHook(page) {
+                                        highlightText({$query}, '.page_wrap_content_main', ['text', "span[data-highlight='_appDesc']"])
+                                    }
+
+                                    __scrollHook()
                                 </script>
                             {elseif $section === 'posts'}
-                                <div class='search_content' n:foreach="$data as $dat">
+                                <div class='scroll_node search_content' n:foreach="$data as $dat">
                                     {if !$dat || $dat->getWallOwner()->isHideFromGlobalFeedEnabled()}
                                         {_closed_group_post}.
                                     {else}
@@ -222,31 +234,47 @@
                                 </div>
 
                                 <script n:if='$count > 0 && !empty($query)'>
-                                    highlightText({$query}, '.page_wrap_content_main', [".post:not(.comment) > tbody > tr > td > .post-content > .text .really_text"])
+                                    function __scrollHook(page) {
+                                        highlightText({$query}, '.page_wrap_content_main', [".post:not(.comment) > tbody > tr > td > .post-content > .text .really_text"])
+                                    }
+
+                                    __scrollHook()
                                 </script>
                             {elseif $section === 'videos'}
-                                <div class='search_content' n:foreach="$data as $dat">
+                                <div class='scroll_node search_content' n:foreach="$data as $dat">
                                     {include "../components/video.xml", video => $dat}
                                 </div>    
 
                                 <script n:if='$count > 0 && !empty($query)'>
-                                    highlightText({$query}, '.page_wrap_content_main', [".video_name", ".video_description"])
+                                    function __scrollHook(page) {
+                                        highlightText({$query}, '.page_wrap_content_main', [".video_name", ".video_description"])
+                                    }
+
+                                    __scrollHook()
                                 </script>
                             {elseif $section === 'audios'}
-                                <div class='search_content' n:foreach="$data as $dat">
+                                <div class='scroll_node search_content' n:foreach="$data as $dat">
                                     {include "../Audio/player.xml", audio => $dat}
                                 </div>
                                 
                                 <script n:if="$count > 0 && !empty($query) && empty($_REQUEST['only_performers'])">
-                                    highlightText({$query}, '.page_wrap_content_main', [".mediaInfo .performer a", ".mediaInfo .title"])
+                                    function __scrollHook(page) {
+                                        highlightText({$query}, '.page_wrap_content_main', [".mediaInfo .performer a", ".mediaInfo .title"])
+                                    }
+
+                                    __scrollHook()
                                 </script>
                             {elseif $section === 'audios_playlists'}
-                                <div class='search_content' n:foreach="$data as $dat">
+                                <div class='scroll_node search_content' n:foreach="$data as $dat">
                                     {include "../Audio/playlistListView.xml", playlist => $dat}
                                 </div>
 
                                 <script n:if="$count > 0 && !empty($query) && empty($_REQUEST['only_performers'])">
-                                    highlightText({$query}, '.page_wrap_content_main', [".playlistName", ".playlistDesc"])
+                                    function __scrollHook(page) {
+                                        highlightText({$query}, '.page_wrap_content_main', [".playlistName", ".playlistDesc"])
+                                    }
+
+                                    __scrollHook()
                                 </script>
                             {/if}
                         {else}
diff --git a/Web/Presenters/templates/User/Groups.xml b/Web/Presenters/templates/User/Groups.xml
index bca006e8..d3528268 100644
--- a/Web/Presenters/templates/User/Groups.xml
+++ b/Web/Presenters/templates/User/Groups.xml
@@ -2,6 +2,7 @@
 {var $iterator = $user->getClubs($page, $admin)}
 {var $count    = $user->getClubCount($admin)}
 
+{block noscroll}{/block}
 {block title}
     {_groups}
 {/block} 
@@ -121,8 +122,8 @@
                 <h4>{_search_group}</h4>
                 <span>{_search_group_desc}</span>
                 <form action="/search">
-                    <input name="type" type="hidden" value="groups">
-                    <input name="query" class="header_search_input" value="" style="background: none; width: 155px; padding-left: 3px;">
+                    <input name="section" type="hidden" value="groups">
+                    <input name="q" class="header_search_input" value="" style="background: none; width: 155px; padding-left: 3px;">
                     <button class="button">{_search_by_groups}</button>
                 </form>
             </div>
diff --git a/Web/Presenters/templates/Wall/Feed.xml b/Web/Presenters/templates/Wall/Feed.xml
index e609419c..94658a81 100644
--- a/Web/Presenters/templates/Wall/Feed.xml
+++ b/Web/Presenters/templates/Wall/Feed.xml
@@ -23,10 +23,12 @@
         {include "../components/textArea.xml", route => "/wall" . $thisUser->getId() . "/makePost", graffiti => true, polls => true, notes => true, hasSource => true}
     </div>
     
-    {foreach $posts as $post}
-        <a name="postGarter={$post->getId()}"></a>
-        {include "../components/post.xml", post => $post, onWallOf => true, commentSection => true}
-    {/foreach}
+    <div class='scroll_container'>
+        <div class='scroll_node' n:foreach='$posts as $post'>
+            <a name="postGarter={$post->getId()}"></a>
+            {include "../components/post.xml", post => $post, onWallOf => true, commentSection => true}
+        </div>
+    </div>
 
     <div class="postFeedBottom">
         <div class="postFeedPaginator">
@@ -55,8 +57,4 @@
             window.location.assign(url.replace("__padding", e.target.value));
         });
     </script>
-
-    {if isset($thisUser) && $thisUser->hasMicroblogEnabled()}
-        {script "js/al_comments.js"}
-    {/if}
 {/block}
diff --git a/Web/Presenters/templates/Wall/Wall.xml b/Web/Presenters/templates/Wall/Wall.xml
index 80452295..4d3d1cee 100644
--- a/Web/Presenters/templates/Wall/Wall.xml
+++ b/Web/Presenters/templates/Wall/Wall.xml
@@ -31,13 +31,13 @@
                 {include "../components/textArea.xml", route => "/wall$owner/makePost", hasSource => true}
             </div>
             
-            <div class="content">
+            <div class="content scroll_container">
                 {if sizeof($posts) > 0}
-                    {foreach $posts as $post}
+                    <div class='scroll_node' n:foreach='$posts as $post'>
                         <a name="postGarter={$post->getId()}"></a>
                         
                         {include "../components/post.xml", post => $post, commentSection => true}
-                    {/foreach}
+                    </div>
                     {include "../components/paginator.xml", conf => $paginatorConf}
                 {else}
                     {_no_posts_abstract}
@@ -45,8 +45,4 @@
             </div>
         </div>
     </div>
-
-    {if isset($thisUser) && $thisUser->hasMicroblogEnabled()}
-        {script "js/al_comments.js"}
-    {/if}
 {/block}
diff --git a/Web/Presenters/templates/components/comments.xml b/Web/Presenters/templates/components/comments.xml
index 0df0c91f..53253b5f 100644
--- a/Web/Presenters/templates/components/comments.xml
+++ b/Web/Presenters/templates/components/comments.xml
@@ -9,14 +9,14 @@
 </div>
 
 {if sizeof($comments) > 0}
-    {foreach $comments as $comment}
-        {include "comment.xml", comment => $comment}
-    {/foreach}
+    <div class='scroll_container'>
+        <div class='scroll_node' n:foreach="$comments as $comment">
+            {include "comment.xml", comment => $comment}
+        </div>
+    </div>
     <div style="margin-top: 11px;">
         {include "paginator.xml", conf => (object) ["page" => $page, "count" => $count, "amount" => sizeof($comments), "perPage" => 10]}
     </div>
 {else}
     {_comments_tip}
 {/if}
-
-{script "js/al_comments.js"}
diff --git a/Web/Presenters/templates/components/paginator.xml b/Web/Presenters/templates/components/paginator.xml
index 0dffd004..3db45de9 100644
--- a/Web/Presenters/templates/components/paginator.xml
+++ b/Web/Presenters/templates/components/paginator.xml
@@ -2,7 +2,7 @@
 {var $pageCount = ceil($conf->count / $conf->perPage)}
 
 <div n:if="!($conf->page === 1 && $conf->count <= $conf->perPage)" n:attr="style => (!$conf->tidy ? 'padding: 8px;')">
-    <div n:class="paginator, ($conf->atBottom ?? false) ? paginator-at-bottom, ($conf->tidy ? 'tidy')">
+    <div n:class="paginator, (($conf->atTop || $atTop) ?? false) ? paginator-at-top, ($conf->atBottom ?? false) ? paginator-at-bottom, ($conf->tidy ? 'tidy')">
         <a n:if="$conf->page > $space" n:attr="class => ($conf->page === 1 ? 'active')" href="?{http_build_query(array_merge($_GET, ['p' => 1]), 'k', '&', PHP_QUERY_RFC3986)}">&laquo;</a>
         {for $j = $conf->page - ($space-1); $j <= $conf->page + ($space-1); $j++}
             <a n:if="$j > 0 && $j <= $pageCount" n:attr="class => ($conf->page === $j ? 'active')" href="?{http_build_query(array_merge($_GET, ['p' => $j]), 'k', '&', PHP_QUERY_RFC3986)}">{$j}</a>
diff --git a/Web/Presenters/templates/components/wall.xml b/Web/Presenters/templates/components/wall.xml
index 91e08fbc..b00681a1 100644
--- a/Web/Presenters/templates/components/wall.xml
+++ b/Web/Presenters/templates/components/wall.xml
@@ -13,13 +13,13 @@
             {include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true, polls => true, notes => true, hasSource => true}
         </div>
         
-        <div class="content">
+        <div class="content scroll_container">
             {if sizeof($posts) > 0}
-                {foreach $posts as $post}
+                <div class='scroll_node' n:foreach='$posts as $post'>
                     <a name="postGarter={$post->getId()}"></a>
                     
                     {include "../components/post.xml", post => $post, commentSection => true}
-                {/foreach}
+                </div>
                 {include "../components/paginator.xml", conf => $paginatorConf}
             {else}
                 {_no_posts_abstract}
@@ -28,7 +28,3 @@
     </div>
     </div>
 </div>
-
-{if isset($thisUser) && $thisUser->hasMicroblogEnabled()}
-    {script "js/al_comments.js"}
-{/if}
diff --git a/Web/static/css/main.css b/Web/static/css/main.css
index e5591d13..18455cba 100644
--- a/Web/static/css/main.css
+++ b/Web/static/css/main.css
@@ -1435,7 +1435,7 @@ textarea {
 
 .toTop {
     position: fixed;
-    padding: 20px;
+    padding: 12px;
     width: 100px;
     height: 100%;
     background-color: #f3f3f3;
@@ -1445,9 +1445,34 @@ textarea {
     opacity: 0;
     transition: .1s all;
     z-index: 129;
+    user-select: none;
 }
 
-body.scrolled .toTop:hover {
+.toTop > div svg {
+    display: inline-block;
+    margin-right: 2px;
+    width: 8px;
+    height: 7px;
+    fill: #3f3f3f;
+}
+
+.toTop > div span {
+    font-weight: bold;
+}
+
+.toTop.has_down #to_up, .toTop #to_back {
+    display: none;
+}
+
+.toTop.has_down #to_back {
+    display: block;
+}
+
+.toTop.has_down {
+    opacity: .3;
+}
+
+body.scrolled .toTop:hover, .toTop.has_down:hover {
     opacity: .5;
     cursor: pointer;
 }
diff --git a/Web/static/js/al_comments.js b/Web/static/js/al_comments.js
index f4172428..ad398637 100644
--- a/Web/static/js/al_comments.js
+++ b/Web/static/js/al_comments.js
@@ -1,4 +1,4 @@
-u(".comment-reply").on("click", function(e) {
+u(document).on("click", ".comment-reply", function(e) {
     let comment   = u(e.target).closest(".post");
     let authorId  = comment.data("owner-id");
     let authorNm  = u(".post-author > a > b", comment.first()).text().trim();
@@ -8,6 +8,8 @@ u(".comment-reply").on("click", function(e) {
     let mention   = ("[" + (fromGroup ? "club" : "id") + authorId + "|" + authorNm + "], ");
     
     // Substitute pervious mention if present, prepend otherwise
-    inputbox.nodes[0].value = inputbox.nodes[0].value.replace(/(^\[([A-Za-z0-9]+)\|([\p{L} 0-9@]+)\], |^)/u, mention);
+    inputbox.nodes.forEach(node => {
+        node.value = node.value.replace(/(^\[([A-Za-z0-9]+)\|([\p{L} 0-9@]+)\], |^)/u, mention);
+    })
     inputbox.trigger("focusin");
 });
diff --git a/Web/static/js/al_feed.js b/Web/static/js/al_feed.js
index 2831d6f1..7ca8a798 100644
--- a/Web/static/js/al_feed.js
+++ b/Web/static/js/al_feed.js
@@ -84,6 +84,7 @@ u(document).on('click', '#__feed_settings_link', (e) => {
                 const CURRENT_PERPAGE = Number(__temp_url.searchParams.get('posts') ?? 10)
                 const CURRENT_PAGE = Number(__temp_url.searchParams.get('p') ?? 1)
                 const CURRENT_RETURN_BANNED = Number(__temp_url.searchParams.get('return_banned') ?? 0)
+                const CURRENT_AUTO_SCROLL = Number(localStorage.getItem('ux.auto_scroll') ?? 1)
                 const COUNT = [1, 5, 10, 20, 30, 40, 50]
                 u('#_feed_settings_container #__content').html(`
                     <table cellspacing="7" cellpadding="0" border="0" align="center">
@@ -107,11 +108,21 @@ u(document).on('click', '#__feed_settings_link', (e) => {
                             <tr>
                                 <td width="120" valign="top">
                                     <span class="nobold">
-                                        <input type='checkbox' id="showIgnored" ${CURRENT_RETURN_BANNED == 1 ? 'checked' : ''}>
+                                        <input type='checkbox' name='showIgnored' id="showIgnored" ${CURRENT_RETURN_BANNED == 1 ? 'checked' : ''}>
                                     </span>
                                 </td>
                                 <td>
-                                ${tr('show_ignored_sources')}
+                                    <label for='showIgnored'>${tr('show_ignored_sources')}</label>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td width="120" valign="top">
+                                    <span class="nobold">
+                                        <input type='checkbox' data-act='localstorage_item' name='ux.auto_scroll' id="ux.auto_scroll" ${CURRENT_AUTO_SCROLL == 1 ? 'checked' : ''}>
+                                    </span>
+                                </td>
+                                <td>
+                                    <label for='ux.auto_scroll'>${tr('auto_scroll')}</label>
                                 </td>
                             </tr>
                             <tr>
@@ -268,3 +279,7 @@ u(document).on('click', '#__feed_settings_link', (e) => {
 
     __switchTab('main')
 })
+
+u(document).on('change', `input[data-act='localstorage_item']`, (e) => {
+    localStorage.setItem(e.target.name, Number(e.target.checked))
+})
diff --git a/Web/static/js/al_mentions.js b/Web/static/js/al_mentions.js
index 1be77563..d324cca6 100644
--- a/Web/static/js/al_mentions.js
+++ b/Web/static/js/al_mentions.js
@@ -19,7 +19,8 @@ var tooltipTemplate = Handlebars.compile(`
     </table>
 `);
 
-tippy(".mention", {
+tippy.delegate("body", {
+    target: '.mention',
     theme: "light vk",
     content: "⌛",
     allowHTML: true,
diff --git a/Web/static/js/al_music.js b/Web/static/js/al_music.js
index ac609358..2c8e11db 100644
--- a/Web/static/js/al_music.js
+++ b/Web/static/js/al_music.js
@@ -631,6 +631,35 @@ class bigPlayer {
             duration: this.tracks["currentTrack"].length
         })
     }
+
+    loadContextPage(page, lesser = false) {
+        const formdata = new FormData()
+        formdata.append("context", this.context["context_type"])
+        formdata.append("context_entity", this.context["context_id"])
+        formdata.append("hash", u("meta[name=csrf]").attr("value"))
+        formdata.append("page", page)
+
+        ky.post("/audios/context", {
+            hooks: {
+                afterResponse: [
+                    async (_request, _options, response) => {
+                        const newArr = await response.json()
+
+                        if(lesser)
+                            this.tracks["tracks"] = newArr["items"].concat(this.tracks["tracks"])
+                        else
+                            this.tracks["tracks"] = this.tracks["tracks"].concat(newArr["items"])
+
+                        this.context["playedPages"].push(String(newArr["page"]))
+                        
+                        this.updateButtons()
+                        console.info("Loaded context for page " + page)
+                    }
+                ]
+            }, 
+            body: formdata
+        })
+    }
 }
 
 document.addEventListener("DOMContentLoaded", function() {
diff --git a/Web/static/js/al_suggestions.js b/Web/static/js/al_suggestions.js
index 1c3da3e8..befe78b7 100644
--- a/Web/static/js/al_suggestions.js
+++ b/Web/static/js/al_suggestions.js
@@ -201,7 +201,7 @@ $(document).on("click", ".sugglist a", (e) => {
 })
 
 // нажатие на пагинатор у постов предложки
-$(document).on("click", "#postz .paginator a", (e) => {
+/*$(document).on("click", "#postz .paginator a", (e) => {
     e.preventDefault()
     
     ky(e.currentTarget.href, {
@@ -228,4 +228,4 @@ $(document).on("click", "#postz .paginator a", (e) => {
             ]
         }
     })
-})
+})*/
diff --git a/Web/static/js/al_wall.js b/Web/static/js/al_wall.js
index 29722564..6430e47c 100644
--- a/Web/static/js/al_wall.js
+++ b/Web/static/js/al_wall.js
@@ -399,6 +399,7 @@ async function OpenVideo(video_arr = [], init_player = true)
             details.find('.media-page-wrapper-description b').remove()
 
             u('#ovk-player-info').html(details.html())
+            bsdnHydrate()
         }
     })
 
@@ -542,7 +543,8 @@ var tooltipClientNoInfoTemplate = Handlebars.compile(`
     </table>
 `);
 
-tippy(".client_app", {
+tippy.delegate("body", {
+    target: '.client_app',
     theme: "light vk",
     content: "⌛",
     allowHTML: true,
@@ -2003,6 +2005,85 @@ $(document).on("click", ".avatarDelete", (e) => {
     ]);
 })
 
+async function __processPaginatorNextPage(page)
+{
+    const container = u('.scroll_container')
+    const container_node = '.scroll_node'
+    const parser = new DOMParser
+
+    const replace_url = new URL(location.href)
+    replace_url.searchParams.set('p', page)
+    /*replace_url.searchParams.set('al', 1)
+    replace_url.searchParams.set('hash', u("meta[name=csrf]").attr("value"))*/
+
+    const new_content = await fetch(replace_url.href)
+    const new_content_response = await new_content.text()
+    const parsed_content = parser.parseFromString(new_content_response, 'text/html')
+
+    const nodes = parsed_content.querySelectorAll(container_node)
+    nodes.forEach(node => {
+        container.append(node)
+    })
+
+    u(`.paginator:not(.paginator-at-top)`).html(parsed_content.querySelector('.paginator:not(.paginator-at-top)').innerHTML)
+    if(u(`.paginator:not(.paginator-at-top)`).nodes[0].closest('.scroll_container')) {
+        container.nodes[0].append(u(`.paginator:not(.paginator-at-top)`).nodes[0].parentNode)
+    }
+    
+    if(window.player) {
+        window.player.loadContextPage(page)
+    }
+
+    if(typeof __scrollHook != 'undefined') {
+        __scrollHook(page)
+    }
+}
+
+const showMoreObserver = new IntersectionObserver(entries => {
+    entries.forEach(async x => {
+        if(x.isIntersecting) {
+            if(Number(localStorage.getItem('ux.auto_scroll') ?? 1) == 0) {
+                return
+            }
+
+            if(u('.scroll_container').length < 1) {
+                return
+            }
+            
+            const target = u(x.target)
+            if(target.length < 1 || target.hasClass('paginator-at-top')) {
+                return
+            }
+
+            const current_url = new URL(location.href)
+            if(current_url.searchParams && !isNaN(parseInt(current_url.searchParams.get('p')))) {
+                return
+            }
+
+            target.addClass('lagged')
+            const active_tab = target.find('.active')
+            const next_page  = u(active_tab.nodes[0] ? active_tab.nodes[0].nextElementSibling : null)
+            if(next_page.length < 1) {
+                u('.paginator:not(.paginator-at-top)').removeClass('lagged')
+                return
+            }
+
+            const page_number = Number(next_page.html())
+            await __processPaginatorNextPage(page_number)
+            bsdnHydrate()
+            u('.paginator:not(.paginator-at-top)').removeClass('lagged')
+        }
+    })
+}, {
+    root: null,
+    rootMargin: '0px',
+    threshold: 0,
+})
+
+if(u('.paginator:not(.paginator-at-top)').length > 0) {
+    showMoreObserver.observe(u('.paginator:not(.paginator-at-top)').nodes[0])
+}
+
 u(document).on('click', '#__sourceAttacher', (e) => {
     MessageBox(tr('add_source'), `
         <div id='source_flex_kunteynir'>
diff --git a/Web/static/js/openvk.cls.js b/Web/static/js/openvk.cls.js
index 428fd165..709630c8 100644
--- a/Web/static/js/openvk.cls.js
+++ b/Web/static/js/openvk.cls.js
@@ -63,7 +63,7 @@ document.addEventListener("DOMContentLoaded", function() { //BEGIN
     });
     /* @rem-pai why this func wasn't named as "#_deleteDialog"? It looks universal IMO */
 
-    u("#_noteDelete").on("click", function(e) {
+    u(document).on("click", "#_noteDelete", function(e) {
         var formHtml = "<form id='tmpPhDelF' action='" + u(this).attr("href") + "' >";
         formHtml    += "<input type='hidden' name='hash' value='" + u("meta[name=csrf]").attr("value") + "' />";
         formHtml    += "</form>";
@@ -140,7 +140,7 @@ document.addEventListener("DOMContentLoaded", function() { //BEGIN
         return false;
     });
 
-    u("#_submitUserSubscriptionAction").handle("submit", async function(e) {
+    u(document).handle("submit", "#_submitUserSubscriptionAction", async function(e) {
         u(this).nodes[0].parentElement.classList.add('loading');
         u(this).nodes[0].parentElement.classList.add('disable');
         console.log(e.target);
diff --git a/Web/static/js/scroll.js b/Web/static/js/scroll.js
index 3b652afd..dff0ca4b 100644
--- a/Web/static/js/scroll.js
+++ b/Web/static/js/scroll.js
@@ -1,14 +1,35 @@
 window.addEventListener("scroll", function(e) {
     if(window.scrollY < 100) {
+        if(window.temp_y_scroll) {
+            u('.toTop').addClass('has_down')
+        }
         document.body.classList.toggle("scrolled", false);
     } else {
         document.body.classList.toggle("scrolled", true);
+        u('.toTop').removeClass('has_down')
     }
 });
 
 u(".toTop").on("click", function(e) {
-    window.scrollTo({
-        top: 0,
-        behavior: "smooth"
-    });
-});
\ No newline at end of file
+    const y_scroll = window.scrollY
+    const scroll_margin = 20
+
+    if(y_scroll > 100) {
+        window.temp_y_scroll = y_scroll
+        window.scrollTo(0, scroll_margin)
+        window.scrollTo({
+            top: 0,
+            behavior: "smooth"
+        })
+    } else {
+        if(window.temp_y_scroll) {
+            window.scrollTo(0, window.temp_y_scroll - scroll_margin)
+            window.scrollTo({
+                top: window.temp_y_scroll,
+                behavior: "smooth"
+            })
+        }
+    }
+
+    u(document).trigger('scroll')
+})
diff --git a/locales/en.strings b/locales/en.strings
index 7a84ae09..c1ff0822 100644
--- a/locales/en.strings
+++ b/locales/en.strings
@@ -220,6 +220,7 @@
 "all_news" = "All news";
 "posts_per_page" = "Number of posts per page";
 "show_ignored_sources" = "Show ignored sources";
+"auto_scroll" = "Autoscroll";
 
 "attachment" = "Attachment";
 "post_as_group" = "Post as group";
diff --git a/locales/ru.strings b/locales/ru.strings
index 33dd70d1..8a2d4e8c 100644
--- a/locales/ru.strings
+++ b/locales/ru.strings
@@ -205,6 +205,7 @@
 "all_news" = "Все новости";
 "posts_per_page" = "Количество записей на странице";
 "show_ignored_sources" = "Показывать игнорируемые источники";
+"auto_scroll" = "Автоматическая прокрутка";
 "attachment" = "Вложение";
 "post_as_group" = "От имени сообщества";
 "comment_as_group" = "От имени сообщества";