Users, Groups: Add a block with links

This is similar to the links that are in the original VK, but unlike the original, links can be not only the group but also the page (Linktree moment).
This commit is contained in:
Maxim Leshchenko 2022-09-25 17:47:25 +02:00
parent 7d72cd182b
commit 1b71a3ad25
No known key found for this signature in database
GPG key ID: BB9C44A8733FBEEE
12 changed files with 488 additions and 2 deletions

View file

@ -0,0 +1,117 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use Nette\Utils\Image;
use Nette\Utils\UnknownImageFileException;
use openvk\Web\Models\Repositories\Users;
use openvk\Web\Models\Repositories\Clubs;
use openvk\Web\Models\RowModel;
class Link extends RowModel
{
protected $tableName = "links";
private function getIconsDir(): string
{
$uploadSettings = OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"];
if($uploadSettings["mode"] === "server" && $uploadSettings["server"]["kind"] === "cdn")
return $uploadSettings["server"]["directory"];
else
return OPENVK_ROOT . "/storage/";
}
function getId(): int
{
return $this->getRecord()->id;
}
function getOwner(): RowModel
{
$ownerId = (int) $this->getRecord()->owner;
if($ownerId > 0)
return (new Users)->get($ownerId);
else
return (new Clubs)->get($ownerId * -1);
}
function getTitle(): string
{
return $this->getRecord()->title;
}
function getDescription(): ?string
{
return $this->getRecord()->description;
}
function getDescriptionOrDomain(): string
{
$description = $this->getDescription();
if(is_null($description))
return $this->getDomain();
else
return $description;
}
function getUrl(): string
{
return $this->getRecord()->url;
}
function getIconUrl(): string
{
$serverUrl = ovk_scheme(true) . $_SERVER["HTTP_HOST"];
if(is_null($this->getRecord()->icon_hash))
return "$serverUrl/assets/packages/static/openvk/img/camera_200.png";
$hash = $this->getRecord()->icon_hash;
switch(OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["mode"]) {
default:
case "default":
case "basic":
return "$serverUrl/blob_" . substr($hash, 0, 2) . "/$hash" . "_link_icon.png";
case "accelerated":
return "$serverUrl/openvk-datastore/$hash" . "_link_icon.png";
case "server":
$settings = (object) OPENVK_ROOT_CONF["openvk"]["preferences"]["uploads"]["server"];
return (
$settings->protocol ?? ovk_scheme() .
"://" . $settings->host .
$settings->path .
substr($hash, 0, 2) . "/$hash" . "_link_icon.png"
);
}
}
function setIcon(array $file): int
{
if($file["error"] !== UPLOAD_ERR_OK)
return -1;
try {
$image = Image::fromFile($file["tmp_name"]);
} catch (UnknownImageFileException $e) {
return -2;
}
$hash = hash_file("adler32", $file["tmp_name"]);
if(!is_dir($this->getIconsDir() . substr($hash, 0, 2)))
if(!mkdir($this->getIconsDir() . substr($hash, 0, 2)))
return -3;
$image->resize(140, 140, Image::STRETCH);
$image->save($this->getIconsDir() . substr($hash, 0, 2) . "/$hash" . "_link_icon.png");
$this->stateChanges("icon_hash", $hash);
return 0;
}
function getDomain(): string
{
return parse_url($this->getUrl(), PHP_URL_HOST);
}
use Traits\TOwnable;
}

View file

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Link;
use Chandler\Database\DatabaseConnection;
class Links
{
private $context;
private $links;
function __construct()
{
$this->context = DatabaseConnection::i()->getContext();
$this->links = $this->context->table("links");
}
function get(int $id): ?Link
{
$link = $this->links->get($id);
if(!$link) return NULL;
return new Link($link);
}
function getByOwnerId(int $ownerId, int $page = 1, ?int $perPage = NULL): \Traversable
{
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
$links = $this->links->where("owner", $ownerId)->page($page, $perPage);
foreach($links as $link)
yield new Link($link);
}
function getCountByOwnerId(int $id): int
{
return sizeof($this->links->where("owner", $id));
}
}

View file

@ -2,7 +2,7 @@
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Club, Photo}; use openvk\Web\Models\Entities\{Club, Photo};
use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification;
use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics}; use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Links};
use Chandler\Security\Authenticator; use Chandler\Security\Authenticator;
final class GroupPresenter extends OpenVKPresenter final class GroupPresenter extends OpenVKPresenter
@ -27,6 +27,8 @@ final class GroupPresenter extends OpenVKPresenter
$this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club);
$this->template->topics = (new Topics)->getLastTopics($club, 3); $this->template->topics = (new Topics)->getLastTopics($club, 3);
$this->template->topicsCount = (new Topics)->getClubTopicsCount($club); $this->template->topicsCount = (new Topics)->getClubTopicsCount($club);
$this->template->links = (new Links)->getByOwnerId($club->getId() * -1, 1, 5);
$this->template->linksCount = (new Links)->getCountByOwnerId($club->getId() * -1);
$this->template->club = $club; $this->template->club = $club;
} }

View file

@ -0,0 +1,137 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Link;
use openvk\Web\Models\Repositories\{Links, Clubs, Users};
final class LinksPresenter extends OpenVKPresenter
{
private $links;
function __construct(Links $links)
{
$this->links = $links;
parent::__construct();
}
function renderList(int $ownerId): void
{
$owner = ($ownerId < 0 ? (new Clubs) : (new Users))->get(abs($ownerId));
if(!$owner)
$this->notFound();
$this->template->owner = $owner;
$this->template->ownerId = $ownerId;
$page = (int) ($this->queryParam("p") ?? 1);
$this->template->links = $this->links->getByOwnerId($ownerId, $page);
$this->template->count = $this->links->getCountByOwnerId($ownerId);
$this->template->paginatorConf = (object) [
"count" => $this->template->count,
"page" => $page,
"amount" => NULL,
"perPage" => OPENVK_DEFAULT_PER_PAGE,
];
}
function renderCreate(int $ownerId): void
{
$this->assertUserLoggedIn();
$owner = ($ownerId < 0 ? (new Clubs) : (new Users))->get(abs($ownerId));
if(!$owner)
$this->notFound();
if($ownerId < 0 ? !$owner->canBeModifiedBy($this->user->identity) : $owner->getId() !== $this->user->id)
$this->notFound();
$this->template->_template = "Links/Edit.xml";
$this->template->create = true;
$this->template->owner = $owner;
$this->template->ownerId = $ownerId;
}
function renderEdit(int $ownerId, int $id): void
{
$this->assertUserLoggedIn();
$owner = ($ownerId < 0 ? (new Clubs) : (new Users))->get(abs($ownerId));
if(!$owner)
$this->notFound();
$link = $this->links->get($id);
if(!$link && $id !== 0) // If the link ID is 0, consider the request as link creation
$this->notFound();
if($ownerId < 0 ? !$owner->canBeModifiedBy($this->user->identity) : $owner->getId() !== $this->user->id)
$this->notFound();
if($_SERVER["REQUEST_METHOD"] === "POST") {
$this->willExecuteWriteAction();
$create = $id === 0;
$title = $this->postParam("title");
$description = $this->postParam("description");
$url = $this->postParam("url");
$url = (!parse_url($url, PHP_URL_SCHEME) ? "https://" : "") . $url;
if(!$title || !$url)
$this->flashFail("err", tr($create ? "failed_to_create_link" : "failed_to_change_link"), tr("not_all_data_entered"));
if(!filter_var($url, FILTER_VALIDATE_URL))
$this->flashFail("err", tr($create ? "failed_to_create_link" : "failed_to_change_link"), tr("wrong_address"));
if($create)
$link = new Link;
$link->setOwner($ownerId);
$link->setTitle(ovk_proc_strtr($title, 127));
$link->setDescription($description === "" ? NULL : ovk_proc_strtr($description, 127));
$link->setUrl($url);
if(isset($_FILES["icon"]) && $_FILES["icon"]["size"] > 0) {
if(($res = $link->setIcon($_FILES["icon"])) !== 0)
$this->flashFail("err", tr("unable_to_upload_icon"), tr("unable_to_upload_icon_desc", $res));
}
$link->save();
$this->flash("succ", tr("information_-1"), tr($create ? "link_created" : "link_changed"));
$this->redirect("/links" . $ownerId);
}
if($id === 0) // But there is a separate handler for displaying page with the fields to create, so here we do not skip
$this->notFound();
$this->template->linkId = $link->getId();
$this->template->title = $link->getTitle();
$this->template->description = $link->getDescription();
$this->template->url = $link->getUrl();
$this->template->create = false;
$this->template->owner = $owner;
$this->template->ownerId = $ownerId;
$this->template->link = $link;
}
function renderDelete(int $ownerId, int $id): void
{
$this->assertUserLoggedIn();
$owner = ($ownerId < 0 ? (new Clubs) : (new Users))->get(abs($ownerId));
if(!$owner)
$this->notFound();
$link = $this->links->get($id);
if(!$link)
$this->notFound();
if(!$link->canBeModifiedBy($this->user->identity))
$this->notFound();
$this->willExecuteWriteAction();
$link->delete(false);
$this->flashFail("succ", tr("information_-1"), tr("link_deleted"));
}
}

View file

@ -4,7 +4,7 @@ use openvk\Web\Util\Sms;
use openvk\Web\Themes\Themepacks; use openvk\Web\Themes\Themepacks;
use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification}; use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification};
use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification}; use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification};
use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications}; use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications, Links};
use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Models\Exceptions\InvalidUserNameException;
use openvk\Web\Util\Validator; use openvk\Web\Util\Validator;
use Chandler\Security\Authenticator; use Chandler\Security\Authenticator;
@ -43,6 +43,8 @@ final class UserPresenter extends OpenVKPresenter
$this->template->videosCount = (new Videos)->getUserVideosCount($user); $this->template->videosCount = (new Videos)->getUserVideosCount($user);
$this->template->notes = (new Notes)->getUserNotes($user, 1, 4); $this->template->notes = (new Notes)->getUserNotes($user, 1, 4);
$this->template->notesCount = (new Notes)->getUserNotesCount($user); $this->template->notesCount = (new Notes)->getUserNotesCount($user);
$this->template->links = (new Links)->getByOwnerId($user->getId(), 1, 5);
$this->template->linksCount = (new Links)->getCountByOwnerId($user->getId());
$this->template->user = $user; $this->template->user = $user;
} }

View file

@ -191,6 +191,32 @@
</div> </div>
</div> </div>
</div> </div>
<div n:if="$linksCount > 0 || ($thisUser && $club->canBeModifiedBy($thisUser))">
<div class="content_title_expanded" onclick="hidePanel(this, {$linksCount});">
{_links}
</div>
<div>
<div class="content_subtitle">
{tr("links_count", $linksCount)}
<div style="float: right;">
<a href="/links-{$club->getId()}">{_all_title}</a>
</div>
</div>
<div class="avatar-list">
<div class="avatar-list-item" n:foreach="$links as $link">
<div class="avatar">
<a href="/away.php?to={$link->getUrl()}">
<img height="32" class="ava" src="{$link->getIconUrl()}" />
</a>
</div>
<div class="info">
<a href="/away.php?to={$link->getUrl()}" class="title">{$link->getTitle()}</a>
<div class="subtitle">{$link->getDescriptionOrDomain()}</div>
</div>
</div>
</div>
</div>
</div>
<div n:if="$albumsCount > 0 || ($thisUser && $club->canBeModifiedBy($thisUser))"> <div n:if="$albumsCount > 0 || ($thisUser && $club->canBeModifiedBy($thisUser))">
<div class="content_title_expanded" onclick="hidePanel(this, {$albumsCount});"> <div class="content_title_expanded" onclick="hidePanel(this, {$albumsCount});">
{_albums} {_albums}

View file

@ -0,0 +1,71 @@
{extends "../@layout.xml"}
{block title}
{if $create}
{_new_link}
{else}
{_edit_link} "{$title}"
{/if}
{/block}
{block header}
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a>
»
<a href="/links{$ownerId}">{_links}</a>
»
{if $create}{_new_link}{else}{_edit_link}{/if}
{/block}
{block content}
<div class="container_gray">
<h4>{if $create}{_new_link}{else}{_edit_link}{/if}</h4>
<form method="POST" action="/links{$ownerId}/edit{$linkId ?? 0}" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
<tbody>
<tr>
<td width="120" valign="top">
<span class="nobold">{_title}:</span>
</td>
<td>
<input type="text" name="title" value="{$title ?? ''}" />
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_address}:</span>
</td>
<td>
<input type="text" name="url" value="{$url ?? ''}" />
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_description}:</span>
</td>
<td>
<input type="text" name="description" value="{$description ?? ''}" />
</td>
</tr>
<tr>
<td width="120" valign="top">
<span class="nobold">{_icon}: </span>
</td>
<td>
<input type="file" name="icon" accept="image/*" />
</td>
</tr>
<tr>
<td width="120" valign="top">
</td>
<td>
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_save}" class="button" />
</td>
</tr>
</tbody>
</table>
<input type="hidden" name="hash" value="{$csrfToken}" />
</form>
</div>
{/block}

View file

@ -0,0 +1,42 @@
{extends "../@listView.xml"}
{var $iterator = iterator_to_array($links)}
{var $page = $paginatorConf->page}
{block title}{_links} {$owner->getCanonicalName()}{/block}
{block header}
<a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a> » {_links}
<div n:if="!is_null($thisUser) && ($ownerId > 0 ? $ownerId === $thisUser->getId() : $owner->canBeModifiedBy($thisUser))" style="float: right;">
<a href="/links{$ownerId}/create">{_create_link}</a>
</div>
{/block}
{* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml}
/away.php?to={$x->getUrl()}
{/block}
{block preview}
<img src="{$x->getIconUrl()}" alt="{$x->getTitle()}" width=75 />
{/block}
{block name}
{$x->getTitle()}
{/block}
{block description}
{$x->getDescriptionOrDomain()}
{/block}
{block actions}
{if !is_null($thisUser) && $x->canBeModifiedBy($thisUser)}
<a class="profile_link" href="/links{$ownerId}/edit{$x->getId()}">
{_edit}
</a>
<a class="profile_link" href="/links{$ownerId}/delete{$x->getId()}">
{_delete}
</a>
{/if}
{/block}

View file

@ -229,6 +229,32 @@
</div> </div>
</div> </div>
</div> </div>
<div n:if="$linksCount > 0 || $thisUser != NULL && $user->getId() === $thisUser->getId()">
<div class="content_title_expanded" onclick="hidePanel(this, {$linksCount});">
{_links}
</div>
<div>
<div class="content_subtitle">
{tr("links_count", $linksCount)}
<div style="float: right;">
<a href="/links{$user->getId()}">{_all_title}</a>
</div>
</div>
<div class="avatar-list">
<div class="avatar-list-item" n:foreach="$links as $link">
<div class="avatar">
<a href="/away.php?to={$link->getUrl()}">
<img height="32" class="ava" src="{$link->getIconUrl()}" />
</a>
</div>
<div class="info">
<a href="/away.php?to={$link->getUrl()}" class="title">{$link->getTitle()}</a>
<div class="subtitle">{$link->getDescriptionOrDomain()}</div>
</div>
</div>
</div>
</div>
</div>
<div n:if="$albumsCount > 0 && $user->getPrivacyPermission('photos.read', $thisUser ?? NULL)"> <div n:if="$albumsCount > 0 && $user->getPrivacyPermission('photos.read', $thisUser ?? NULL)">
<div class="content_title_expanded" onclick="hidePanel(this, {$albumsCount});"> <div class="content_title_expanded" onclick="hidePanel(this, {$albumsCount});">
{_albums} {_albums}

View file

@ -24,6 +24,7 @@ services:
- openvk\Web\Presenters\ThemepacksPresenter - openvk\Web\Presenters\ThemepacksPresenter
- openvk\Web\Presenters\VKAPIPresenter - openvk\Web\Presenters\VKAPIPresenter
- openvk\Web\Presenters\BannedLinkPresenter - openvk\Web\Presenters\BannedLinkPresenter
- openvk\Web\Presenters\LinksPresenter
- openvk\Web\Models\Repositories\Users - openvk\Web\Models\Repositories\Users
- openvk\Web\Models\Repositories\Posts - openvk\Web\Models\Repositories\Posts
- openvk\Web\Models\Repositories\Photos - openvk\Web\Models\Repositories\Photos
@ -42,6 +43,7 @@ services:
- openvk\Web\Models\Repositories\Gifts - openvk\Web\Models\Repositories\Gifts
- openvk\Web\Models\Repositories\Topics - openvk\Web\Models\Repositories\Topics
- openvk\Web\Models\Repositories\Applications - openvk\Web\Models\Repositories\Applications
- openvk\Web\Models\Repositories\Links
- openvk\Web\Models\Repositories\ContentSearchRepository - openvk\Web\Models\Repositories\ContentSearchRepository
- openvk\Web\Models\Repositories\Aliases - openvk\Web\Models\Repositories\Aliases
- openvk\Web\Models\Repositories\BannedLinks - openvk\Web\Models\Repositories\BannedLinks

View file

@ -207,6 +207,14 @@ routes:
handler: "Topics->edit" handler: "Topics->edit"
- url: "/topic{num}_{num}/delete" - url: "/topic{num}_{num}/delete"
handler: "Topics->delete" handler: "Topics->delete"
- url: "/links{num}"
handler: "Links->list"
- url: "/links{num}/create"
handler: "Links->create"
- url: "/links{num}/edit{num}"
handler: "Links->edit"
- url: "/links{num}/delete{num}"
handler: "Links->delete"
- url: "/audios{num}" - url: "/audios{num}"
handler: "Audios->app" handler: "Audios->app"
- url: "/audios{num}.json" - url: "/audios{num}.json"

View file

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS `links` (
`id` bigint(20) unsigned NOT NULL,
`owner` bigint(20) NOT NULL,
`title` varchar(128) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`description` varchar(128) COLLATE utf8mb4_unicode_520_ci,
`url` varchar(128) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`icon_hash` char(128) COLLATE utf8mb4_unicode_520_ci
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
ALTER TABLE `links`
ADD PRIMARY KEY (`id`),
ADD KEY `owner` (`owner`);
ALTER TABLE `links`
MODIFY `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT;