Merge branch 'master' into other-gender-options

This commit is contained in:
n1rwana 2022-10-13 12:08:08 +03:00 committed by GitHub
commit 95f8f4a3cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 2193 additions and 2713 deletions

1
.gitignore vendored
View file

@ -10,5 +10,6 @@ tmp/*
themepacks/* themepacks/*
!themepacks/.gitkeep !themepacks/.gitkeep
!themepacks/openvk_modern !themepacks/openvk_modern
!themepacks/midnight
storage/* storage/*
!storage/.gitkeep !storage/.gitkeep

50
ServiceAPI/Mentions.php Normal file
View file

@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\{Users, Clubs};
class Mentions implements Handler
{
protected $user;
function __construct(?User $user)
{
$this->user = $user;
}
function resolve(int $id, callable $resolve, callable $reject): void
{
if($id > 0) {
$user = (new Users)->get($id);
if(!$user) {
$reject("Not found");
return;
}
$resolve([
"url" => $user->getURL(),
"name" => $user->getFullName(),
"ava" => $user->getAvatarURL("miniscule"),
"about" => $user->getStatus() ?? "",
"online" => ($user->isFemale() ? tr("was_online_f") : tr("was_online_m")) . " " . $user->getOnline(),
"verif" => $user->isVerified(),
]);
return;
}
$club = (new Clubs)->get(abs($id));
if(!$club) {
$reject("Not found");
return;
}
$resolve([
"url" => $club->getURL(),
"name" => $club->getName(),
"ava" => $club->getAvatarURL("miniscule"),
"about" => $club->getDescription() ?? "",
"online" => tr("participants", $club->getFollowersCount()),
"verif" => $club->isVerified(),
]);
}
}

70
ServiceAPI/Polls.php Normal file
View file

@ -0,0 +1,70 @@
<?php declare(strict_types=1);
namespace openvk\ServiceAPI;
use Chandler\MVC\Routing\Router;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Exceptions\{AlreadyVotedException, InvalidOptionException, PollLockedException};
use openvk\Web\Models\Repositories\Polls as PollRepo;
use UnexpectedValueException;
class Polls implements Handler
{
protected $user;
protected $polls;
function __construct(?User $user)
{
$this->user = $user;
$this->polls = new PollRepo;
}
private function getPollHtml(int $poll): string
{
return Router::i()->execute("/poll$poll", "SAPI");
}
function vote(int $pollId, string $options, callable $resolve, callable $reject): void
{
$poll = $this->polls->get($pollId);
if(!$poll) {
$reject("Poll not found");
return;
}
try {
$options = explode(",", $options);
$poll->vote($this->user, $options);
} catch(AlreadyVotedException $ex) {
$reject("Poll state changed: user has already voted.");
return;
} catch(PollLockedException $ex) {
$reject("Poll state changed: poll has ended.");
return;
} catch(InvalidOptionException $ex) {
$reject("Foreign options passed.");
return;
} catch(UnexpectedValueException $ex) {
$reject("Too much options passed.");
return;
}
$resolve(["html" => $this->getPollHtml($pollId)]);
}
function unvote(int $pollId, callable $resolve, callable $reject): void
{
$poll = $this->polls->get($pollId);
if(!$poll) {
$reject("Poll not found");
return;
}
try {
$poll->revokeVote($this->user);
} catch(PollLockedException $ex) {
$reject("Votes can't be revoked from this poll.");
return;
}
$resolve(["html" => $this->getPollHtml($pollId)]);
}
}

View file

@ -13,7 +13,7 @@ final class Account extends VKAPIRequestHandler
"last_name" => $this->getUser()->getLastName(), "last_name" => $this->getUser()->getLastName(),
"home_town" => $this->getUser()->getHometown(), "home_town" => $this->getUser()->getHometown(),
"status" => $this->getUser()->getStatus(), "status" => $this->getUser()->getStatus(),
"bdate" => $this->getUser()->getBirthday()->format('%e.%m.%Y'), "bdate" => is_null($this->getUser()->getBirthday()) ? '01.01.1970' : $this->getUser()->getBirthday()->format('%e.%m.%Y'),
"bdate_visibility" => $this->getUser()->getBirthdayPrivacy(), "bdate_visibility" => $this->getUser()->getBirthdayPrivacy(),
"phone" => "+420 ** *** 228", # TODO "phone" => "+420 ** *** 228", # TODO
"relation" => $this->getUser()->getMaritalStatus(), "relation" => $this->getUser()->getMaritalStatus(),

View file

@ -133,15 +133,18 @@ final class Friends extends VKAPIRequestHandler
return $response; return $response;
} }
function getRequests(string $fields = "", int $offset = 0, int $count = 100): object function getRequests(string $fields = "", int $offset = 0, int $count = 100, int $extended = 0): object
{ {
if ($count >= 1000)
$this->fail(100, "One of the required parameters was not passed or is invalid.");
$this->requireUser(); $this->requireUser();
$i = 0; $i = 0;
$offset++; $offset++;
$followers = []; $followers = [];
foreach($this->getUser()->getFollowers() as $follower) { foreach($this->getUser()->getFollowers($offset, $count) as $follower) {
$followers[$i] = $follower->getId(); $followers[$i] = $follower->getId();
$i++; $i++;
} }
@ -149,8 +152,10 @@ final class Friends extends VKAPIRequestHandler
$response = $followers; $response = $followers;
$usersApi = new Users($this->getUser()); $usersApi = new Users($this->getUser());
if(!is_null($fields)) if($extended == 1)
$response = $usersApi->get(implode(',', $followers), $fields, 0, $count); # FIXME $response = $usersApi->get(implode(',', $followers), $fields, 0, $count);
else
$response = $usersApi->get(implode(',', $followers), "", 0, $count);
foreach($response as $user) foreach($response as $user)
$user->user_id = $user->id; $user->user_id = $user->id;

View file

@ -10,7 +10,7 @@ final class Groups extends VKAPIRequestHandler
$this->requireUser(); $this->requireUser();
if($user_id == 0) { if($user_id == 0) {
foreach($this->getUser()->getClubs($offset+1) as $club) foreach($this->getUser()->getClubs($offset, false, $count, true) as $club)
$clbs[] = $club; $clbs[] = $club;
$clbsCount = $this->getUser()->getClubCount(); $clbsCount = $this->getUser()->getClubCount();
} else { } else {
@ -20,7 +20,7 @@ final class Groups extends VKAPIRequestHandler
if(is_null($user)) if(is_null($user))
$this->fail(15, "Access denied"); $this->fail(15, "Access denied");
foreach($user->getClubs($offset+1) as $club) foreach($user->getClubs($offset, false, $count, true) as $club)
$clbs[] = $club; $clbs[] = $club;
$clbsCount = $user->getClubCount(); $clbsCount = $user->getClubCount();
@ -33,17 +33,9 @@ final class Groups extends VKAPIRequestHandler
$ic = $count; $ic = $count;
if(!empty($clbs)) { if(!empty($clbs)) {
$clbs = array_slice($clbs, $offset * $count);
for($i=0; $i < $ic; $i++) { for($i=0; $i < $ic; $i++) {
$usr = $clbs[$i]; $usr = $clbs[$i];
if(is_null($usr)) { if(is_null($usr)) {
$rClubs[$i] = (object)[
"id" => $clbs[$i],
"name" => "DELETED",
"deactivated" => "deleted"
];
} else if($clbs[$i] == NULL) {
} else { } else {
$rClubs[$i] = (object) [ $rClubs[$i] = (object) [
@ -102,23 +94,32 @@ final class Groups extends VKAPIRequestHandler
]; ];
} }
function getById(string $group_ids = "", string $group_id = "", string $fields = ""): ?array function getById(string $group_ids = "", string $group_id = "", string $fields = "", int $offset = 0, int $count = 500): ?array
{ {
/* Both offset and count SHOULD be used only in OpenVK code,
not in your app or script, since it's not oficially documented by VK */
$clubs = new ClubsRepo; $clubs = new ClubsRepo;
if($group_ids == NULL && $group_id != NULL) if(empty($group_ids) && !empty($group_id))
$group_ids = $group_id; $group_ids = $group_id;
if($group_ids == NULL && $group_id == NULL) if(empty($group_ids) && empty($group_id))
$this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined"); $this->fail(100, "One of the parameters specified was missing or invalid: group_ids is undefined");
$clbs = explode(',', $group_ids); $clbs = explode(',', $group_ids);
$response; $response = array();
$ic = sizeof($clbs); $ic = sizeof($clbs);
if(sizeof($clbs) > $count)
$ic = $count;
$clbs = array_slice($clbs, $offset * $count);
for($i=0; $i < $ic; $i++) { for($i=0; $i < $ic; $i++) {
if($i > 500) if($i > 500 || $clbs[$i] == 0)
break; break;
if($clbs[$i] < 0) if($clbs[$i] < 0)
@ -142,6 +143,7 @@ final class Groups extends VKAPIRequestHandler
"screen_name" => $clb->getShortCode() ?? "club".$clb->getId(), "screen_name" => $clb->getShortCode() ?? "club".$clb->getId(),
"is_closed" => false, "is_closed" => false,
"type" => "group", "type" => "group",
"is_member" => !is_null($this->getUser()) ? (int) $clb->getSubscriptionStatus($this->getUser()) : 0,
"can_access_closed" => true, "can_access_closed" => true,
]; ];
@ -204,10 +206,6 @@ final class Groups extends VKAPIRequestHandler
else else
$response[$i]->can_post = $clb->canPost(); $response[$i]->can_post = $clb->canPost();
break; break;
case "is_member":
if(!is_null($this->getUser()))
$response[$i]->is_member = (int) $clb->getSubscriptionStatus($this->getUser());
break;
} }
} }
} }
@ -215,4 +213,24 @@ final class Groups extends VKAPIRequestHandler
return $response; return $response;
} }
function search(string $q, int $offset = 0, int $count = 100)
{
$clubs = new ClubsRepo;
$array = [];
$find = $clubs->find($q);
foreach ($find as $group)
$array[] = $group->getId();
return (object) [
"count" => $find->size(),
"items" => $this->getById(implode(',', $array), "", "is_admin,is_member,is_advertiser,photo_50,photo_100,photo_200", $offset, $count)
/*
* As there is no thing as "fields" by the original documentation
* i'll just bake this param by the example shown here: https://dev.vk.com/method/groups.search
*/
];
}
} }

View file

@ -21,10 +21,17 @@ final class Wall extends VKAPIRequestHandler
$groups = []; $groups = [];
$cnt = $posts->getPostCountOnUserWall($owner_id); $cnt = $posts->getPostCountOnUserWall($owner_id);
$wallOnwer = (new UsersRepo)->get($owner_id); if ($owner_id > 0)
$wallOnwer = (new UsersRepo)->get($owner_id);
else
$wallOnwer = (new ClubsRepo)->get($owner_id * -1);
if(!$wallOnwer || $wallOnwer->isDeleted() || $wallOnwer->isDeleted()) if ($owner_id > 0)
$this->fail(18, "User was deleted or banned"); if(!$wallOnwer || $wallOnwer->isDeleted())
$this->fail(18, "User was deleted or banned");
else
if(!$wallOnwer)
$this->fail(15, "Access denied: wall is disabled"); // Don't search for logic here pls
foreach($posts->getPostsFromUsersWall($owner_id, 1, $count, $offset) as $post) { foreach($posts->getPostsFromUsersWall($owner_id, 1, $count, $offset) as $post) {
$from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId(); $from_id = get_class($post->getOwner()) == "openvk\Web\Models\Entities\Club" ? $post->getOwner()->getId() * (-1) : $post->getOwner()->getId();
@ -37,12 +44,14 @@ final class Wall extends VKAPIRequestHandler
continue; continue;
$attachments[] = $this->getApiPhoto($attachment); $attachments[] = $this->getApiPhoto($attachment);
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = []; $repostAttachments = [];
foreach($attachment->getChildren() as $repostAttachment) { foreach($attachment->getChildren() as $repostAttachment) {
if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) { if($repostAttachment instanceof \openvk\Web\Models\Entities\Photo) {
if($attachment->isDeleted()) if($repostAttachment->isDeleted())
continue; continue;
$repostAttachments[] = $this->getApiPhoto($repostAttachment); $repostAttachments[] = $this->getApiPhoto($repostAttachment);
@ -178,6 +187,8 @@ final class Wall extends VKAPIRequestHandler
foreach($post->getChildren() as $attachment) { foreach($post->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) { if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$attachments[] = $this->getApiPhoto($attachment); $attachments[] = $this->getApiPhoto($attachment);
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $user);
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = []; $repostAttachments = [];
@ -561,7 +572,7 @@ final class Wall extends VKAPIRequestHandler
return 1; return 1;
} }
private function getApiPhoto($attachment) { private function getApiPhoto($attachment) {
return [ return [
"type" => "photo", "type" => "photo",
@ -576,4 +587,44 @@ final class Wall extends VKAPIRequestHandler
] ]
]; ];
} }
private function getApiPoll($attachment, $user) {
$answers = array();
foreach($attachment->getResults()->options as $answer) {
$answers[] = (object)[
"id" => $answer->id,
"rate" => $answer->pct,
"text" => $answer->name,
"votes" => $answer->votes
];
}
$userVote = array();
foreach($attachment->getUserVote($user) as $vote)
$userVote[] = $vote[0];
return [
"type" => "poll",
"poll" => [
"multiple" => $attachment->isMultipleChoice(),
"end_date" => $attachment->endsAt() == NULL ? 0 : $attachment->endsAt()->timestamp(),
"closed" => $attachment->hasEnded(),
"is_board" => false,
"can_edit" => false,
"can_vote" => $attachment->canVote($user),
"can_report" => false,
"can_share" => true,
"created" => 0,
"id" => $attachment->getId(),
"owner_id" => $attachment->getOwner()->getId(),
"question" => $attachment->getTitle(),
"votes" => $attachment->getVoterCount(),
"disable_unvote" => $attachment->isRevotable(),
"anonymous" => $attachment->isAnonymous(),
"answer_ids" => $userVote,
"answers" => $answers,
"author_id" => $attachment->getOwner()->getId(),
]
];
}
} }

View file

@ -27,14 +27,23 @@ class NewMessageEvent implements ILPEmitable
if($peer === $userId) if($peer === $userId)
$peer = $msg->getRecipient()->getId(); $peer = $msg->getRecipient()->getId();
/*
* Source:
* https://github.com/danyadev/longpoll-doc
*/
return [ return [
4, # event type 4, # event type
$msg->getId(), # messageId
256, # checked for spam flag 256, # checked for spam flag
$peer, # TODO calculate peer correctly $peer, # TODO calculate peer correctly
$msg->getSendTime()->timestamp(), # creation time in unix $msg->getSendTime()->timestamp(), # creation time in unix
$msg->getText(), # text (formatted) $msg->getText(), # text (formatted)
[], # empty additional info
[], # empty attachments [], # empty attachments
$msg->getId() << 2, # id as random_id $msg->getId() << 2, # id as random_id
$peer, # conversation id
0 # not edited yet
]; ];
} }
} }

View file

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\{User, Club};
use openvk\Web\Models\Repositories\{Users, Clubs};
class Alias extends RowModel
{
protected $tableName = "aliases";
function getOwnerId(): int
{
return $this->getRecord()->owner_id;
}
function getType(): string
{
if ($this->getOwnerId() < 0)
return "club";
return "user";
}
function getUser(): ?User
{
return (new Users)->get($this->getOwnerId());
}
function getClub(): ?Club
{
return (new Clubs)->get($this->getOwnerId() * -1);
}
}

View file

@ -0,0 +1,295 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Entities;
use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Util\DateTime;
use \UnexpectedValueException;
use Nette\InvalidStateException;
use openvk\Web\Models\Repositories\Users;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Exceptions\PollLockedException;
use openvk\Web\Models\Exceptions\AlreadyVotedException;
use openvk\Web\Models\Exceptions\InvalidOptionException;
class Poll extends Attachable
{
protected $tableName = "polls";
private $choicesToPersist = [];
function getTitle(): string
{
return $this->getRecord()->title;
}
function getMetaDescription(): string
{
$props = [];
$props[] = tr($this->isAnonymous() ? "poll_anon" : "poll_public");
if($this->isMultipleChoice()) $props[] = tr("poll_multi");
if(!$this->isRevotable()) $props[] = tr("poll_lock");
if(!is_null($this->endsAt())) $props[] = tr("poll_until", $this->endsAt());
return implode("", $props);
}
function getOwner(): User
{
return (new Users)->get($this->getRecord()->owner);
}
function getOptions(): array
{
$options = $this->getRecord()->related("poll_options.poll");
$res = [];
foreach($options as $opt)
$res[$opt->id] = $opt->name;
return $res;
}
function getUserVote(User $user): ?array
{
$ctx = DatabaseConnection::i()->getContext();
$votedOpts = $ctx->table("poll_votes")
->where(["user" => $user->getId(), "poll" => $this->getId()]);
if($votedOpts->count() == 0)
return NULL;
$res = [];
foreach($votedOpts as $votedOpt) {
$option = $ctx->table("poll_options")->get($votedOpt->option);
$res[] = [$option->id, $option->name];
}
return $res;
}
function getVoters(int $optionId, int $page = 1, ?int $perPage = NULL): array
{
$res = [];
$ctx = DatabaseConnection::i()->getContext();
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
$voters = $ctx->table("poll_votes")->where(["poll" => $this->getId(), "option" => $optionId]);
foreach($voters->page($page, $perPage) as $vote)
$res[] = (new Users)->get($vote->user);
return $res;
}
function getVoterCount(?int $optionId = NULL): int
{
$votes = DatabaseConnection::i()->getContext()->table("poll_votes");
if(!$optionId)
return $votes->select("COUNT(DISTINCT user) AS c")->where("poll", $this->getId())->fetch()->c;
return $votes->where(["poll" => $this->getId(), "option" => $optionId])->count();
}
function getResults(?User $user = NULL): object
{
$ctx = DatabaseConnection::i()->getContext();
$voted = NULL;
if(!is_null($user))
$voted = $this->getUserVote($user);
$result = (object) [];
$result->totalVotes = $this->getVoterCount();
$unsOptions = [];
foreach($this->getOptions() as $id => $title) {
$option = (object) [];
$option->id = $id;
$option->name = $title;
$option->votes = $this->getVoterCount($id);
$option->pct = $result->totalVotes == 0 ? 0 : min(100, floor(($option->votes / $result->totalVotes) * 100));
$option->voters = $this->getVoters($id, 1, 10);
if(!$user || !$voted)
$option->voted = NULL;
else
$option->voted = in_array([$id, $title], $voted);
$unsOptions[$id] = $option;
}
$optionsC = sizeof($unsOptions);
$sOptions = $unsOptions;
usort($sOptions, function($a, $b) { return $a->votes <=> $b->votes; });
for($i = 0; $i < $optionsC; $i++)
$unsOptions[$id]->rate = $optionsC - $i - 1;
$result->options = array_values($unsOptions);
return $result;
}
function isAnonymous(): bool
{
return (bool) $this->getRecord()->is_anonymous;
}
function isMultipleChoice(): bool
{
return (bool) $this->getRecord()->allows_multiple;
}
function isRevotable(): bool
{
return (bool) $this->getRecord()->can_revote;
}
function endsAt(): ?DateTime
{
if(!$this->getRecord()->until)
return NULL;
return new DateTime($this->getRecord()->until);
}
function hasEnded(): bool
{
if($this->getRecord()->ended)
return true;
if(!is_null($this->getRecord()->until))
return time() >= $this->getRecord()->until;
return false;
}
function hasVoted(User $user): bool
{
return !is_null($this->getUserVote($user));
}
function canVote(User $user): bool
{
return !$this->hasEnded() && !$this->hasVoted($user);
}
function vote(User $user, array $optionIds): void
{
if($this->hasEnded())
throw new PollLockedException;
if($this->hasVoted($user))
throw new AlreadyVotedException;
$optionIds = array_map(function($x) { return (int) $x; }, array_unique($optionIds));
$validOpts = array_keys($this->getOptions());
if(empty($optionIds) || (sizeof($optionIds) > 1 && !$this->isMultipleChoice()))
throw new UnexpectedValueException;
if(sizeof(array_diff($optionIds, $validOpts)) > 0)
throw new InvalidOptionException;
foreach($optionIds as $opt) {
DatabaseConnection::i()->getContext()->table("poll_votes")->insert([
"user" => $user->getId(),
"poll" => $this->getId(),
"option" => $opt,
]);
}
}
function revokeVote(User $user): void
{
if(!$this->isRevotable())
throw new PollLockedException;
$this->getRecord()->related("poll_votes.poll")
->where("user", $user->getId())->delete();
}
function setOwner(User $owner): void
{
$this->stateChanges("owner", $owner->getId());
}
function setEndDate(int $timestamp): void
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
$this->stateChanges("until", $timestamp);
}
function setEnded(): void
{
$this->stateChanges("ended", 1);
}
function setOptions(array $options): void
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
if(sizeof($options) > ovkGetQuirk("polls.max-opts"))
throw new TooMuchOptionsException;
$this->choicesToPersist = $options;
}
function setRevotability(bool $canReVote): void
{
if(!is_null($this->getRecord()))
throw new PollLockedException;
$this->stateChanges("can_revote", $canReVote);
}
function setAnonymity(bool $anonymous): void
{
$this->stateChanges("is_anonymous", $anonymous);
}
function setMultipleChoice(bool $mc): void
{
$this->stateChanges("allows_multiple", $mc);
}
function importXML(User $owner, string $xml): void
{
$xml = simplexml_load_string($xml);
$this->setOwner($owner);
$this->setTitle($xml["title"] ?? "Untitled");
$this->setMultipleChoice(($xml["multiple"] ?? "no") == "yes");
$this->setAnonymity(($xml["anonymous"] ?? "no") == "yes");
$this->setRevotability(($xml["locked"] ?? "no") == "no");
if(ctype_digit((string) ($xml["duration"] ?? "")))
$this->setEndDate(time() + ((86400 * (int) $xml["duration"])));
$options = [];
foreach($xml->options->option as $opt)
$options[] = (string) $opt;
if(empty($options))
throw new UnexpectedValueException;
$this->setOptions($options);
}
static function import(User $owner, string $xml): Poll
{
$poll = new Poll;
$poll->importXML($owner, $xml);
$poll->save();
return $poll;
}
function save(): void
{
if(empty($this->choicesToPersist))
throw new InvalidStateException;
parent::save();
foreach($this->choicesToPersist as $option) {
DatabaseConnection::i()->getContext()->table("poll_options")->insert([
"poll" => $this->getId(),
"name" => $option,
]);
}
}
}

View file

@ -1,7 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Entities; namespace openvk\Web\Models\Entities;
use Chandler\Database\DatabaseConnection as DB; use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Repositories\Clubs; use openvk\Web\Models\Repositories\{Clubs, Users};
use openvk\Web\Models\RowModel; use openvk\Web\Models\RowModel;
use openvk\Web\Models\Entities\Notifications\LikeNotification; use openvk\Web\Models\Entities\Notifications\LikeNotification;
@ -55,6 +55,15 @@ class Post extends Postable
{ {
return $this->getRecord()->wall; return $this->getRecord()->wall;
} }
function getWallOwner()
{
$w = $this->getRecord()->wall;
if($w < 0)
return (new Clubs)->get(abs($w));
return (new Users)->get($w);
}
function getRepostCount(): int function getRepostCount(): int
{ {

View file

@ -535,12 +535,15 @@ class User extends RowModel
return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1])); return sizeof(DatabaseConnection::i()->getContext()->table("messages")->where(["recipient_id" => $this->getId(), "unread" => 1]));
} }
function getClubs(int $page = 1, bool $admin = false): \Traversable function getClubs(int $page = 1, bool $admin = false, int $count = OPENVK_DEFAULT_PER_PAGE, bool $offset = false): \Traversable
{ {
if(!$offset)
$page = ($page - 1) * $count;
if($admin) { if($admin) {
$id = $this->getId(); $id = $this->getId();
$query = "SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?"; $query = "SELECT `id` FROM `groups` WHERE `owner` = ? UNION SELECT `club` as `id` FROM `group_coadmins` WHERE `user` = ?";
$query .= " LIMIT " . OPENVK_DEFAULT_PER_PAGE . " OFFSET " . ($page - 1) * OPENVK_DEFAULT_PER_PAGE; $query .= " LIMIT " . $count . " OFFSET " . $page;
$sel = DatabaseConnection::i()->getConnection()->query($query, $id, $id); $sel = DatabaseConnection::i()->getConnection()->query($query, $id, $id);
foreach($sel as $target) { foreach($sel as $target) {
@ -550,7 +553,7 @@ class User extends RowModel
yield $target; yield $target;
} }
} else { } else {
$sel = $this->getRecord()->related("subscriptions.follower")->page($page, OPENVK_DEFAULT_PER_PAGE); $sel = $this->getRecord()->related("subscriptions.follower")->limit($count, $page);
foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) { foreach($sel->where("model", "openvk\\Web\\Models\\Entities\\Club") as $target) {
$target = (new Clubs)->get($target->target); $target = (new Clubs)->get($target->target);
if(!$target) continue; if(!$target) continue;
@ -926,6 +929,10 @@ class User extends RowModel
$pClub = DatabaseConnection::i()->getContext()->table("groups")->where("shortcode", $code)->fetch(); $pClub = DatabaseConnection::i()->getContext()->table("groups")->where("shortcode", $code)->fetch();
if(!is_null($pClub)) if(!is_null($pClub))
return false; return false;
$pAlias = DatabaseConnection::i()->getContext()->table("aliases")->where("shortcode", $code)->fetch();
if(!is_null($pAlias))
return false;
} }
$this->stateChanges("shortcode", $code); $this->stateChanges("shortcode", $code);

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class AlreadyVotedException extends \RuntimeException
{
}

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class InvalidOptionException extends \UnexpectedValueException
{
}

View file

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
use Nette\InvalidStateException;
final class PollLockedException extends InvalidStateException
{
}

View file

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Exceptions;
final class TooMuchOptionsException extends \UnexpectedValueException
{
}

View file

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Alias;
use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection as DB;
use openvk\Web\Models\Entities\{Club, User};
use openvk\Web\Models\Repositories\{Clubs, Users};
class Aliases
{
private $context;
private $aliases;
function __construct()
{
$this->context = DB::i()->getContext();
$this->aliases = $this->context->table("aliases");
}
private function toAlias(?ActiveRow $ar): ?Alias
{
return is_null($ar) ? NULL : new Alias($ar);
}
function get(int $id): ?Alias
{
return $this->toAlias($this->aliases->get($id));
}
function getByShortcode(string $shortcode): ?Alias
{
return $this->toAlias($this->aliases->where("shortcode", $shortcode)->fetch());
}
}

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories; namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\Club; use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Repositories\Aliases;
use Nette\Database\Table\ActiveRow; use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
@ -22,7 +23,17 @@ class Clubs
function getByShortURL(string $url): ?Club function getByShortURL(string $url): ?Club
{ {
return $this->toClub($this->clubs->where("shortcode", $url)->fetch()); $shortcode = $this->toClub($this->clubs->where("shortcode", $url)->fetch());
if ($shortcode)
return $shortcode;
$alias = (new Aliases)->getByShortcode($url);
if (!$alias) return NULL;
if ($alias->getType() !== "club") return NULL;
return $alias->getClub();
} }
function get(int $id): ?Club function get(int $id): ?Club
@ -45,6 +56,9 @@ class Clubs
function getPopularClubs(): \Traversable function getPopularClubs(): \Traversable
{ {
// TODO rewrite
/*
$query = "SELECT ROW_NUMBER() OVER (ORDER BY `subscriptions` DESC) as `place`, `target` as `id`, COUNT(`follower`) as `subscriptions` FROM `subscriptions` WHERE `model` = \"openvk\\\Web\\\Models\\\Entities\\\Club\" GROUP BY `target` ORDER BY `subscriptions` DESC, `id` LIMIT 30;"; $query = "SELECT ROW_NUMBER() OVER (ORDER BY `subscriptions` DESC) as `place`, `target` as `id`, COUNT(`follower`) as `subscriptions` FROM `subscriptions` WHERE `model` = \"openvk\\\Web\\\Models\\\Entities\\\Club\" GROUP BY `target` ORDER BY `subscriptions` DESC, `id` LIMIT 30;";
$entries = DatabaseConnection::i()->getConnection()->query($query); $entries = DatabaseConnection::i()->getConnection()->query($query);
@ -54,6 +68,7 @@ class Clubs
"club" => $this->get($entry["id"]), "club" => $this->get($entry["id"]),
"subscriptions" => $entry["subscriptions"], "subscriptions" => $entry["subscriptions"],
]; ];
*/
} }
use \Nette\SmartObject; use \Nette\SmartObject;

View file

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories;
use Chandler\Database\DatabaseConnection;
use openvk\Web\Models\Entities\Poll;
class Polls
{
private $polls;
function __construct()
{
$this->polls = DatabaseConnection::i()->getContext()->table("polls");
}
function get(int $id): ?Poll
{
$poll = $this->polls->get($id);
if(!$poll)
return NULL;
return new Poll($poll);
}
}

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Models\Repositories; namespace openvk\Web\Models\Repositories;
use openvk\Web\Models\Entities\User; use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Aliases;
use Nette\Database\Table\ActiveRow; use Nette\Database\Table\ActiveRow;
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
use Chandler\Security\User as ChandlerUser; use Chandler\Security\User as ChandlerUser;
@ -9,11 +10,13 @@ class Users
{ {
private $context; private $context;
private $users; private $users;
private $aliases;
function __construct() function __construct()
{ {
$this->context = DatabaseConnection::i()->getContext(); $this->context = DatabaseConnection::i()->getContext();
$this->users = $this->context->table("profiles"); $this->users = $this->context->table("profiles");
$this->aliases = $this->context->table("aliases");
} }
private function toUser(?ActiveRow $ar): ?User private function toUser(?ActiveRow $ar): ?User
@ -28,7 +31,17 @@ class Users
function getByShortURL(string $url): ?User function getByShortURL(string $url): ?User
{ {
return $this->toUser($this->users->where("shortcode", $url)->fetch()); $shortcode = $this->toUser($this->users->where("shortcode", $url)->fetch());
if ($shortcode)
return $shortcode;
$alias = (new Aliases)->getByShortcode($url);
if (!$alias) return NULL;
if ($alias->getType() !== "user") return NULL;
return $alias->getUser();
} }
function getByChandlerUser(ChandlerUser $user): ?User function getByChandlerUser(ChandlerUser $user): ?User

View file

@ -64,7 +64,7 @@ final class AboutPresenter extends OpenVKPresenter
$this->template->usersStats = (new Users)->getStatistics(); $this->template->usersStats = (new Users)->getStatistics();
$this->template->clubsCount = (new Clubs)->getCount(); $this->template->clubsCount = (new Clubs)->getCount();
$this->template->postsCount = (new Posts)->getCount(); $this->template->postsCount = (new Posts)->getCount();
$this->template->popularClubs = iterator_to_array((new Clubs)->getPopularClubs()); $this->template->popularClubs = [];
$this->template->admins = iterator_to_array((new Users)->getInstanceAdmins()); $this->template->admins = iterator_to_array((new Users)->getInstanceAdmins());
} }

View file

@ -6,7 +6,7 @@ use openvk\Web\Models\Repositories\Applications;
final class AppsPresenter extends OpenVKPresenter final class AppsPresenter extends OpenVKPresenter
{ {
private $apps; private $apps;
protected $presenterName = "apps";
function __construct(Applications $apps) function __construct(Applications $apps)
{ {
$this->apps = $apps; $this->apps = $apps;

View file

@ -6,6 +6,7 @@ use openvk\Web\Models\Repositories\{Comments, Clubs};
final class CommentPresenter extends OpenVKPresenter final class CommentPresenter extends OpenVKPresenter
{ {
protected $presenterName = "comment";
private $models = [ private $models = [
"posts" => "openvk\\Web\\Models\\Repositories\\Posts", "posts" => "openvk\\Web\\Models\\Repositories\\Posts",
"photos" => "openvk\\Web\\Models\\Repositories\\Photos", "photos" => "openvk\\Web\\Models\\Repositories\\Photos",

View file

@ -7,6 +7,7 @@ final class GiftsPresenter extends OpenVKPresenter
{ {
private $gifts; private $gifts;
private $users; private $users;
protected $presenterName = "gifts";
function __construct(Gifts $gifts, Users $users) function __construct(Gifts $gifts, Users $users)
{ {

View file

@ -8,7 +8,8 @@ use Chandler\Security\Authenticator;
final class GroupPresenter extends OpenVKPresenter final class GroupPresenter extends OpenVKPresenter
{ {
private $clubs; private $clubs;
protected $presenterName = "group";
function __construct(Clubs $clubs) function __construct(Clubs $clubs)
{ {
$this->clubs = $clubs; $this->clubs = $clubs;

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace openvk\Web\Presenters;
final class MaintenancePresenter extends OpenVKPresenter
{
protected $presenterName = "maintenance";
function renderSection(string $name): void
{
if(!OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"][$name])
$this->flashFail("err", tr("error"), tr("forbidden"));
$this->template->name = [
"photos" => tr("my_photos"),
"videos" => tr("my_videos"),
"messenger" => tr("my_messages"),
"user" => tr("users"),
"group" => tr("my_groups"),
"comment" => tr("comments"),
"gifts" => tr("gifts"),
"apps" => tr("apps"),
"notes" => tr("my_notes"),
"notification" => tr("my_feedback"),
"support" => tr("menu_support"),
"topics" => tr("topics")
][$name] ?? $name;
}
function renderAll(): void
{
}
}

View file

@ -9,11 +9,13 @@ final class MessengerPresenter extends OpenVKPresenter
{ {
private $messages; private $messages;
private $signaler; private $signaler;
protected $presenterName = "messenger";
function __construct(Messages $messages) function __construct(Messages $messages)
{ {
$this->messages = $messages; $this->messages = $messages;
$this->signaler = SignalManager::i(); $this->signaler = SignalManager::i();
parent::__construct(); parent::__construct();
} }
@ -30,7 +32,7 @@ final class MessengerPresenter extends OpenVKPresenter
function renderIndex(): void function renderIndex(): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();
if(isset($_GET["sel"])) if(isset($_GET["sel"]))
$this->pass("openvk!Messenger->app", $_GET["sel"]); $this->pass("openvk!Messenger->app", $_GET["sel"]);
@ -93,6 +95,13 @@ final class MessengerPresenter extends OpenVKPresenter
} }
$legacy = $this->queryParam("version") < 3; $legacy = $this->queryParam("version") < 3;
$time = intval($this->queryParam("wait"));
if($time > 60)
$time = 60;
elseif($time == 0)
$time = 25; // default
$this->signaler->listen(function($event, $eId) use ($id) { $this->signaler->listen(function($event, $eId) use ($id) {
exit(json_encode([ exit(json_encode([
@ -101,7 +110,7 @@ final class MessengerPresenter extends OpenVKPresenter
$event->getVKAPISummary($id), $event->getVKAPISummary($id),
], ],
])); ]));
}, $id); }, $id, $time);
} }
function renderApiGetMessages(int $sel, int $lastMsg): void function renderApiGetMessages(int $sel, int $lastMsg): void

View file

@ -6,7 +6,8 @@ use openvk\Web\Models\Entities\Note;
final class NotesPresenter extends OpenVKPresenter final class NotesPresenter extends OpenVKPresenter
{ {
private $notes; private $notes;
protected $presenterName = "notes";
function __construct(Notes $notes) function __construct(Notes $notes)
{ {
$this->notes = $notes; $this->notes = $notes;

View file

@ -3,6 +3,8 @@ namespace openvk\Web\Presenters;
final class NotificationPresenter extends OpenVKPresenter final class NotificationPresenter extends OpenVKPresenter
{ {
protected $presenterName = "notification";
function renderFeed(): void function renderFeed(): void
{ {
$this->assertUserLoggedIn(); $this->assertUserLoggedIn();

View file

@ -17,7 +17,8 @@ abstract class OpenVKPresenter extends SimplePresenter
protected $deactivationTolerant = false; protected $deactivationTolerant = false;
protected $errorTemplate = "@error"; protected $errorTemplate = "@error";
protected $user = NULL; protected $user = NULL;
protected $presenterName;
private function calculateQueryString(array $data): string private function calculateQueryString(array $data): string
{ {
$rawUrl = "tcp+stratum://fakeurl.net$_SERVER[REQUEST_URI]"; #HTTP_HOST can be tainted $rawUrl = "tcp+stratum://fakeurl.net$_SERVER[REQUEST_URI]"; #HTTP_HOST can be tainted
@ -196,12 +197,13 @@ abstract class OpenVKPresenter extends SimplePresenter
function onStartup(): void function onStartup(): void
{ {
$user = Authenticator::i()->getUser(); $user = Authenticator::i()->getUser();
$this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false; $this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false;
$this->template->isTimezoned = Session::i()->get("_timezoneOffset"); $this->template->isTimezoned = Session::i()->get("_timezoneOffset");
$userValidated = 0; $userValidated = 0;
$cacheTime = OPENVK_ROOT_CONF["openvk"]["preferences"]["nginxCacheTime"] ?? 0; $cacheTime = OPENVK_ROOT_CONF["openvk"]["preferences"]["nginxCacheTime"] ?? 0;
if(!is_null($user)) { if(!is_null($user)) {
$this->user = (object) []; $this->user = (object) [];
$this->user->raw = $user; $this->user->raw = $user;
@ -226,7 +228,7 @@ abstract class OpenVKPresenter extends SimplePresenter
} }
exit; exit;
} }
if($this->user->identity->isBanned() && !$this->banTolerant) { if($this->user->identity->isBanned() && !$this->banTolerant) {
header("HTTP/1.1 403 Forbidden"); header("HTTP/1.1 403 Forbidden");
$this->getTemplatingEngine()->render(__DIR__ . "/templates/@banned.xml", [ $this->getTemplatingEngine()->render(__DIR__ . "/templates/@banned.xml", [
@ -247,23 +249,33 @@ abstract class OpenVKPresenter extends SimplePresenter
]); ]);
exit; exit;
} }
$userValidated = 1; $userValidated = 1;
$cacheTime = 0; # Force no cache $cacheTime = 0; # Force no cache
if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) { if($this->user->identity->onlineStatus() == 0 && !($this->user->identity->isDeleted() || $this->user->identity->isBanned())) {
$this->user->identity->setOnline(time()); $this->user->identity->setOnline(time());
$this->user->identity->save(); $this->user->identity->save();
} }
$this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1); $this->template->ticketAnsweredCount = (new Tickets)->getTicketsCountByUserId($this->user->id, 1);
if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0)) if($user->can("write")->model("openvk\Web\Models\Entities\TicketReply")->whichBelongsTo(0))
$this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0); $this->template->helpdeskTicketNotAnsweredCount = (new Tickets)->getTicketCount(0);
} }
header("X-OpenVK-User-Validated: $userValidated"); header("X-OpenVK-User-Validated: $userValidated");
header("X-Accel-Expires: $cacheTime"); header("X-Accel-Expires: $cacheTime");
setlocale(LC_TIME, ...(explode(";", tr("__locale")))); setlocale(LC_TIME, ...(explode(";", tr("__locale"))));
if (!OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"]["all"]) {
if (OPENVK_ROOT_CONF["openvk"]["preferences"]["maintenanceMode"][$this->presenterName]) {
$this->pass("openvk!Maintenance->section", $this->presenterName);
}
} else {
if ($this->presenterName != "maintenance") {
$this->redirect("/maintenances/");
}
}
parent::onStartup(); parent::onStartup();
} }
@ -272,10 +284,14 @@ abstract class OpenVKPresenter extends SimplePresenter
parent::onBeforeRender(); parent::onBeforeRender();
$whichbrowser = new WhichBrowser\Parser(getallheaders()); $whichbrowser = new WhichBrowser\Parser(getallheaders());
$featurephonetheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultFeaturePhoneTheme"];
$mobiletheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultMobileTheme"]; $mobiletheme = OPENVK_ROOT_CONF["openvk"]["preferences"]["defaultMobileTheme"];
if($mobiletheme && $whichbrowser->isType('mobile') && Session::i()->get("_tempTheme") == NULL)
if($featurephonetheme && $this->isOldThing($whichbrowser) && Session::i()->get("_tempTheme") == NULL) {
$this->setSessionTheme($featurephonetheme);
} elseif($mobiletheme && $whichbrowser->isType('mobile') && Session::i()->get("_tempTheme") == NULL)
$this->setSessionTheme($mobiletheme); $this->setSessionTheme($mobiletheme);
$theme = NULL; $theme = NULL;
if(Session::i()->get("_tempTheme")) { if(Session::i()->get("_tempTheme")) {
$theme = Themepacks::i()[Session::i()->get("_tempTheme", "ovk")]; $theme = Themepacks::i()[Session::i()->get("_tempTheme", "ovk")];
@ -306,4 +322,33 @@ abstract class OpenVKPresenter extends SimplePresenter
header("Content-Length: $size"); header("Content-Length: $size");
exit($payload); exit($payload);
} }
protected function isOldThing($whichbrowser) {
if($whichbrowser->isOs('Series60') ||
$whichbrowser->isOs('Series40') ||
$whichbrowser->isOs('Series80') ||
$whichbrowser->isOs('Windows CE') ||
$whichbrowser->isOs('Windows Mobile') ||
$whichbrowser->isOs('Nokia Asha Platform') ||
$whichbrowser->isOs('UIQ') ||
$whichbrowser->isEngine('NetFront') || // PSP and other japanese portable systems
$whichbrowser->isOs('Android') ||
$whichbrowser->isOs('iOS') ||
$whichbrowser->isBrowser('Internet Explorer', '<=', '8')) {
// yeah, it's old, but ios and android are?
if($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '<=', '9'))
return true;
elseif($whichbrowser->isOs('iOS') && $whichbrowser->isOs('iOS', '>', '9'))
return false;
if($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '<=', '5'))
return true;
elseif($whichbrowser->isOs('Android') && $whichbrowser->isOs('Android', '>', '5'))
return false;
return true;
} else {
return false;
}
}
} }

View file

@ -9,7 +9,8 @@ final class PhotosPresenter extends OpenVKPresenter
private $users; private $users;
private $photos; private $photos;
private $albums; private $albums;
protected $presenterName = "photos";
function __construct(Photos $photos, Albums $albums, Users $users) function __construct(Photos $photos, Albums $albums, Users $users)
{ {
$this->users = $users; $this->users = $users;

View file

@ -0,0 +1,75 @@
<?php declare(strict_types=1);
namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\Poll;
use openvk\Web\Models\Repositories\Polls;
final class PollPresenter extends OpenVKPresenter
{
private $polls;
function __construct(Polls $polls)
{
$this->polls = $polls;
parent::__construct();
}
function renderView(int $id): void
{
$poll = $this->polls->get($id);
if(!$poll)
$this->notFound();
$this->template->id = $poll->getId();
$this->template->title = $poll->getTitle();
$this->template->isAnon = $poll->isAnonymous();
$this->template->multiple = $poll->isMultipleChoice();
$this->template->unlocked = $poll->isRevotable();
$this->template->until = $poll->endsAt();
$this->template->votes = $poll->getVoterCount();
$this->template->meta = $poll->getMetaDescription();
$this->template->ended = $ended = $poll->hasEnded();
if((is_null($this->user) || $poll->canVote($this->user->identity)) && !$ended) {
$this->template->options = $poll->getOptions();
$this->template->_template = "Poll/Poll.xml";
return;
}
if(is_null($this->user)) {
$this->template->voted = false;
$this->template->results = $poll->getResults();
} else {
$this->template->voted = $poll->hasVoted($this->user->identity);
$this->template->results = $poll->getResults($this->user->identity);
}
$this->template->_template = "Poll/PollResults.xml";
}
function renderVoters(int $pollId): void
{
$poll = $this->polls->get($pollId);
if(!$poll)
$this->notFound();
if($poll->isAnonymous())
$this->flashFail("err", tr("forbidden"), tr("poll_err_anonymous"));
$options = $poll->getOptions();
$option = (int) base_convert($this->queryParam("option"), 32, 10);
if(!in_array($option, array_keys($options)))
$this->notFound();
$page = (int) ($this->queryParam("p") ?? 1);
$voters = $poll->getVoters($option, $page);
$this->template->pollId = $pollId;
$this->template->options = $options;
$this->template->option = [$option, $options[$option]];
$this->template->tabs = $options;
$this->template->iterator = $voters;
$this->template->count = $poll->getVoterCount($option);
$this->template->page = $page;
}
}

View file

@ -11,6 +11,7 @@ final class SupportPresenter extends OpenVKPresenter
{ {
protected $banTolerant = true; protected $banTolerant = true;
protected $deactivationTolerant = true; protected $deactivationTolerant = true;
protected $presenterName = "support";
private $tickets; private $tickets;
private $comments; private $comments;
@ -155,11 +156,12 @@ final class SupportPresenter extends OpenVKPresenter
$this->notFound(); $this->notFound();
} else { } else {
if($ticket->getUserId() !== $this->user->id && $this->hasPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0)) if($ticket->getUserId() !== $this->user->id && $this->hasPermission('openvk\Web\Models\Entities\TicketReply', 'write', 0))
$this->redirect("/support/tickets"); $_redirect = "/support/tickets";
else else
$this->redirect("/support"); $_redirect = "/support?act=list";
$ticket->delete(); $ticket->delete();
$this->redirect($_redirect);
} }
} }
} }

View file

@ -7,7 +7,8 @@ final class TopicsPresenter extends OpenVKPresenter
{ {
private $topics; private $topics;
private $clubs; private $clubs;
protected $presenterName = "topics";
function __construct(Topics $topics, Clubs $clubs) function __construct(Topics $topics, Clubs $clubs)
{ {
$this->topics = $topics; $this->topics = $topics;

View file

@ -15,12 +15,13 @@ use Nette\Database\UniqueConstraintViolationException;
final class UserPresenter extends OpenVKPresenter final class UserPresenter extends OpenVKPresenter
{ {
private $users; private $users;
public $deactivationTolerant = false; public $deactivationTolerant = false;
protected $presenterName = "user";
function __construct(Users $users) function __construct(Users $users)
{ {
$this->users = $users; $this->users = $users;
parent::__construct(); parent::__construct();
} }

View file

@ -8,7 +8,8 @@ final class VideosPresenter extends OpenVKPresenter
{ {
private $videos; private $videos;
private $users; private $users;
protected $presenterName = "videos";
function __construct(Videos $videos, Users $users) function __construct(Videos $videos, Users $users)
{ {
$this->videos = $videos; $this->videos = $videos;

View file

@ -1,6 +1,7 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace openvk\Web\Presenters; namespace openvk\Web\Presenters;
use openvk\Web\Models\Entities\{Post, Photo, Video, Club, User}; use openvk\Web\Models\Exceptions\TooMuchOptionsException;
use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification}; use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification};
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums}; use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums};
use Chandler\Database\DatabaseConnection; use Chandler\Database\DatabaseConnection;
@ -44,9 +45,6 @@ final class WallPresenter extends OpenVKPresenter
function renderWall(int $user, bool $embedded = false): void function renderWall(int $user, bool $embedded = false): void
{ {
if(false)
exit(tr("forbidden") . ": " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) { if(is_null($this->user)) {
$canPost = false; $canPost = false;
@ -65,7 +63,10 @@ final class WallPresenter extends OpenVKPresenter
} }
if ($embedded == true) $this->template->_template = "components/wall.xml"; if ($embedded == true) $this->template->_template = "components/wall.xml";
$this->template->oObj = $owner; $this->template->oObj = $owner;
if($user < 0)
$this->template->club = $owner;
$this->template->owner = $user; $this->template->owner = $user;
$this->template->canPost = $canPost; $this->template->canPost = $canPost;
$this->template->count = $this->posts->getPostCountOnUserWall($user); $this->template->count = $this->posts->getPostCountOnUserWall($user);
@ -88,9 +89,6 @@ final class WallPresenter extends OpenVKPresenter
function renderRSS(int $user): void function renderRSS(int $user): void
{ {
if(false)
exit(tr("forbidden") . ": " . (string) random_int(0, 255));
$owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user)); $owner = ($user < 0 ? (new Clubs) : (new Users))->get(abs($user));
if(is_null($this->user)) { if(is_null($this->user)) {
$canPost = false; $canPost = false;
@ -259,16 +257,26 @@ final class WallPresenter extends OpenVKPresenter
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon); $photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon);
} }
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) { if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK)
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon); $video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon);
}
} catch(\DomainException $ex) { } catch(\DomainException $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted")); $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted"));
} catch(ISE $ex) { } catch(ISE $ex) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large")); $this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large"));
} }
if(empty($this->postParam("text")) && !$photo && !$video) try {
$poll = NULL;
$xml = $this->postParam("poll");
if (!is_null($xml) && $xml != "none")
$poll = Poll::import($this->user->identity, $xml);
} catch(TooMuchOptionsException $e) {
$this->flashFail("err", tr("failed_to_publish_post"), tr("poll_err_to_much_options"));
} catch(\UnexpectedValueException $e) {
$this->flashFail("err", tr("failed_to_publish_post"), "Poll format invalid");
}
if(empty($this->postParam("text")) && !$photo && !$video && !$poll)
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
try { try {
@ -291,6 +299,9 @@ final class WallPresenter extends OpenVKPresenter
if(!is_null($video)) if(!is_null($video))
$post->attach($video); $post->attach($video);
if(!is_null($poll))
$post->attach($poll);
if($wall > 0 && $wall !== $this->user->identity->getId()) if($wall > 0 && $wall !== $this->user->identity->getId())
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();

View file

@ -17,62 +17,19 @@
{script "js/l10n.js"} {script "js/l10n.js"}
{script "js/openvk.cls.js"} {script "js/openvk.cls.js"}
{css "js/node_modules/tippy.js/dist/backdrop.css"}
{css "js/node_modules/tippy.js/dist/border.css"}
{css "js/node_modules/tippy.js/dist/svg-arrow.css"}
{css "js/node_modules/tippy.js/themes/light.css"}
{script "js/node_modules/@popperjs/core/dist/umd/popper.min.js"}
{script "js/node_modules/tippy.js/dist/tippy-bundle.umd.min.js"}
{script "js/node_modules/handlebars/dist/handlebars.min.js"}
{if $isTimezoned == NULL} {if $isTimezoned == NULL}
{script "js/timezone.js"} {script "js/timezone.js"}
{/if} {/if}
{ifset $thisUser} {include "_includeCSS.xml"}
{if $thisUser->getNsfwTolerance() < 2}
{css "css/nsfw-posts.css"}
{/if}
{if $theme !== NULL}
{if $theme->inheritDefault()}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/styles.css" />
{if $isXmas}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/resource/xmas.css" />
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
{if $thisUser->getStyleAvatar() == 1}
{css "css/avatar.1.css"}
{/if}
{if $thisUser->getStyleAvatar() == 2}
{css "css/avatar.2.css"}
{/if}
{if $thisUser->hasMicroblogEnabled() == 1}
{css "css/microblog.css"}
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/nsfw-posts.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/ifset}
{ifset headIncludes} {ifset headIncludes}
{include headIncludes} {include headIncludes}
@ -325,6 +282,8 @@
{script "js/scroll.js"} {script "js/scroll.js"}
{script "js/al_wall.js"} {script "js/al_wall.js"}
{script "js/al_api.js"} {script "js/al_api.js"}
{script "js/al_mentions.js"}
{script "js/al_polls.js"}
{ifset $thisUser} {ifset $thisUser}
{script "js/al_notifs.js"} {script "js/al_notifs.js"}

View file

@ -0,0 +1,20 @@
{extends "../@layout.xml"}
{block title}
{_global_maintenance}
{/block}
{block header}
{_global_maintenance}
{/block}
{block content}
<div class="container_gray">
<center style="background: white;border: #DEDEDE solid 1px;">
<img src="/assets/packages/static/openvk/img/oof.apng" style="width: 20%;" />
<span style="color: #707070;margin: 10px 0;display: block;">
{_undergoing_global_maintenance}
</span>
</center>
</div>
{/block}

View file

@ -0,0 +1,20 @@
{extends "../@layout.xml"}
{block title}
{_section_maintenance}
{/block}
{block header}
{_section_maintenance}
{/block}
{block content}
<div class="container_gray">
<center style="background: white;border: #DEDEDE solid 1px;">
<img src="/assets/packages/static/openvk/img/oof.apng" style="width: 20%;" />
<span style="color: #707070;margin: 10px 0;display: block;">
{tr("undergoing_section_maintenance", $name)|noescape}
</span>
</center>
</div>
{/block}

View file

@ -0,0 +1,44 @@
{if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'}
<link rel="shortcut icon" href="/assets/packages/static/openvk/img/icon.ico" />
<meta n:ifset="$csrfToken" name="csrf" value="{$csrfToken}" />
<script src="/language/{getLanguage()}.js" crossorigin="anonymous"></script>
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
{script "js/messagebox.js"}
{script "js/l10n.js"}
{script "js/al_api.js"}
{script "js/al_polls.js"}
{include "../_includeCSS.xml"}
<style>body { margin: 8px; }</style>
{/if}
<div class="poll">
<h4>{$title}</h4>
<form onsubmit="pollFormSubmit(event, this)" data-multi="{$multiple ? '1' : '0'}" data-pid="{$id}">
<div class="poll-options">
<div n:foreach="$options as $oid => $option" class="poll-option">
<label>
{if $multiple}
<input n:attr="disabled => is_null($thisUser)" type="checkbox" name="option{$oid}" onclick="pollCheckBoxPressed(this)" />
{else}
<input n:attr="disabled => is_null($thisUser)" type="radio" value="{$oid}" name="vote" onclick="pollRadioPressed(this)" />
{/if}
{$option}
</label>
</div>
</div>
{if $multiple}
<br/>
<input type="submit" class="button" value="{_cast_vote}" disabled="disabled" />
{/if}
</form>
<div class="poll-meta">
{tr("poll_voter_count", $votes)|noescape}<br/>
<span class="nobold">{$meta}</span>
</div>
</div>

View file

@ -0,0 +1,56 @@
{if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'}
<link rel="shortcut icon" href="/assets/packages/static/openvk/img/icon.ico" />
<meta n:ifset="$csrfToken" name="csrf" value="{$csrfToken}" />
<script src="/language/{getLanguage()}.js" crossorigin="anonymous"></script>
{script "js/node_modules/jquery/dist/jquery.min.js"}
{script "js/node_modules/umbrellajs/umbrella.min.js"}
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
{script "js/messagebox.js"}
{script "js/l10n.js"}
{script "js/al_api.js"}
{script "js/al_polls.js"}
{include "../_includeCSS.xml"}
<style>body { margin: 8px; } .poll { border: 1px solid #e3e3e3; }</style>
{/if}
<div class="poll" data-id="{$id}">
<a n:if="$unlocked && $voted" href="javascript:pollRetractVote({$id})" class="poll-retract-vote">{_retract_vote}</a>
<h4>{$title}</h4>
<div class="poll-results">
<div n:foreach="$results->options as $option" class="poll-result">
{if $isAnon}
<a href="javascript:false">
{if $option->voted}
<b>{$option->name}</b>
{else}
{$option->name}
{/if}
</a>
{else}
<a href="/poll{$id}/voters?option={base_convert($option->id, 10, 32)}">
{if $option->voted}
<b>{$option->name}</b>
{else}
{$option->name}
{/if}
</a>
{/if}
<div class="poll-result-barspace">
<div class="poll-result-bar">
<span class="poll-result-count">{$option->votes}</span>
<div class="poll-result-bar-sub" style="width: {$option->pct}%">&nbsp;</div>
</div>
<div class="poll-result-pct">
<strong>{$option->pct}%</strong>
</div>
</div>
</div>
</div>
<div class="poll-meta">
{tr("poll_voter_count", $votes)|noescape}<br/>
<span class="nobold">{$meta}</span>
</div>
</div>

View file

@ -0,0 +1,40 @@
{extends "../@listView.xml"}
{block title}
{_poll_voters_list}
{/block}
{block header}
{_poll_voters_list} »
{$option[1]}
{/block}
{block tabs}
<div n:foreach="$options as $optionId => $optionName" class="tab" id="{$optionId == $option[0] ? 'activetabs' : 'ki'}">
<a id="{$optionId == $option[0] ? 'act_tab_a' : ''}" href="/poll{$pollId}/voters?option={base_convert($optionId, 10, 32)}">{$optionName}</a>
</div>
{/block}
{* BEGIN ELEMENTS DESCRIPTION *}
{block link|strip|stripHtml}
{$x->getURL()}
{/block}
{block preview}
<img src="{$x->getAvatarUrl('miniscule')}" width="75" alt="Фотография пользователя" />
{/block}
{block name}
{$x->getCanonicalName()}
<img n:if="$x->isVerified()"
class="name-checkmark"
src="/assets/packages/static/openvk/img/checkmark.png"
/>
{/block}
{block description}
{/block}
{block actions}
{/block}

View file

@ -53,7 +53,7 @@
<div n:attr="id => ($act === 'online' ? 'activetabs' : 'ki')" class="tab"> <div n:attr="id => ($act === 'online' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'online' ? 'act_tab_a' : 'ki')" href="?act=online">{_online}</a> <a n:attr="id => ($act === 'online' ? 'act_tab_a' : 'ki')" href="?act=online">{_online}</a>
</div> </div>
<div n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'activetabs' : 'ki')" class="tab"> <div n:if="!is_null($thisUser) && $user->getId() === $thisUser->getId()" n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'act_tab_a' : 'ki')" href="?act=incoming">{_req}</a> <a n:attr="id => ($act === 'incoming' || $act === 'outcoming' ? 'act_tab_a' : 'ki')" href="?act=incoming">{_req}</a>
</div> </div>
{/block} {/block}

View file

@ -79,9 +79,9 @@
{/block} {/block}
{block actions} {block actions}
<a href="{$x->getURL()}" class="profile_link">{_check_community}</a>
{if $x->canBeModifiedBy($thisUser ?? NULL)} {if $x->canBeModifiedBy($thisUser ?? NULL)}
{var $clubPinned = $thisUser->isClubPinned($x)} {var $clubPinned = $thisUser->isClubPinned($x)}
<a href="{$x->getURL()}" class="profile_link">{_check_community}</a>
<a href="/groups_pin?club={$x->getId()}&hash={rawurlencode($csrfToken)}" class="profile_link" n:if="$clubPinned || $thisUser->getPinnedClubCount() <= 10" id="_pinGroup" data-group-name="{$x->getName()}" data-group-url="{$x->getUrl()}"> <a href="/groups_pin?club={$x->getId()}&hash={rawurlencode($csrfToken)}" class="profile_link" n:if="$clubPinned || $thisUser->getPinnedClubCount() <= 10" id="_pinGroup" data-group-name="{$x->getName()}" data-group-url="{$x->getUrl()}">
{if $clubPinned} {if $clubPinned}
{_remove_from_left_menu} {_remove_from_left_menu}
@ -89,21 +89,21 @@
{_add_to_left_menu} {_add_to_left_menu}
{/if} {/if}
</a> </a>
{if $x->getSubscriptionStatus($thisUser) == false} {/if}
<form class="profile_link_form" action="/setSub/club" method="post"> {if $x->getSubscriptionStatus($thisUser) == false}
<input type="hidden" name="act" value="add" /> <form class="profile_link_form" action="/setSub/club" method="post">
<input type="hidden" name="id" value="{$x->getId()}" /> <input type="hidden" name="act" value="add" />
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="id" value="{$x->getId()}" />
<input type="submit" class="profile_link" value="{_join_community}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
</form> <input type="submit" class="profile_link" value="{_join_community}" />
{else} </form>
<form class="profile_link_form" action="/setSub/club" method="post"> {else}
<input type="hidden" name="act" value="rem" /> <form class="profile_link_form" action="/setSub/club" method="post">
<input type="hidden" name="id" value="{$x->getId()}" /> <input type="hidden" name="act" value="rem" />
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="id" value="{$x->getId()}" />
<input type="submit" class="profile_link" value="{_leave_community}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
</form> <input type="submit" class="profile_link" value="{_leave_community}" />
{/if} </form>
{/if} {/if}
{/block} {/block}

View file

@ -16,7 +16,7 @@
</div> </div>
<div n:class="postFeedWrapper, $thisUser->hasMicroblogEnabled() ? postFeedWrapperMicroblog"> <div n:class="postFeedWrapper, $thisUser->hasMicroblogEnabled() ? postFeedWrapperMicroblog">
{include "../components/textArea.xml", route => "/wall" . $thisUser->getId() . "/makePost"} {include "../components/textArea.xml", route => "/wall" . $thisUser->getId() . "/makePost", graffiti => true, polls => true}
</div> </div>
{foreach $posts as $post} {foreach $posts as $post}

View file

@ -0,0 +1,52 @@
{ifset $thisUser}
{if $thisUser->getNsfwTolerance() < 2}
{css "css/nsfw-posts.css"}
{/if}
{if $theme !== NULL}
{if $theme->inheritDefault()}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/styles.css" />
{if $isXmas}
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/resource/xmas.css" />
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/if}
{if $thisUser->getStyleAvatar() == 1}
{css "css/avatar.1.css"}
{/if}
{if $thisUser->getStyleAvatar() == 2}
{css "css/avatar.2.css"}
{/if}
{if $thisUser->hasMicroblogEnabled() == 1}
{css "css/microblog.css"}
{/if}
{else}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/nsfw-posts.css"}
{css "css/notifications.css"}
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/ifset}

View file

@ -11,6 +11,8 @@
{/if} {/if}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Video} {elseif $attachment instanceof \openvk\Web\Models\Entities\Video}
<video class="media" src="{$attachment->getURL()}" controls="controls"></video> <video class="media" src="{$attachment->getURL()}" controls="controls"></video>
{elseif $attachment instanceof \openvk\Web\Models\Entities\Poll}
{presenter "openvk!Poll->view", $attachment->getId()}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Post} {elseif $attachment instanceof \openvk\Web\Models\Entities\Post}
{php $GLOBALS["_nesAttGloCou"] = (isset($GLOBALS["_nesAttGloCou"]) ? $GLOBALS["_nesAttGloCou"] : 0) + 1} {php $GLOBALS["_nesAttGloCou"] = (isset($GLOBALS["_nesAttGloCou"]) ? $GLOBALS["_nesAttGloCou"] : 0) + 1}
{if $GLOBALS["_nesAttGloCou"] > 2} {if $GLOBALS["_nesAttGloCou"] > 2}

View file

@ -24,17 +24,15 @@
<img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png"> <img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png">
{$post->isDeactivationMessage() ? ($author->isFemale() ? tr($deac . "_f") : tr($deac . "_m"))} {$post->isDeactivationMessage() ? ($author->isFemale() ? tr($deac . "_f") : tr($deac . "_m"))}
{if ($onWallOf ?? false) &&!$post->isPostedOnBehalfOfGroup() && $post->getOwnerPost() !== $post->getTargetWall()} {if ($onWallOf ?? false) &&!$post->isPostedOnBehalfOfGroup() && $post->getOwnerPost() !== $post->getTargetWall()}
{var $wallId = $post->getTargetWall()} {var $wallOwner = $post->getWallOwner()}
{var $wallURL = $wallId > -1 ? "/id$wallId" : "/club" . abs($wallId)} <a href="{$wallOwner->getURL()}" class="mention" data-mention-ref="{$post->getTargetWall()}">
на
<a href="{$wallURL}">
<b> <b>
{if isset($thisUser) && $thisUser->getId() === $wallId} {if isset($thisUser) && $thisUser->getId() === $post->getTargetWall()}
вашей {_post_on_your_wall}
{/if} {elseif $wallOwner instanceof \openvk\Web\Models\Entities\Club}
стене {tr("post_on_group_wall", ovk_proc_strtr($wallOwner->getName(), 52))}
{if $wallId < 0} {else}
группы {tr("post_on_user_wall", $wallOwner->getMorphedName("genitive", false))}
{/if} {/if}
</b> </b>
</a> </a>
@ -77,7 +75,7 @@
{var $actualAuthor = $post->getOwner(false)} {var $actualAuthor = $post->getOwner(false)}
<span> <span>
{_author}: {_author}:
<a href="{$actualAuthor->getURL()}"> <a href="{$actualAuthor->getURL()}" class="mention" data-mention-ref="{$actualAuthor->getId()}">
{$actualAuthor->getCanonicalName()} {$actualAuthor->getCanonicalName()}
</a> </a>
</span> </span>

View file

@ -20,17 +20,15 @@
<img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png"> <img n:if="$author->isVerified()" class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png">
{$post->isDeactivationMessage() ? ($author->isFemale() ? tr($deac . "_f") : tr($deac . "_m")) : ($post->isPostedOnBehalfOfGroup() ? tr("post_writes_g") : ($author->isFemale() ? tr("post_writes_f") : tr("post_writes_m")))} {$post->isDeactivationMessage() ? ($author->isFemale() ? tr($deac . "_f") : tr($deac . "_m")) : ($post->isPostedOnBehalfOfGroup() ? tr("post_writes_g") : ($author->isFemale() ? tr("post_writes_f") : tr("post_writes_m")))}
{if ($onWallOf ?? false) &&!$post->isPostedOnBehalfOfGroup() && $post->getOwnerPost() !== $post->getTargetWall()} {if ($onWallOf ?? false) &&!$post->isPostedOnBehalfOfGroup() && $post->getOwnerPost() !== $post->getTargetWall()}
{var $wallId = $post->getTargetWall()} {var $wallOwner = $post->getWallOwner()}
{var $wallURL = $wallId > -1 ? "/id$wallId" : "/club" . abs($wallId)} <a href="{$wallOwner->getURL()}" class="mention" data-mention-ref="{$post->getTargetWall()}">
на
<a href="{$wallURL}">
<b> <b>
{if isset($thisUser) && $thisUser->getId() === $wallId} {if isset($thisUser) && $thisUser->getId() === $post->getTargetWall()}
вашей {_post_on_your_wall}
{/if} {elseif $wallOwner instanceof \openvk\Web\Models\Entities\Club}
стене {tr("post_on_group_wall", ovk_proc_strtr($wallOwner->getName(), 52))}
{if $wallId < 0} {else}
группы {tr("post_on_user_wall", $wallOwner->getMorphedName("genitive", false))}
{/if} {/if}
</b> </b>
</a> </a>
@ -58,7 +56,7 @@
{var $actualAuthor = $post->getOwner(false)} {var $actualAuthor = $post->getOwner(false)}
<span> <span>
{_author}: {_author}:
<a href="{$actualAuthor->getURL()}"> <a href="{$actualAuthor->getURL()}" class="mention" data-mention-ref="{$actualAuthor->getId()}">
{$actualAuthor->getCanonicalName()} {$actualAuthor->getCanonicalName()}
</a> </a>
</span> </span>

View file

@ -11,9 +11,11 @@
<div class="post-upload"> <div class="post-upload">
{_attachment}: <span>(unknown)</span> {_attachment}: <span>(unknown)</span>
</div> </div>
<div class="post-has-poll">
{_poll}
</div>
<div n:if="$postOpts ?? true" class="post-opts"> <div n:if="$postOpts ?? true" class="post-opts">
{var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']} {var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']}
{if !is_null($thisUser) && !is_null($club ?? NULL) && $owner < 0} {if !is_null($thisUser) && !is_null($club ?? NULL) && $owner < 0}
{if $club->canBeModifiedBy($thisUser)} {if $club->canBeModifiedBy($thisUser)}
<script> <script>
@ -43,6 +45,7 @@
<input type="checkbox" name="nsfw" /> {_contains_nsfw} <input type="checkbox" name="nsfw" /> {_contains_nsfw}
</label> </label>
</div> </div>
<div n:if="!($postOpts ?? true) && !is_null($thisUser) && !is_null($club ?? NULL) && $club->canBeModifiedBy($thisUser)" class="post-opts"> <div n:if="!($postOpts ?? true) && !is_null($thisUser) && !is_null($club ?? NULL) && $club->canBeModifiedBy($thisUser)" class="post-opts">
<label> <label>
<input type="checkbox" name="as_group" /> {_comment_as_group} <input type="checkbox" name="as_group" /> {_comment_as_group}
@ -50,6 +53,7 @@
</div> </div>
<input type="file" class="postFileSel" id="postFilePic" name="_pic_attachment" accept="image/*" style="display:none;" /> <input type="file" class="postFileSel" id="postFilePic" name="_pic_attachment" accept="image/*" style="display:none;" />
<input type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display:none;" /> <input type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display:none;" />
<input type="hidden" name="poll" value="none" />
<input type="hidden" name="type" value="1" /> <input type="hidden" name="type" value="1" />
<input type="hidden" name="hash" value="{$csrfToken}" /> <input type="hidden" name="hash" value="{$csrfToken}" />
<br/> <br/>
@ -75,6 +79,10 @@
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/draw-brush.png" /> <img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/draw-brush.png" />
{_graffiti} {_graffiti}
</a> </a>
<a n:if="$polls ?? false" href="javascript:initPoll({$textAreaId})">
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/office-chart-bar-stacked.png" />
{_poll}
</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -8,7 +8,7 @@
</div> </div>
<div> <div>
<div n:if="$canPost" class="content_subtitle"> <div n:if="$canPost" class="content_subtitle">
{include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true} {include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true, polls => true}
</div> </div>
<div class="content"> <div class="content">

View file

@ -21,7 +21,8 @@ class DateTime
$then = date_create("@" . $this->timestamp); $then = date_create("@" . $this->timestamp);
$now = date_create(); $now = date_create();
$diff = date_diff($now, $then); $diff = date_diff($now, $then);
if($diff->invert === 0) return __OPENVK_ERROR_CLOCK_IN_FUTURE; if($diff->invert === 0)
return ovk_strftime_safe("%e %B %Y ", $this->timestamp) . tr("time_at_sp") . ovk_strftime_safe(" %R %p", $this->timestamp);
if($this->timestamp >= strtotime("midnight")) { # Today if($this->timestamp >= strtotime("midnight")) { # Today
if($diff->h >= 1) if($diff->h >= 1)
@ -52,13 +53,10 @@ class DateTime
switch($type) { switch($type) {
case static::RELATIVE_FORMAT_NORMAL: case static::RELATIVE_FORMAT_NORMAL:
return mb_convert_case($this->zmdate(), MB_CASE_TITLE_SIMPLE); return mb_convert_case($this->zmdate(), MB_CASE_TITLE_SIMPLE);
break;
case static::RELATIVE_FORMAT_LOWER: case static::RELATIVE_FORMAT_LOWER:
return $this->zmdate(); return $this->zmdate();
break;
case static::RELATIVE_FORMAT_SHORT: case static::RELATIVE_FORMAT_SHORT:
return ""; return "";
break;
} }
} }

View file

@ -23,9 +23,11 @@ services:
- openvk\Web\Presenters\AppsPresenter - openvk\Web\Presenters\AppsPresenter
- openvk\Web\Presenters\ThemepacksPresenter - openvk\Web\Presenters\ThemepacksPresenter
- openvk\Web\Presenters\VKAPIPresenter - openvk\Web\Presenters\VKAPIPresenter
- openvk\Web\Presenters\PollPresenter
- openvk\Web\Presenters\BannedLinkPresenter - openvk\Web\Presenters\BannedLinkPresenter
- openvk\Web\Models\Repositories\Users - openvk\Web\Models\Repositories\Users
- openvk\Web\Models\Repositories\Posts - openvk\Web\Models\Repositories\Posts
- openvk\Web\Models\Repositories\Polls
- openvk\Web\Models\Repositories\Photos - openvk\Web\Models\Repositories\Photos
- openvk\Web\Models\Repositories\Albums - openvk\Web\Models\Repositories\Albums
- openvk\Web\Models\Repositories\Clubs - openvk\Web\Models\Repositories\Clubs
@ -43,4 +45,6 @@ services:
- openvk\Web\Models\Repositories\Topics - openvk\Web\Models\Repositories\Topics
- openvk\Web\Models\Repositories\Applications - openvk\Web\Models\Repositories\Applications
- openvk\Web\Models\Repositories\ContentSearchRepository - openvk\Web\Models\Repositories\ContentSearchRepository
- openvk\Web\Models\Repositories\Aliases
- openvk\Web\Models\Repositories\BannedLinks - openvk\Web\Models\Repositories\BannedLinks
- openvk\Web\Presenters\MaintenancePresenter

View file

@ -273,6 +273,10 @@ routes:
handler: "Apps->edit" handler: "Apps->edit"
- url: "/apps/uninstall" - url: "/apps/uninstall"
handler: "Apps->unInstall" handler: "Apps->unInstall"
- url: "/poll{num}"
handler: "Poll->view"
- url: "/poll{num}/voters"
handler: "Poll->voters"
- url: "/admin" - url: "/admin"
handler: "Admin->index" handler: "Admin->index"
- url: "/admin/users" - url: "/admin/users"
@ -331,3 +335,7 @@ routes:
handler: "UnknownTextRouteStrategy->delegate" handler: "UnknownTextRouteStrategy->delegate"
placeholders: placeholders:
shortCode: "[a-z][a-z0-9\\@\\.\\_]{0,30}[a-z0-9]" shortCode: "[a-z][a-z0-9\\@\\.\\_]{0,30}[a-z0-9]"
- url: "/maintenance/{text}"
handler: "Maintenance->section"
- url: "/maintenances/"
handler: "Maintenance->all"

View file

@ -448,6 +448,15 @@ table {
color: #e8e8e8; color: #e8e8e8;
} }
.button[disabled] {
filter: opacity(0.5);
cursor: not-allowed;
}
.button[disabled]:hover {
color: #fff;
}
.button-loading { .button-loading {
display: inline-block; display: inline-block;
background-image: url('/assets/packages/static/openvk/img/loading_mini.gif'); background-image: url('/assets/packages/static/openvk/img/loading_mini.gif');
@ -801,6 +810,8 @@ table.User {
padding: 0 10px; padding: 0 10px;
margin-left: -10px; margin-left: -10px;
width: 607px; width: 607px;
overflow-x: auto;
white-space: nowrap;
} }
.tabs.stupid-fix { .tabs.stupid-fix {
@ -1338,14 +1349,14 @@ body.scrolled .toTop:hover {
font-weight: bold; font-weight: bold;
} }
.post-upload { .post-upload, .post-has-poll {
margin-top: 11px; margin-top: 11px;
margin-left: 3px; margin-left: 3px;
color: #3c3c3c; color: #3c3c3c;
display: none; display: none;
} }
.post-upload::before { .post-upload::before, .post-has-poll::before {
content: " "; content: " ";
width: 8px; width: 8px;
height: 8px; height: 8px;
@ -2051,6 +2062,88 @@ table td[width="120"] {
max-height: 250px; max-height: 250px;
} }
.poll {
padding: 8px;
transition: .1s filter ease-in;
border: 1px solid #e3e3e3;
}
.poll.loading {
filter: opacity(0.5);
cursor: progress;
user-select: none;
}
.poll.loading * {
pointer-events: none;
}
.poll-embed {
float: right;
}
.poll h4 {
padding-bottom: 4px;
margin-bottom: 8px;
}
.poll-meta .nobold {
font-style: oblique;
}
.poll-result-barspace {
display: flex;
}
.poll-result {
margin-bottom: 5px;
}
.poll-result a {
color: unset;
}
.poll-result-bar {
position: relative;
margin: 5px 0;
background-color: #f7f7f7;
height: 13pt;
flex: 14;
}
span.poll-result-count {
position: absolute;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
color: #7d96af;
}
.poll-result-bar-sub {
height: 100%;
background-color: #d9e1ea;
}
.poll-result-pct {
flex: 2;
display: flex;
justify-content: flex-end;
align-items: center;
padding-left: 5px;
}
a.poll-retract-vote {
float: right;
}
.tippy-box[data-theme~="vk"] {
user-select: none;
background-color: #fff;
border: 1px solid #DCE1E6;
border-radius: 1px;
}
@keyframes appearing { @keyframes appearing {
from { from {
opacity: 0; opacity: 0;

View file

@ -0,0 +1,42 @@
var tooltipTemplate = Handlebars.compile(`
<table>
<tr>
<td width="54" valign="top">
<img src="{{ava}}" width="54" />
</td>
<td width="1"></td>
<td width="150" valign="top">
<span>
<a href="{{url}}"><b>{{name}}</b></a>
{{#if verif}}
<img class="name-checkmark" src="/assets/packages/static/openvk/img/checkmark.png" />
{{/if}}
</span><br/>
<span style="color: #444;">{{online}}</span><br/>
<span style="color: #000;">{{about}}</span>
</td>
</tr>
</table>
`);
tippy(".mention", {
theme: "light vk",
content: "⌛",
allowHTML: true,
interactive: true,
interactiveDebounce: 500,
onCreate: async function(that) {
that._resolvedMention = null;
},
onShow: async function(that) {
if(!that._resolvedMention) {
let id = Number(that.reference.dataset.mentionRef);
that._resolvedMention = await API.Mentions.resolve(id);
}
let res = that._resolvedMention;
that.setContent(tooltipTemplate(res));
}
});

154
Web/static/js/al_polls.js Normal file
View file

@ -0,0 +1,154 @@
function escapeXML(text) {
return $("<span/>").text(text).html();
}
async function pollRetractVote(id) {
let poll = $(`.poll[data-id=${id}]`);
poll.addClass("loading");
try {
let html = (await API.Polls.unvote(poll.data("id"))).html;
poll.prop("outerHTML", html);
} catch(e) {
MessageBox(tr("error"), "Sorry: " + e.message, ["OK"], [Function.noop]);
} finally {
poll.removeClass("loading");
}
}
async function pollFormSubmit(e, form) {
e.preventDefault();
form = $(form);
let options;
let isMultiple = form.data("multi");
let pollId = form.data("pid");
let formData = form.serializeArray();
if(!isMultiple) {
options = [Number(formData[0].value)];
} else {
options = [];
formData.forEach(function(record) {
if(!record.name.startsWith("option") || record.value !== "on")
return;
options.push(Number(record.name.substr(6)));
});
}
let poll = form.parent();
poll.addClass("loading");
try {
let html = (await API.Polls.vote(pollId, options.join(","))).html;
poll.prop("outerHTML", html);
} catch(e) {
MessageBox(tr("error"), "Sorry: " + e.message, ["OK"], [Function.noop]);
} finally {
poll.removeClass("loading");
}
}
function pollCheckBoxPressed(cb) {
cb = $(cb);
let form = cb.parent().parent().parent().parent();
let checked = $("input:checked", form);
if(checked.length >= 1)
$("input[type=submit]", form).removeAttr("disabled");
else
$("input[type=submit]", form).attr("disabled", "disabled");
}
function pollRadioPressed(radio) {
let form = $(radio).parent().parent().parent().parent();
form.submit();
}
function initPoll(id) {
let form = $(`#wall-post-input${id}`).parent();
let mBody = `
<div id="poll_editor${id}">
<input type="text" name="title" placeholder="${tr("poll_title")}" />
<div class="poll-options" style="margin-top: 10px;"></div>
<input type="text" name="newOption" placeholder="${tr("poll_add_option")}" style="margin: 5px 0;" />
<hr/>
<label><input type="checkbox" name="anon" /> ${tr("poll_anonymous")}</label><br/>
<label><input type="checkbox" name="multi" /> ${tr("poll_multiple")}</label><br/>
<label><input type="checkbox" name="locked" /> ${tr("poll_locked")}</label><br/>
<label>
<input type="checkbox" name="expires" />
${tr("poll_edit_expires")}
<select name="expires_in" style="width: unset;">
${[...Array(32).keys()].reduce((p, c) => (!p ? '' : p) + ("<option value='" + c + "'>" + c + " " + tr("poll_edit_expires_days") + "</option>\n"))}
</select>
</label>
<div class="nobold" style="margin: 10px 5px 0">${tr("poll_editor_tips")}</div>
</div>
`;
MessageBox(tr("create_poll"), mBody, [tr("attach"), tr("cancel")], [
function() {
let dialog = $(this.$dialog().nodes[0]);
$("input", dialog).unbind();
let title = $("input[name=title]", dialog).val();
let anon = $("input[name=anon]", dialog).prop("checked") ? "yes" : "no";
let multi = $("input[name=multi]", dialog).prop("checked") ? "yes" : "no";
let lock = $("input[name=locked]", dialog).prop("checked") ? "yes" : "no";
let expires = "infinite";
if($("input[name=expires]", dialog).prop("checked"))
expires = $("select[name=expires_in]", dialog).val();
let options = "";
$(".poll-option", dialog).each(function() {
if($(this).val().length === 0)
return;
options += `<option>${escapeXML($(this).val())}</option>`;
});
let xml = `
<Poll title="${title}" anonymous="${anon}" multiple="${multi}" locked="${lock}" duration="${expires}">
<options>${options}</options>
</Poll>
`;
$("input[name=poll]", form).val(xml);
$(".post-has-poll", form).show();
},
function() {
$("input", $(this.$dialog().nodes[0])).unbind();
}
]);
let editor = $(`#poll_editor${id}`);
$("input[name=newOption]", editor).bind("focus", function() {
let newOption = $('<input type="text" class="poll-option" style="margin: 5px 0;" />');
newOption.appendTo($(".poll-options", editor));
newOption.focus();
newOption.bind("keydown", function(e) {
if(e.key === "Enter" && $(this).next().length === 0) {
$("input[name=newOption]", editor).focus();
return;
}
if($(this).val().length > 0)
return;
if(e.key !== "Backspace")
return;
if($(this).siblings().length === 0)
return;
if($(this).prev().length === 0)
$(this).next().focus();
else
$(this).prev().focus();
e.preventDefault();
$(this).unbind("keydown");
$(this).remove();
});
});
}

View file

@ -2,6 +2,7 @@
"dependencies": { "dependencies": {
"@atlassian/aui": "^8.5.1", "@atlassian/aui": "^8.5.1",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"handlebars": "^4.7.7",
"jquery": "^2.1.0", "jquery": "^2.1.0",
"knockout": "^3.5.1", "knockout": "^3.5.1",
"ky": "^0.19.0", "ky": "^0.19.0",
@ -15,6 +16,7 @@
"requirejs": "^2.3.6", "requirejs": "^2.3.6",
"soundjs": "^1.0.1", "soundjs": "^1.0.1",
"textfit": "^2.4.0", "textfit": "^2.4.0",
"tippy.js": "^6.3.7",
"umbrellajs": "^3.1.0" "umbrellajs": "^3.1.0"
} }
} }

View file

@ -29,6 +29,11 @@
resolved "https://registry.yarnpkg.com/@atlassian/tipsy/-/tipsy-1.3.2.tgz#ab759d461670d712425b2dac7573b79575a10502" resolved "https://registry.yarnpkg.com/@atlassian/tipsy/-/tipsy-1.3.2.tgz#ab759d461670d712425b2dac7573b79575a10502"
integrity sha512-H7qWMs66bztELt2QpOCLYDU9ZM3VZfE0knbRHHLBukH7v9dMkIS5ZwqcGREjWnVt0KNETaBeXxj0FD88TEOGVw== integrity sha512-H7qWMs66bztELt2QpOCLYDU9ZM3VZfE0knbRHHLBukH7v9dMkIS5ZwqcGREjWnVt0KNETaBeXxj0FD88TEOGVw==
"@popperjs/core@^2.9.0":
version "2.11.6"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
asap@~2.0.3: asap@~2.0.3:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
@ -89,6 +94,18 @@ fbjs@^0.8.0:
setimmediate "^1.0.5" setimmediate "^1.0.5"
ua-parser-js "^0.7.18" ua-parser-js "^0.7.18"
handlebars@^4.7.7:
version "4.7.7"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
dependencies:
minimist "^1.2.5"
neo-async "^2.6.0"
source-map "^0.6.1"
wordwrap "^1.0.0"
optionalDependencies:
uglify-js "^3.1.4"
iconv-lite@^0.6.2: iconv-lite@^0.6.2:
version "0.6.3" version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
@ -170,6 +187,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
dependencies: dependencies:
js-tokens "^3.0.0 || ^4.0.0" js-tokens "^3.0.0 || ^4.0.0"
minimist@^1.2.5:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
monaco-editor@^0.20.0: monaco-editor@^0.20.0:
version "0.20.0" version "0.20.0"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea" resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea"
@ -185,6 +207,11 @@ msgpack-lite@^0.1.26:
int64-buffer "^0.1.9" int64-buffer "^0.1.9"
isarray "^1.0.0" isarray "^1.0.0"
neo-async@^2.6.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
node-fetch@^1.0.1: node-fetch@^1.0.1:
version "1.7.3" version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@ -271,11 +298,23 @@ soundjs@^1.0.1:
resolved "https://registry.yarnpkg.com/soundjs/-/soundjs-1.0.1.tgz#99970542d28d0df2a1ebd061ae75c961a98c8180" resolved "https://registry.yarnpkg.com/soundjs/-/soundjs-1.0.1.tgz#99970542d28d0df2a1ebd061ae75c961a98c8180"
integrity sha512-MgFPvmKYfpcNiE3X5XybNvScie3DMQlZgmNzUn4puBcpw64f4LqjH/fhM8Sb/eTJ8hK57Crr7mWy0bfJOqPj6Q== integrity sha512-MgFPvmKYfpcNiE3X5XybNvScie3DMQlZgmNzUn4puBcpw64f4LqjH/fhM8Sb/eTJ8hK57Crr7mWy0bfJOqPj6Q==
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
textfit@^2.4.0: textfit@^2.4.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/textfit/-/textfit-2.4.0.tgz#80cba8006bfb9c3d9d552739257957bdda95c79c" resolved "https://registry.yarnpkg.com/textfit/-/textfit-2.4.0.tgz#80cba8006bfb9c3d9d552739257957bdda95c79c"
integrity sha512-/x4aoY5+/tJmu+iwpBH1yw75TFp86M6X15SvaaY/Eep7YySQYtqdOifEtfvVyMwzl7SZ+G4RQw00FD9g5R6i1Q== integrity sha512-/x4aoY5+/tJmu+iwpBH1yw75TFp86M6X15SvaaY/Eep7YySQYtqdOifEtfvVyMwzl7SZ+G4RQw00FD9g5R6i1Q==
tippy.js@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
dependencies:
"@popperjs/core" "^2.9.0"
trim-extra-html-whitespace@1.3.0: trim-extra-html-whitespace@1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/trim-extra-html-whitespace/-/trim-extra-html-whitespace-1.3.0.tgz#b47efb0d1a5f2a56a85cc45cea525651e93404cf" resolved "https://registry.yarnpkg.com/trim-extra-html-whitespace/-/trim-extra-html-whitespace-1.3.0.tgz#b47efb0d1a5f2a56a85cc45cea525651e93404cf"
@ -286,6 +325,11 @@ ua-parser-js@^0.7.18:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
uglify-js@^3.1.4:
version "3.17.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.3.tgz#f0feedf019c4510f164099e8d7e72ff2d7304377"
integrity sha512-JmMFDME3iufZnBpyKL+uS78LRiC+mK55zWfM5f/pWBJfpOttXAqYfdDGRukYhJuyRinvPVAtUhvy7rlDybNtFg==
umbrellajs@^3.1.0: umbrellajs@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/umbrellajs/-/umbrellajs-3.1.0.tgz#a4e6f0f6381f9d93110b5eee962e0e0864b10bd0" resolved "https://registry.yarnpkg.com/umbrellajs/-/umbrellajs-3.1.0.tgz#a4e6f0f6381f9d93110b5eee962e0e0864b10bd0"
@ -300,3 +344,8 @@ whatwg-fetch@>=0.10.0:
version "3.6.2" version "3.6.2"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
wordwrap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==

2504
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,36 +2,36 @@ OpenVK-KB-Heading: Правила
Адміністрація сайту дозволяє Вам користуватися сайтом на умовах, вказаних у цих правилах. Адміністрація сайту дозволяє Вам користуватися сайтом на умовах, вказаних у цих правилах.
Робити можна усе те, що не відноситься до заборонених дій, а до заборонених дій відносяться: Робити можна усе те, що не належать до заборонених дій, а до заборонених дій відносяться:
1. Відмова у підпорядкуванні правилам чи їх ігнорування, 1. Відмова у підпорядкуванні правилам чи їх ігнорування,
2. Відмова від своєчасного виконання запитів агентів тех. підтримки чи адміністраціі, 2. Відмова від своєчасного виконання запитів агентів технічної підтримки чи адміністрації,
3. Використання чужих сторінок без дозволу власника, 3. Використання чужих сторінок без дозволу власника,
4. Видавання себе за інших людей для отримання вигоди. Винятки: 4. Видавання себе за інших людей для отримання вигоди. Винятки:
1. Людина дозволила використання своєї особи, 1. Людина дозволила використання своєї особи,
2. Людина є оригінальним персонажем, та належить Вам 2. Людина є оригінальним персонажем, та належить Вам
5. Створення массових розсилок будь-яким способом, 5. Створення масових розсилок будь-яким способом,
6. Створення ситуацій, будь-яким чином заважаючих роботі OpenVK, 6. Створення ситуацій, що заважає роботі OpenVK,
7. Публікація та зберігання на ресурсі вмісту, який: 7. Публікація та зберігання на ресурсі вмісту, який:
1. Є незаконним на територіі Франціі, 1. Є незаконним на території Франції,
2. Містить порнографічні сцени за участю осіб молодше 18 років, 2. Містить порнографічні сцени за участю осіб молодше 18 років,
3. Містить рекламу заборонених у Франціі препаратів чи інструкціі по їх виготовленню, 3. Містить рекламу заборонених у Франції препаратів чи інструкції по їх виготовленню,
4. Містить інформацію заборонену на територіі Франціі, 4. Містить інформацію заборонену на території Франції,
5. Містить сцени нелюдського поводження з людьми або тваринами, 5. Містить сцени нелюдського поводження з людьми або тваринами,
6. Порушує авторські та суміжні права, 6. Порушує авторські та суміжні права,
7. Порушує права людини, 7. Порушує права людини,
8. Заважає користувачам виконувати правила чи нормально користуватися послугами, наданими проектом OpenVK. 8. Заважає користувачам виконувати правила чи нормально користуватися послугами, наданими проєктом OpenVK.
Адміністрация є вищим органом влади, який має повне право приймати рішення на рахунок спірних ситуацій, не описаних у правилах. Адміністрація є вищим органом влади, який має повне право приймати рішення на рахунок спірних ситуацій, не описаних у правилах.
Адміністрація може видати покарання, якщо користувачі: Адміністрація може видати покарання, якщо користувачі:
1. Публікують заборонений контент на сторінках інших користувачів, 1. Публікують заборонений контент на сторінках інших користувачів,
2. Обдурюють адміністрацію чи агентів тех. підтримки, 2. Обдурюють адміністрацію чи агентів технічної підтримки,
3. Вводять в оману користувачів сайту, 3. Вводять в оману користувачів сайту,
4. Публічно необґрунтовано критикують OpenVK, чи адміністрацію з ціллю принизити чи образити учасників проекту, чи його керівників, 4. Публічно необґрунтовано критикують OpenVK чи адміністрацію з ціллю принизити, образити учасників проєкт, його керівників,
5. Забанені у офіційному чаті OpenVK у Telegram, 5. Заблоковані в офіційному чаті OpenVK у Telegram,
6. Не поважають адміністрацію проекту чи агентів тех. підтримки. 6. Не поважають адміністрацію проєкт чи агентів технічної підтримки.
При порушенні правил, адміністрація може: При порушенні правил, адміністрація може:

View file

@ -1,14 +1,13 @@
CREATE TABLE `links_banned` ( CREATE TABLE `links_banned` (
`id` bigint UNSIGNED NOT NULL, `id` bigint UNSIGNED NOT NULL,
`domain` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `domain` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
`regexp_rule` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `regexp_rule` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci NOT NULL,
`reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, `reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci,
`initiator` bigint UNSIGNED NOT NULL `initiator` bigint UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
ALTER TABLE `links_banned` ALTER TABLE `links_banned`
ADD PRIMARY KEY (`id`); ADD PRIMARY KEY (`id`);
ALTER TABLE `links_banned` ALTER TABLE `links_banned`
MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT; MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT;
COMMIT;

View file

@ -0,0 +1,11 @@
CREATE TABLE `aliases` (
`id` bigint UNSIGNED NOT NULL,
`owner_id` bigint NOT NULL,
`shortcode` varchar(36) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
ALTER TABLE `aliases`
ADD PRIMARY KEY (`id`);
ALTER TABLE `aliases`
MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT;

View file

@ -0,0 +1,32 @@
CREATE TABLE IF NOT EXISTS `polls` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`owner` bigint unsigned NOT NULL,
`title` text NOT NULL,
`allows_multiple` bit(1) NOT NULL DEFAULT b'0',
`is_anonymous` bit(1) NOT NULL DEFAULT b'0',
`can_revote` bit(1) NOT NULL DEFAULT b'0',
`until` bigint unsigned DEFAULT NULL,
`ended` bit(1) NOT NULL DEFAULT b'0',
`deleted` bit(1) NOT NULL DEFAULT b'0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `poll_options` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`poll` bigint unsigned NOT NULL,
`name` varchar(512) NOT NULL,
PRIMARY KEY (`id`),
KEY `poll` (`poll`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
CREATE TABLE IF NOT EXISTS `poll_votes` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user` bigint unsigned NOT NULL,
`poll` bigint unsigned NOT NULL,
`option` bigint unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_option` (`user`,`option`),
KEY `option` (`option`),
KEY `poll` (`poll`),
KEY `user_poll` (`user`,`poll`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;

View file

@ -17,6 +17,8 @@
"password" = "Գաղտնաբառ"; "password" = "Գաղտնաբառ";
"registration" = "Գրանցում"; "registration" = "Գրանցում";
"forgot_password" = "Մոռացե՞լ եք գաղտնաբառը"; "forgot_password" = "Մոռացե՞լ եք գաղտնաբառը";
"checkbox_in_registration" = "Ես համաձայն եմ <a href='/privacy'>կոնֆիդենցիալության քաղաքականությանն</a> ու <a href='/about'>կայքի կանոնադրությանը</a>։";
"checkbox_in_registration_unchecked" = "Դուք պետք է համաձայնվեք պայմանների հետ նախքան գրանցվելը։";
"login_failed" = "Չհաջողվեց մուտք գործել"; "login_failed" = "Չհաջողվեց մուտք գործել";
"invalid_username_or_password" = "Սխալ օգտատիրոջ անուն կամ գաղտնաբառ։ Դուք կարող եք <a href='/restore'>վերականգնել ձեր գաղտնաբառը</a>։"; "invalid_username_or_password" = "Սխալ օգտատիրոջ անուն կամ գաղտնաբառ։ Դուք կարող եք <a href='/restore'>վերականգնել ձեր գաղտնաբառը</a>։";
@ -93,6 +95,9 @@
"years_many" = "$1 տարեկան"; "years_many" = "$1 տարեկան";
"years_other" = "$1 տարեկան"; "years_other" = "$1 տարեկան";
"show_my_birthday" = "Ցույց տալ ծննդյան օրը";
"show_only_month_and_day" = "Ցուցադրել միայն ամիսն ու օրը";
"relationship" = "Ընտանեկան դրություն"; "relationship" = "Ընտանեկան դրություն";
"relationship_0" = "Ընտրված չէ"; "relationship_0" = "Ընտրված չէ";
@ -144,8 +149,8 @@
"updated_at" = "Թարմացված է $1"; "updated_at" = "Թարմացված է $1";
"user_banned" = "Ցավո՛ք, մենք ստիպված <b>$1</b>-ի էջը կասեցրել ենք։"; "user_banned" = "Ցավո՛ք, մենք ստիպված կասեցրել ենք <b>$1</b>-ի էջը։";
"user_banned_comment" = "Մոդերատորի մեկնաբանությունը․ "; "user_banned_comment" = "Մոդերատորի մեկնաբանությունը․";
/* Wall */ /* Wall */
@ -154,6 +159,10 @@
"post_writes_m" = "գրել է"; "post_writes_m" = "գրել է";
"post_writes_f" = "գրել է"; "post_writes_f" = "գրել է";
"post_writes_g" = "հրապարակել են"; "post_writes_g" = "հրապարակել են";
"post_deact_m" = "ջնջել է էջը հետևյալ բառերով.";
"post_deact_f" = "ջնջել է էջը հետևյալ բառերով.";
"post_deact_silent_m" = "սուս ու փուս ջնջել է էջը։";
"post_deact_silent_f" = "սուս ու փուս ջնջել է էջը։";
"wall" = "Պատ"; "wall" = "Պատ";
"post" = "Գրություն"; "post" = "Գրություն";
"write" = "Գրել"; "write" = "Գրել";
@ -167,7 +176,7 @@
"comments_tip" = "Եղե՛ք առաջինը ով կթողնի իր կարծիքը։"; "comments_tip" = "Եղե՛ք առաջինը ով կթողնի իր կարծիքը։";
"your_comment" = "Ձեր մեկնաբանությունը"; "your_comment" = "Ձեր մեկնաբանությունը";
"shown" = "Ցուցադրված է"; "shown" = "Ցուցադրված է";
"x_out_of" = "$1 –ը ՝"; "x_out_of" = "$1ը";
"wall_zero" = "գրություն չկա"; "wall_zero" = "գրություն չկա";
"wall_one" = "մեկ գրություն"; "wall_one" = "մեկ գրություն";
"wall_few" = "$1 գրություն"; "wall_few" = "$1 գրություն";
@ -215,6 +224,8 @@
"incoming_req" = "Բաժանորդներ"; "incoming_req" = "Բաժանորդներ";
"outcoming_req" = "Հայցեր"; "outcoming_req" = "Հայցեր";
"req" = "Հայցեր"; "req" = "Հայցեր";
"friends_online" = "Ընկերները ցանցում";
"all_friends" = "Բոլոր ընկերները";
"req_zero" = "Ոչ մի հայտ չի գտնվել..."; "req_zero" = "Ոչ մի հայտ չի գտնվել...";
"req_one" = "Գտնվեց մեկ հայտ"; "req_one" = "Գտնվեց մեկ հայտ";
@ -246,6 +257,12 @@
"subscriptions_many" = "$1 բաժանորդագրություն"; "subscriptions_many" = "$1 բաժանորդագրություն";
"subscriptions_other" = "$1 բաժանորդագրություն"; "subscriptions_other" = "$1 բաժանորդագրություն";
"friends_list_online_zero" = "Դուք դեռ չունեք ցանցի մեջ գտնվող ընկերներ";
"friends_list_online_one" = "Ձեր $1 ընկերը ցանցի մեջ է";
"friends_list_online_few" = "Ձեր $1 ընկերները ցանցի մեջ են";
"friends_list_online_many" = "Ձեր $1 ընկերները ցանցի մեջ են";
"friends_list_online_other" = "Ձեր $1 ընկերները ցանցի մեջ են";
/* Group */ /* Group */
"name_group" = "Անվանում"; "name_group" = "Անվանում";
@ -364,6 +381,10 @@
"edit_note" = "Խմբագրել նշումը"; "edit_note" = "Խմբագրել նշումը";
"actions" = "Գործողություններ"; "actions" = "Գործողություններ";
"notes_start_screen" = "Նշումների շնորհիվ Դուք կարող եք կիսվել ընկերների հետ տարբեր իրադարձություններով, և իմանալ թե ինչ է կատարվում իրենց մոտ։"; "notes_start_screen" = "Նշումների շնորհիվ Դուք կարող եք կիսվել ընկերների հետ տարբեր իրադարձություններով, և իմանալ թե ինչ է կատարվում իրենց մոտ։";
"note_preview" = "Նախադիտում";
"note_preview_warn" = "Սա ընդամենը նախադիտում է";
"note_preview_warn_details" = "Պահպանելուց հետո նշումները կարող են այլ տեսք ունենալ։ Ու մեկ էլ, այդքան հաճախ նախադիտում մի արեք։";
"note_preview_empty_err" = "Ինչու՞ նախադիտել նշումը առանց վերնագրի կամ բովանդակության։";
"edited" = "Խմբագրված է"; "edited" = "Խմբագրված է";
@ -428,6 +449,7 @@
"avatar" = "Ավատար"; "avatar" = "Ավատար";
"privacy" = "Գաղտնիություն"; "privacy" = "Գաղտնիություն";
"interface" = "Արտաքին տեսք"; "interface" = "Արտաքին տեսք";
"security" = "Անվտանգություն";
"profile_picture" = "Էջի նկար"; "profile_picture" = "Էջի նկար";
@ -445,7 +467,7 @@
"arbitrary_avatars" = "Կամայական"; "arbitrary_avatars" = "Կամայական";
"cut" = "Կտրվածք"; "cut" = "Կտրվածք";
"round_avatars" = "Կլոր ավատար"; "round_avatars" = "Շրջանաձև";
"apply_style_for_this_device" = "Հաստատել տեսքը միայն այս սարքի համար"; "apply_style_for_this_device" = "Հաստատել տեսքը միայն այս սարքի համար";
@ -508,6 +530,33 @@
"email_change_confirm_message" = "Որպեսզի Ձեր փոփոխությունը կատարվի, հաստատե՛ք Ձեր նոր էլեկտրոնային հասցեն։ Այնտեղ մենք ուղարկել ենք հրահանգները։"; "email_change_confirm_message" = "Որպեսզի Ձեր փոփոխությունը կատարվի, հաստատե՛ք Ձեր նոր էլեկտրոնային հասցեն։ Այնտեղ մենք ուղարկել ենք հրահանգները։";
"profile_deactivate" = "Էջի հեռացում";
"profile_deactivate_button" = "Ջնջել էջը";
"profile_deactivate_header" = "Մենք ցավում ենք որ Դուք ցանկանում եք ջնջել ձեր էջը։ Դրա համար Դուք կարող եք նշել հեռացման պատճառը և Ձեր կարծիքը այդ առումով։ Մեզ համար կարևոր է Ձեր կարծիքը, որպեսզի դարձնենք կայքը ավելի լավը։";
"profile_deactivate_reason_header" = "Խնդրում ենք, նշել Ձեր էջի ջնջման պատճառները";
"profile_deactivate_reason_1" = "Ես ունեմ այլ էջ այս կայնքում";
"profile_deactivate_reason_1_text" = "Ես նոր էջ եմ սկսել ու ցանկանում եմ սկսել ամեն ինչ նորից";
"profile_deactivate_reason_2" = "Կայքն ինձնից շատ ժամանակ է խլում";
"profile_deactivate_reason_2_text" = "Թեկուզ այս կայքը լավն է, բայց ցավոք ինձնից ահագին ժամանակ է խլում։";
"profile_deactivate_reason_3" = "Կայքը բազմաթիվ անցանկանալի մատերիալներ է պարունակում";
"profile_deactivate_reason_3_text" = "Ես բազմաթիվ պոռնոգրաֆիա ու անօրինական կինոներ եմ գրել բոլ ա։ Հիմա հավես չկա։";
"profile_deactivate_reason_4" = "Ինձ անհանգստացնում է իմ տվյալների անվտանգությունը";
"profile_deactivate_reason_4_text" = "Ինձ հետևում են ու շատ վախենալու է ինձ այստեղ գտնվելը";
"profile_deactivate_reason_5" = "Իմ էջը չեն մեկնաբանում";
"profile_deactivate_reason_5_text" = "Ինձ այստեղ շան տեղ դնող չկա ու ես տխրում եմ։ Դուք կզղջաք որ ես հեռացա...";
"profile_deactivate_reason_6" = "Այլ պատճառ";
"profile_deactivated_msg" = "Ձեր էջը <b>ջնջված է</b>։<br/><br/>Եթե Դուք ուզենաք նորից օգտվել Ձեր էջով, կարող եք <a href='/settings/reactivate'>ապաակտիվացնել այն</a> մինչև $1:";
"profile_deactivated_status" = "Էջը ջնջված է";
"profile_deactivated_info" = "Օգտատիրոջ էջը հեռացվել է։<br/>Մանրամասն տեղեկատվությունը բացակայում է։";
"share_with_friends" = "Պատմել ընկերներին";
"end_all_sessions" = "Դուրս գալ բոլոր սեսսիաներից";
"end_all_sessions_description" = "Եթե ցանկանում եք դուրս գալ $1ից ամեն դեվայսից, սեղմե՛ք ներքևի կոճակը";
"end_all_sessions_done" = "Բոլոր սեսսիաները նետված են, ներառյալ բջջային հավելվածները";
/* Two-factor authentication */ /* Two-factor authentication */
"two_factor_authentication" = "Երկքայլ աուտենտիֆիկացիա"; "two_factor_authentication" = "Երկքայլ աուտենտիֆիկացիա";
@ -708,6 +757,75 @@
"users_gifts" = "Նվերներ"; "users_gifts" = "Նվերներ";
/* Apps */
"app" = "Հավելված";
"apps" = "Հավելվածներ";
"my_apps" = "Իմ հավելվածները";
"all_apps" = "Բոլոր հավելվածները";
"installed_apps" = "Տեղադրված հավելվածները";
"own_apps" = "Կառավարում";
"own_apps_alternate" = "Իմ այլ հավելվածները";
"app_play" = "միացնել";
"app_uninstall" = "անջատել";
"app_edit" = "խմբագրել";
"app_dev" = "Մշակող";
"create_app" = "Ստեղծել հավելված";
"edit_app" = "Խմբագրել հավելվածը";
"new_app" = "Նոր հավելված";
"app_news" = "Նորություններով նշում";
"app_state" = "Կարգավիճակ";
"app_enabled" = "Միացված է";
"app_creation_hint_url" = "URLում նշեք կոնկրետ հասցեն իր սխեմայով (https), պորտով (80) և անհրաժեշտ միացման կարգավորումներով։";
"app_creation_hint_iframe" = "Ձեր հավելվածը բացված է iframeով։";
"app_balance" = "Հավելվածի հաշվին կա <b>$1</b> ձայն։";
"app_users" = "Ձեր հավելվածով օգտվում է <b>$1</b> հոգի։";
"app_withdrawal_q" = "դուրս բերե՞լ";
"app_withdrawal" = "Միջոցների դուրս բերում";
"app_withdrawal_empty" = "Կներեք, դատարկությունը չհաջողվեց դուրս բերել։";
"app_withdrawal_created" = "$1 ձայնի դուրս բերման հայտը գրանցված է։ Սպասեք հաշվառմանը։";
"appjs_payment" = "Գնման վճարում";
"appjs_payment_intro" = "Դուք պատրաստվում եք հավելվածի գնումը վճարել";
"appjs_order_items" = "Գնման ցուցակ";
"appjs_payment_total" = "Վճարման ընդհանուր գին";
"appjs_payment_confirm" = "Վճարել";
"appjs_err_funds" = "Չհաջողվե՛ց վճարել գնումը անբավարար միջոցների համար։";
"appjs_wall_post" = "Հրապարակել գրությունտը";
"appjs_wall_post_desc" = "ցանկանում է Ձեր պատին գրություն թողնել";
"appjs_sperm_friends" = "ձեր ընկերներին";
"appjs_sperm_friends_desc" = "ավելացնել օգտատերերին որպես ընկերներ և կարդալ Ձեր գրությունները";
"appjs_sperm_wall" = "ձեր պատին";
"appjs_sperm_wall_desc" = "դիտել Ձեր լուրերը, կարդալ պատն ու թողել գրություններ";
"appjs_sperm_messages" = "ձեր նամակներին";
"appjs_sperm_messages_desc" = "կարդալ և գրել նամակներ Ձեր անունից";
"appjs_sperm_groups" = "ձեր հանրություններին";
"appjs_sperm_groups_desc" = "դիտել Ձեր խմբերի ցուցակն ու բաժանորդագրել դեպի այլ խմբեր";
"appjs_sperm_likes" = "լայքելու ֆունկցիոնալին";
"appjs_sperm_likes_desc" = "տեղադրել և հանել \"Դուր գալու\" ռեակցիաները ձայնագրություններից";
"appjs_sperm_request" = "Հասանելիության հարցում";
"appjs_sperm_requests" = "հասանելիություն է խնդրում";
"appjs_sperm_can" = "Հավելվածը կարող է";
"appjs_sperm_allow" = "Թույլատրել";
"appjs_sperm_disallow" = "Չթույլատրել";
"app_uninstalled" = "Հավելվածն անջատված է";
"app_uninstalled_desc" = "Այն Ձեր անունից էլ չի կարող կատարել գործողություններ։";
"app_err_not_found" = "Հավելվածը չի գտնվել";
"app_err_not_found_desc" = "Սխալ կամ անջատված իդենտիֆիկատոր։";
"app_err_forbidden_desc" = "Այս հավելվածը Ձերը չէ։";
"app_err_url" = "Սխալ հասցե";
"app_err_url_desc" = "Հավելվածի հասցեն չանցավ ստուգումը. համոզվե՛ք որ այն ճիշտ է գրված:";
"app_err_ava" = "Չհաջողվե՛ց վերբեռնել ավատարը:";
"app_err_ava_desc" = "Ավատարը չափազանց մեծ և ծուռ է. ընդհանուր բնույթի սխալ №$res.";
"app_err_note" = "Չհաջողվե՛ց ամրացնել նորությունների նիշքը";
"app_err_note_desc" = "Համոզվե՛ք որ հղումը ճիշտ է և պատկանում է Ձեզ։";
/* Support */ /* Support */
"support_opened" = "Բաց"; "support_opened" = "Բաց";
@ -767,7 +885,12 @@
"banned_alt" = "Օգտատերը արգելափակված է"; "banned_alt" = "Օգտատերը արգելափակված է";
"banned_1" = "Կներե՛ք, <b>$1</b>, բայց Դուք կասեցված եք։"; "banned_1" = "Կներե՛ք, <b>$1</b>, բայց Դուք կասեցված եք։";
"banned_2" = "Պատճառը հետևյալն է․ <b>$1</b>. Ափսոս, բայց մենք ստիպված Ձեզ հավերժ ենք կասեցրել;"; "banned_2" = "Պատճառը հետևյալն է․ <b>$1</b>. Ափսոս, բայց մենք ստիպված Ձեզ հավերժ ենք կասեցրել;";
"banned_perm" = "Ցավոք, մենք ստիպված արգելափակել ենք Ձեզ ընդմիշտ։";
"banned_until_time" = "Այս անգամ մենք ստիպված արգելափակել ենք Ձեզ մինչև <b>$1</b>";
"banned_3" = "Դուք դեռ կարող եք <a href=\"/support?act=new\">գրել նամակ աջակցության ծառայությանը</a>, եթե համարում եք որ դա սխալմունք է, կամ էլ կարող եք <a href=\"/logout?hash=$1\">դուրս գալ</a>։"; "banned_3" = "Դուք դեռ կարող եք <a href=\"/support?act=new\">գրել նամակ աջակցության ծառայությանը</a>, եթե համարում եք որ դա սխալմունք է, կամ էլ կարող եք <a href=\"/logout?hash=$1\">դուրս գալ</a>։";
"banned_unban_myself" = "Ապասառեցնել էջը";
"banned_unban_title" = "Ձեր հաշիվը ապասառեցված է։";
"banned_unban_description" = "Աշխատե՛ք այլևս չխախտել կանոնները։";
/* Registration confirm */ /* Registration confirm */
@ -996,6 +1119,17 @@
"admin_commerce_disabled" = "Կոմմերցիան անջատված է համակարգային ադմինիստրատորի կողմից"; "admin_commerce_disabled" = "Կոմմերցիան անջատված է համակարգային ադմինիստրատորի կողմից";
"admin_commerce_disabled_desc" = "Վաուչերների և նվերների կարգավորումները կպահպանվեն, բայց ոչ մի ազդեցություն չեն ունենա։"; "admin_commerce_disabled_desc" = "Վաուչերների և նվերների կարգավորումները կպահպանվեն, բայց ոչ մի ազդեցություն չեն ունենա։";
"admin_banned_links" = "Արգելափակված հղումներ";
"admin_banned_link" = "Հղում";
"admin_banned_domain" = "Դոմեն";
"admin_banned_link_description" = "Պրոտոկոլով (https://example.com/)";
"admin_banned_link_regexp" = "Ռեգուլյար արտահայտություն";
"admin_banned_link_regexp_description" = "Տեղադրվում է վերոնշյալ դոմենից հետո։ Մի լրացրե՛ք, եթե ցանկանում եք արգելափակել ամբողջ դոմենը";
"admin_banned_link_reason" = "Պատճառ";
"admin_banned_link_initiator" = "Նախաձեռնող";
"admin_banned_link_not_specified" = "Հղումը նշված չէ";
"admin_banned_link_not_found" = "Հղումը չի գտնվել";
/* Paginator (deprecated) */ /* Paginator (deprecated) */
"paginator_back" = "Հետ"; "paginator_back" = "Հետ";
@ -1059,3 +1193,21 @@
"cookies_popup_content" = "Cookie բառը անգլերենից նշանակում է թխվածքաբլիթ, իսկ թխվածքաբլիթը համեղ է։ Մեր կայքը չի ուտում թխվածք, բայց օգտագործում է այն ուղղակի սեսսիան կողմնորոշելու համար։ Ավելի մանրամասն կարող եք ծանոթանալ մեր <a href='/privacy'>գաղտնիության քաղաքականությանը</a> հավելյալ ինֆորմացիայի համար։"; "cookies_popup_content" = "Cookie բառը անգլերենից նշանակում է թխվածքաբլիթ, իսկ թխվածքաբլիթը համեղ է։ Մեր կայքը չի ուտում թխվածք, բայց օգտագործում է այն ուղղակի սեսսիան կողմնորոշելու համար։ Ավելի մանրամասն կարող եք ծանոթանալ մեր <a href='/privacy'>գաղտնիության քաղաքականությանը</a> հավելյալ ինֆորմացիայի համար։";
"cookies_popup_agree" = "Համաձայն եմ"; "cookies_popup_agree" = "Համաձայն եմ";
/* Away */
"url_is_banned" = "Անցումն անհնար է";
"url_is_banned_comment" = "Ադմինիստրացիան <b>$1</b> խորհուրդ չի տալից անցնել այս հղումով։";
"url_is_banned_comment_r" = "Ադմինիստրացիան <b>$1</b> խորհուրդ չի տալից անցնել այս հղումով։<br><br>Պատճառը: <b>$2</b>";
"url_is_banned_default_reason" = "Հղումը դեպի կայք կարող է ստեղծված լինել շորթողներից ՝ օգտատերերին խաբելու և խարդախության նպատակներով, շահույթ ստանալու համար։";
"url_is_banned_title" = "Հղում դեպի կասկածելի կայք";
"url_is_banned_proceed" = "Անցնել հղումով";
/* Maintenance */
"global_maintenance" = "Տեխնիկական աշխատանքներ";
"section_maintenance" = "Բաժինը անհասանելի է";
"undergoing_global_maintenance" = "Ցավոք սրտի, հիմա հոսքը փակված է տեխնիկական աշխատանքներ անցկացնելու համար։ Մենք արդեն աշխատում ենք խնդիրները շտկելու ուղղությամբ։ Խնդրում ենք այցելել մի քիչ ուշ։";
"undergoing_section_maintenance" = "Ցավոք սրտի, <b>$1</b> բաժինը ժամանակավորապես անհասանելի է։ Մենք արդեն աշխատում ենք խնդիրները շտկելու ուղղությամբ։ Խնդրում ենք այցելել մի քիչ ուշ։";
"topics" = "Թեմաներ";

View file

@ -16,6 +16,9 @@
"registration" = "Registration"; "registration" = "Registration";
"forgot_password" = "Forgot your password?"; "forgot_password" = "Forgot your password?";
"checkbox_in_registration" = "I agree to the <a href='/privacy'>privacy policy</a> and <a href='/about'>site policies</a>";
"checkbox_in_registration_unchecked" = "You must agree to the privacy policy and rules in order to register.";
"login_failed" = "Login failed"; "login_failed" = "Login failed";
"invalid_username_or_password" = "The username or password you entered is incorrect. <a href='/restore'>Forgot your password?</a>"; "invalid_username_or_password" = "The username or password you entered is incorrect. <a href='/restore'>Forgot your password?</a>";
@ -158,6 +161,9 @@
"post_deact_f" = "deleted her profile saying:"; "post_deact_f" = "deleted her profile saying:";
"post_deact_silent_m" = "silently deleted his profile."; "post_deact_silent_m" = "silently deleted his profile.";
"post_deact_silent_f" = "silently deleted her profile."; "post_deact_silent_f" = "silently deleted her profile.";
"post_on_your_wall" = "on your wall";
"post_on_group_wall" = "in $1";
"post_on_user_wall" = "on $1's wall";
"wall" = "Wall"; "wall" = "Wall";
"post" = "Post"; "post" = "Post";
"write" = "Write"; "write" = "Write";
@ -874,6 +880,38 @@
"messages_error_1" = "Message not delivered"; "messages_error_1" = "Message not delivered";
"messages_error_1_description" = "There was a general error in sending this message..."; "messages_error_1_description" = "There was a general error in sending this message...";
/* Polls */
"poll" = "Poll";
"create_poll" = "Create poll";
"poll_title" = "Ask a question";
"poll_add_option" = "Add an option...";
"poll_anonymous" = "Anonymous votes";
"poll_multiple" = "Multiple answers";
"poll_locked" = "Quiz mode (no retraction)";
"poll_edit_expires" = "Expires in: ";
"poll_edit_expires_days" = "days";
"poll_editor_tips" = "Pressing backspace in empty option will remove it. Use Tab/Enter (in last option) to create new options faster.";
"poll_embed" = "Embed code";
"poll_voter_count_zero" = "Be <b>the first one</b> to vote!";
"poll_voter_count_one" = "<b>Only one</b> user voted.";
"poll_voter_count_few" = "<b>$1</b> users voted.";
"poll_voter_count_many" = "<b>$1</b> users voted.";
"poll_voter_count_other" = "<b>$1</b> users voted.";
"poll_voters_list" = "Voters";
"poll_anon" = "Anonymous";
"poll_public" = "Public";
"poll_multi" = "multiple choice";
"poll_lock" = "can't revoke";
"poll_until" = "until $1";
"poll_err_to_much_options" = "Too much options supplied.";
"poll_err_anonymous" = "Can't access voters list: poll is anonymous.";
"cast_vote" = "Vote!";
"retract_vote" = "Cancel my vote";
/* Discussions */ /* Discussions */
"discussions" = "Discussions"; "discussions" = "Discussions";

View file

@ -13,7 +13,7 @@ list:
flag: "ua" flag: "ua"
name: "Ukrainian" name: "Ukrainian"
native_name: "Україньска" native_name: "Україньска"
author: "Andrej Lenťaj, Maxim Hrabovi (dechioyo) and Kirill (mbsoft)" author: "Yaroslav Bjelograd, Andrej Lenťaj, Maxim Hrabovi (dechioyo) and Kirill (mbsoft)"
- code: "by" - code: "by"
flag: "by" flag: "by"
name: "Belarussian" name: "Belarussian"

1
locales/qqx.strings Normal file
View file

@ -0,0 +1 @@
/* Used for viewing message keys */

View file

@ -166,6 +166,9 @@
"post_deact_f" = "удалила страницу со словами:"; "post_deact_f" = "удалила страницу со словами:";
"post_deact_silent_m" = "молча удалил свою страницу."; "post_deact_silent_m" = "молча удалил свою страницу.";
"post_deact_silent_f" = "молча удалила свою страницу."; "post_deact_silent_f" = "молча удалила свою страницу.";
"post_on_your_wall" = "на вашей стене";
"post_on_group_wall" = "в $1";
"post_on_user_wall" = "на стене $1";
"wall" = "Стена"; "wall" = "Стена";
"post" = "Запись"; "post" = "Запись";
"write" = "Написать"; "write" = "Написать";
@ -921,6 +924,38 @@
"messages_error_1" = "Сообщение не доставлено"; "messages_error_1" = "Сообщение не доставлено";
"messages_error_1_description" = "При отправке этого сообщения произошла ошибка общего характера..."; "messages_error_1_description" = "При отправке этого сообщения произошла ошибка общего характера...";
/* Polls */
"poll" = "Опрос";
"create_poll" = "Новый опрос";
"poll_title" = "Тема опроса";
"poll_add_option" = "Добавить вариант ответа";
"poll_anonymous" = "Анонимный опрос";
"poll_multiple" = "Множественный выбор";
"poll_locked" = "Запретить отменять свой голос";
"poll_edit_expires" = "Голосование истекает через: ";
"poll_edit_expires_days" = "дней";
"poll_editor_tips" = "Нажатие Backspace в пустом варианте приводит к его удалению. Tab/Enter в последнем добавляет новый.";
"poll_embed" = "Получить код";
"poll_voter_count_zero" = "Будьте <b>первым</b>, кто проголосует!";
"poll_voter_count_one" = "В опросе проголосовал <b>один</b> человек.";
"poll_voter_count_few" = "В опросе проголосовало <b>$1</b> человека.";
"poll_voter_count_many" = "В опросе проголосовало <b>$1</b> человек.";
"poll_voter_count_other" = "В опросе проголосовало <b>$1</b> человек.";
"poll_voters_list" = "Список проголосовавших";
"poll_anon" = "Анонимное голосование";
"poll_public" = "Публичное голосование";
"poll_multi" = "много вариантов";
"poll_lock" = "нельзя переголосовать";
"poll_until" = "до $1";
"poll_err_to_much_options" = "Слишком много вариантов в опросе.";
"poll_err_anonymous" = "Невозможно просмотреть список проголосовавших в анонимном голосовании.";
"cast_vote" = "Проголосовать";
"retract_vote" = "Отменить голос";
/* Discussions */ /* Discussions */
"discussions" = "Обсуждения"; "discussions" = "Обсуждения";
@ -996,7 +1031,7 @@
"changes_saved_comment" = "Новые данные появятся на вашей странице"; "changes_saved_comment" = "Новые данные появятся на вашей странице";
"photo_saved" = "Фотография сохранена"; "photo_saved" = "Фотография сохранена";
"photo_saved_comment" = "Новое изображние профиля появится у вас на странице"; "photo_saved_comment" = "Новое изображение профиля появится у вас на странице";
"shared_succ" = "Запись появится на вашей стене. Нажмите на уведомление, чтобы перейти к своей стене."; "shared_succ" = "Запись появится на вашей стене. Нажмите на уведомление, чтобы перейти к своей стене.";

View file

@ -15,6 +15,8 @@
"password" = "Пароль"; "password" = "Пароль";
"registration" = "Реєстрація"; "registration" = "Реєстрація";
"forgot_password" = "Забули пароль?"; "forgot_password" = "Забули пароль?";
"checkbox_in_registration" = "Я згоден з <a href='/privacy'>політикою конфіденційності</a> і <a href='/about'>правилами сайту</a>";
"checkbox_in_registration_unchecked" = "Ви повинні погодитися з політикою конфіденційності та правилами, щоб зареєструватися.";
"login_failed" = "Не вдалося увійти"; "login_failed" = "Не вдалося увійти";
"invalid_username_or_password" = "Неправильне ім'я користувача або пароль. <a href='/restore'>Забули пароль?</a>"; "invalid_username_or_password" = "Неправильне ім'я користувача або пароль. <a href='/restore'>Забули пароль?</a>";
@ -160,6 +162,9 @@
"post_deact_f" = "видалила сторінку зі словами:"; "post_deact_f" = "видалила сторінку зі словами:";
"post_deact_silent_m" = "мовчки видалив свою сторінку."; "post_deact_silent_m" = "мовчки видалив свою сторінку.";
"post_deact_silent_f" = "мовчки видалила свою сторінку."; "post_deact_silent_f" = "мовчки видалила свою сторінку.";
"post_on_your_wall" = "на вашій стіні";
"post_on_group_wall" = "в $1";
"post_on_user_wall" = "на стіні $1";
"wall" = "Стіна"; "wall" = "Стіна";
"post" = "Запис"; "post" = "Запис";
"write" = "Написати"; "write" = "Написати";
@ -210,6 +215,7 @@
/* Friends */ /* Friends */
"friends" = "Друзі";
"followers" = "Підписники"; "followers" = "Підписники";
"follower" = "Підписник"; "follower" = "Підписник";
"friends_add" = "Додати в друзі"; "friends_add" = "Додати в друзі";
@ -218,10 +224,11 @@
"friends_accept" = "Прийняти заявку"; "friends_accept" = "Прийняти заявку";
"send_message" = "Відправити повідомлення"; "send_message" = "Відправити повідомлення";
"incoming_req" = "Підписники"; "incoming_req" = "Підписники";
"outcoming_req" = "Вихідні";
"req" = "Заявки";
"outcoming_req" = "Заявки"; "outcoming_req" = "Заявки";
"friends_online" = "Друзі онлайн"; "friends_online" = "Друзі онлайн";
"all_friends" = "Усі друзі"; "all_friends" = "Усі друзі";
"req" = "Заявки";
"req_zero" = "Не знайдено жодної заявки..."; "req_zero" = "Не знайдено жодної заявки...";
"req_one" = "Знайдена $1 заявка"; "req_one" = "Знайдена $1 заявка";
@ -235,18 +242,18 @@
"friends_many" = "$1 друзів"; "friends_many" = "$1 друзів";
"friends_other" = "$1 друзів"; "friends_other" = "$1 друзів";
"friends_list_zero" = "У вас поки немає друзів";
"friends_list_one" = "У Вас $1 друг";
"friends_list_few" = "У Вас $1 друг";
"friends_many" = "$1 друзів";
"friends_other" = "$1 друзів";
"friends_online_zero" = "Жодного друга онлайн"; "friends_online_zero" = "Жодного друга онлайн";
"friends_online_one" = "$1 друг онлайн"; "friends_online_one" = "$1 друг онлайн";
"friends_online_few" = "$1 друга онлайн"; "friends_online_few" = "$1 друга онлайн";
"friends_online_many" = "$1 друзів онлайн"; "friends_online_many" = "$1 друзів онлайн";
"friends_online_other" = "$1 друзів онлайн"; "friends_online_other" = "$1 друзів онлайн";
"friends_list_zero" = "У вас поки немає друзів";
"friends_list_one" = "У Вас $1 друг";
"friends_list_few" = "У Вас $1 друг";
"friends_many" = "$1 друзів";
"friends_other" = "$1 друзів";
"followers_zero" = "Жодного підписника"; "followers_zero" = "Жодного підписника";
"followers_one" = "$1 підписник"; "followers_one" = "$1 підписник";
"followers_few" = "$1 підписника"; "followers_few" = "$1 підписника";
@ -1122,6 +1129,17 @@
"admin_commerce_disabled" = "Комерція відключена системним адміністратором"; "admin_commerce_disabled" = "Комерція відключена системним адміністратором";
"admin_commerce_disabled_desc" = "Налаштування ваучерів та подарунків будуть збережені, але не матимуть впливу."; "admin_commerce_disabled_desc" = "Налаштування ваучерів та подарунків будуть збережені, але не матимуть впливу.";
"admin_banned_links" = "Заблоковані посилання";
"admin_banned_link" = "Посилання";
"admin_banned_domain" = "Домен";
"admin_banned_link_description" = "З протоколом (https://example.org/)";
"admin_banned_link_regexp" = "Регулярний вираз";
"admin_banned_link_regexp_description" = "Підставляється після домену, зазначеного вище. Не заповнюйте, якщо хочете заблокувати весь домен";
"admin_banned_link_reason" = "Причина";
"admin_banned_link_initiator" = "Ініціатор";
"admin_banned_link_not_specified" = "Посилання не зазначено";
"admin_banned_link_not_found" = "Посилання не знайдено";
/* Paginator (deprecated) */ /* Paginator (deprecated) */
"paginator_back" = "Назад"; "paginator_back" = "Назад";
@ -1185,3 +1203,53 @@
"cookies_popup_content" = "Цей веб-сайт використовує cookies для того, щоб ідентифікувати вашу сесію і нічого більше. Ознайомтеся з нашою <a href='/privacy'>політикою конфіденційності</a> для отримання додаткової інформації."; "cookies_popup_content" = "Цей веб-сайт використовує cookies для того, щоб ідентифікувати вашу сесію і нічого більше. Ознайомтеся з нашою <a href='/privacy'>політикою конфіденційності</a> для отримання додаткової інформації.";
"cookies_popup_agree" = "Згоден"; "cookies_popup_agree" = "Згоден";
/* Away */
"url_is_banned" = "Перехід неможливий";
"url_is_banned_comment" = "Адміністрація <b>$1</b> не рекомендує переходити за цим посиланням.";
"url_is_banned_comment_r" = "Адміністрація <b>$1</b> не рекомендує переходити за цим посиланням.<br><br>Підстава: <b>$2</b>";
"url_is_banned_default_reason" = "Посилання, за яким Ви спробували перейти, може вести на сайт, що був створений з метою обману користувачів і отримання шляхом цього неправомірного прибутку.";
"url_is_banned_title" = "Посилання на підозрілий сайт";
"url_is_banned_proceed" = "Перейти за посиланням";
/* Maintenance */
"global_maintenance" = "Технічні роботи";
"section_maintenance" = "Розділ недоступний";
"undergoing_global_maintenance" = "На жаль, зараз інстанція закрита на технічні роботи. Ми вже працюємо над усуненням неполадок. Будь ласка, спробуйте зайти пізніше.";
"undergoing_section_maintenance" = "На жаль, розділ <b>$1</b> тимчасово недоступний. Ми вже працюємо над усуненням неполадок. Будь ласка, спробуйте зайти пізніше.";
"topics" = "Теми";
/* Polls */
"poll" = "Опитування";
"create_poll" = "Нове опитування";
"poll_title" = "Тема опитування";
"poll_add_option" = "Додати новий варіант відповіді";
"poll_anonymous" = "Анонiмне опитування";
"poll_multiple" = "Множинний вибір";
"poll_locked" = "Заборонити відміняти свій голос";
"poll_edit_expires" = "Опитування закінчується через: ";
"poll_edit_expires_days" = "днів";
"poll_editor_tips" = "Натискання Backspace у пустому варианті приведе до його видалення. Tab/Enter у останньому додає новий.";
"poll_embed" = "Отримати код";
"poll_voter_count_zero" = "Станьте <b>першим</b>, хто проголосує!";
"poll_voter_count_one" = "У опитуванні проголосовала <b>одна</b> людина.";
"poll_voter_count_few" = "У опитуванні проголосувало <b>$1</b> людини.";
"poll_voter_count_many" = "У опитуванні проголосувало <b>$1</b> людей.";
"poll_voter_count_other" = "У опитуванні проголосувало <b>$1</b> людей.";
"poll_voters_list" = "Список проголосувавших";
"poll_anon" = "Анонімне опитування";
"poll_public" = "Публічне опитування";
"poll_multi" = "багато вариантів";
"poll_lock" = "не можна переголосувати";
"poll_until" = "до $1";
"poll_err_to_much_options" = "Занадто багато варіантів в опитуванні.";
"poll_err_anonymous" = "Неможливо переглянути список тих, хто проголосував в анонімному голосуванні.";
"cast_vote" = "Проголосувати";
"retract_vote" = "Відмінити голос";

View file

@ -58,6 +58,20 @@ openvk:
susLinks: susLinks:
warnings: true warnings: true
showReason: true showReason: true
maintenanceMode:
all: false
photos: false
videos: false
messenger: false
user: false
group: false
comment: false
gifts: false
apps: false
notes: false
notification: false
support: false
topics: false
ton: ton:
enabled: false enabled: false
address: "🅿" address: "🅿"

View file

@ -44,3 +44,5 @@ comments.allow-graffiti: 0
# + Set this option to -1 if you want to disable the limit # + Set this option to -1 if you want to disable the limit
# + Set this option to any non-negative number to be this limit # + Set this option to any non-negative number to be this limit
wall.repost-liking-recursion-limit: 10 wall.repost-liking-recursion-limit: 10
polls.max-opts: 10

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -0,0 +1,7 @@
.page_header {
background-image: url('/themepack/midnight/0.0.1.0/resource/xheader.png');
}
.page_custom_header {
background-image: url('/themepack/midnight/0.0.1.0/resource/xheader_custom.png');
}

View file

@ -0,0 +1,214 @@
* {
scrollbar-color: #3f365b #1d192b;
}
html {
color-scheme: dark !important;
}
body {
background-color: #0e0b1a;
color: #c6d2e8;
}
span, .post-author .date, .crp-entry--message---text, .messenger-app--messages---message .time, .navigation-lang .link_new {
color: #8b9ab5 !important;
}
.nobold, nobold {
color: #6f82a8;
}
.page_status {
color: #c6d2e8;
}
.profileName h2 {
color: #8eb2d0;
}
.wrap1, .wrap2, .page-wrap, #wrapH, #wrapHI {
border-color: #1c202f;
}
.accountInfo, .left_small_block, #profile_link, .profile_link, .navigation .link, .navigation .link:hover, .navigation_footer .link, .navigation_footer .link:hover, .completeness-gauge, input[type="text"], input[type="password"], input[type~="text"], input[type~="password"], input[type="email"], input[type="phone"], input[type~="email"], input[type~="phone"], input[type="search"], input[type~="search"], input[type~="date"], select, .content_title_expanded, .content_title_unexpanded, .content_subtitle, textarea, .post-content, .post-author, hr, h4, .postFeedWrapper, .tabs, #wallAttachmentMenu, .ovk-diag, .ovk-diag-head, #ovkDraw, #ovkDraw .literally .lc-picker, .literally .lc-options.horz-toolbar, .page_wrap, .container_gray .content, .summaryBar, .groups_options, form[action="/search"] > input, .header_search_input, .header_search_inputbt, .accent-box, .page_status_popup, .messenger-app--input, .messenger-app, .crp-entry:first-of-type, .crp-list, .crp-entry, .note_footer, .page_content > div, #editor, .note_header, center[style="background: white;border: #DEDEDE solid 1px;"], .album-photo img, .mb_tabs, .mb_tab#active div, .navigation-lang .link_new, #faqhead, #faqcontent, .post-divider, .comment, .commentsTextFieldWrap, tr, td, th, #votesBalance, .paginator a.active, .paginator a:hover, .topic-list-item, #userContent blockquote, .tippy-box[data-theme~="vk"], .poll {
border-color: #2c2640 !important;
}
.tippy-box[data-theme~="vk"][data-placement^='top'] > .tippy-arrow::before, .tippy-box[data-theme~="vk"][data-placement^='bottom'] > .tippy-arrow::before {
border-top-color: #1e1a2b;
border-bottom-color: #1e1a2b;
}
hr {
background-color: #2c2640 !important;
}
.cookies-popup {
background: linear-gradient(#1e1a2b, #231e33);
box-shadow: unset;
}
.button, #activetabs, .messagebox-content-header, .accent-box, .button_search {
background-color: #383052;
}
.tab:hover {
background-color: #40375e;
}
.menu_divider, .ovk-diag-action, .minilink .counter {
background-color: #2c2640;
}
#ovkDraw .literally .lc-picker, .literally .lc-options.horz-toolbar, .mb_tab#active {
background-color: #453e5e;
}
.ovk-diag-cont {
background-color: #272e4894;
}
a, .page_footer .link, #profile_link, .profile_link {
color: #8fb9d8;
}
.page_footer .link:hover, .navigation .link:hover, .navigation .edit-button:hover, #profile_link:hover, .profile_link:hover, #wallAttachmentMenu > a:hover, .crp-entry:hover, .navigation-lang .link_new:hover, .paginator a:hover, .post-share-button:hover, .post-like-button:hover {
background-color: #272138 !important;
}
.header_navigation .link a {
color: #bcc3d0;
}
.header_navigation .link a:hover, .home_button_custom {
color: #c7cdd9;
}
.navigation .link {
color: #d9e0ee;
}
.navigation .edit-button {
background-color: #0e0b1a !important;
color: #7b94c4 !important;
}
#test-label, .msg.msg_err {
background-color: #30161d;
}
.msg.msg_succ {
background-color: #163f13;
}
h4, .content_title_expanded, .summaryBar .summary, .content_title_unexpanded {
color: #7c94c5;
}
.notes_titles small, .post-upload, .post-has-poll {
color: #6f82a8;
}
.content_title_expanded, .content_title_unexpanded, .ovk-diag, .settings_delete, center[style="background: white;border: #DEDEDE solid 1px;"], .album-photo img, #faqhead, td.e, tr.e {
background-color: #231e33 !important;
}
.content_subtitle, .postFeedWrapper, .ovk-diag-head, .container_gray, .page_status_popup, .messenger-app--input, .note_header, #faqcontent, .commentsTextFieldWrap, td.v, tr.v, #votesBalance, .expand_button, #userContent blockquote, .tippy-box[data-theme~="vk"] {
background-color: #1e1a2b !important;
}
.post-author {
background-color: #1e1a2b;
/* this is fix to correct the unexpected behavior of the microblog style lol */
}
.content_title_expanded {
background-image: url("/themepack/midnight/0.0.1.4/resource/flex_arrow_open.png");
}
.content_title_unexpanded {
background-image: url("/themepack/midnight/0.0.1.4/resource/flex_arrow_shut.gif");
}
.ovk-video > .preview, .video-preview {
box-shadow: inset 0 0 0 1px #231e33, inset 0 0 0 2px #1e1a2b;
}
#wallAttachmentMenu, .container_gray .content, .mb_tabs {
background-color: #120e1f;
}
#wallAttachmentMenu > .header, .messenger-app--messages---message.unread, tr.h {
background-color: #1d192b;
}
.toTop {
background-color: #272138;
color: #c6d2e8;
}
.page_yellowheader {
color: #c6d2e8;
background-image: url("/themepack/midnight/0.0.1.4/resource/header_purple.png");
background-color: #231f34;
border-color: #231f34;
}
.page_header {
background-image: url("/themepack/midnight/0.0.1.4/resource/header.png");
}
.page_custom_header {
background-image: url("/themepack/midnight/0.0.1.4/resource/header_custom.png");
}
.page_yellowheader span, .page_yellowheader a {
color: #a48aff !important;
}
.completeness-gauge, .poll-result-bar {
background-color: #231e33;
}
.completeness-gauge > div, .poll-result-bar-sub {
background-color: #2c2640;
}
form[action="/search"] > input, .header_search_input, textarea, input[type="text"] {
background-color: #181826 !important;
}
input[type="checkbox"] {
background-image: url("/themepack/midnight/0.0.1.4/resource/checkbox.png");
}
input[type="radio"] {
background-image: url("/themepack/midnight/0.0.1.4/resource/radio.png");
}
.header_navigation .link {
background: unset;
}
.heart {
background-image: url("/themepack/midnight/0.0.1.4/resource/like.gif") !important;
}
.pinned-mark, .post-author .pin {
background-image: url("/themepack/midnight/0.0.1.4/resource/pin.png") !important;
}
.repost-icon {
background-image: url("/themepack/midnight/0.0.1.4/resource/published.gif") !important;
}
.post-author .delete {
background-image: url("/themepack/midnight/0.0.1.4/resource/input_clear.gif") !important;
}
.user-alert {
background-color: #41311a;
color: #d5b88c;
border-color: #514534;
}

View file

@ -0,0 +1,12 @@
id: midnight
version: "0.0.1.4"
openvk_version: 0
enabled: 1
metadata:
name:
_: "OpenVK Midnight"
en: "OpenVK Midnight"
ru: "OpenVK Midnight"
uk: "OpenVK Midnight"
author: "Ilya Prokopenko"
description: "A dark theme for OpenVK"

View file

@ -7,5 +7,6 @@ metadata:
_: "OpenVK Modern" _: "OpenVK Modern"
en: "OpenVK Modern" en: "OpenVK Modern"
ru: "OpenVK Modern" ru: "OpenVK Modern"
uk: "OpenVK Modern"
author: "Mikita Wiśniewski (rudzik8)" author: "Mikita Wiśniewski (rudzik8)"
description: "OpenVK theme in modern style" description: "OpenVK theme in modern style"

View file

@ -1,10 +1,10 @@
# OpenVK Themepacks # OpenVK Themepacks
This folder contains all themes that can be used by any user on instance. This folder contains all themes that can be used by any user on an instance.
## How do i create the theme? ## How do I create a theme?
Create a directory, the name of which should contain only Latin letters and numbers, and create a file there `theme.yml`, and fill it with the following content: Create a directory, the name of which should contain only Latin letters and numbers, then create a file in this directory called `theme.yml`, and fill it with the following content:
```yaml ```yaml
id: vk2007 id: vk2007
@ -24,13 +24,13 @@ metadata:
`id` is the name of the folder `id` is the name of the folder
`version` - version of the theme `version` is the version of the theme
`openvk_version` - version OpenVK *(it is necessary to leave the value 0)* `openvk_version` is the version of OpenVK *(it is necessary to leave the value to 0)*
`metadata`: `metadata`:
* `name` - the name of the theme for the end user. Inside it you can leave names for different languages. `_` (underscore) - for all languages. * `name` - the name of the theme for the end user. Inside it you can leave names for different languages. `_` (underscore) is for all languages.
Next, in `stylesheet.css` you can insert any CSS code, with which you can change the elements of the site. If you need additional pictures or resources, just create a `res` folder, and access the resources via the `/themepack/{directory name}/{theme version}/resource/{resource}` path. Next, in `stylesheet.css` you can insert any CSS code, with which you can change the elements of the site. If you need additional pictures or resources, just create a `res` folder, and access the resources via the `/themepack/{directory name}/{theme version}/resource/{resource}` path.

View file

@ -34,7 +34,7 @@ metadata:
Далее, в `stylesheet.css` вставляем любой CSS код, с помощью которого вы можете изменить элементы сайта. Если вам нужны дополнительные картинки или ресурсы, то для этого просто создайте папку `res`, и в CSS коде обращайтесь к ресурсам через путь `/themepack/{название директории}/{версия темы}/resource/{ресурс}`. Далее, в `stylesheet.css` вставляем любой CSS код, с помощью которого вы можете изменить элементы сайта. Если вам нужны дополнительные картинки или ресурсы, то для этого просто создайте папку `res`, и в CSS коде обращайтесь к ресурсам через путь `/themepack/{название директории}/{версия темы}/resource/{ресурс}`.
Для поддержки новогоднего насторения, которое включается автоматически с 1 декабря по 15 января, создайте файл `xmas.css` в папку `res`, и внесите вам нужные изменения. Для поддержки новогоднего настроения, которое включается автоматически с 1 декабря по 15 января, создайте файл `xmas.css` в папку `res`, и внесите вам нужные изменения.
**В конце концов, иерархия директории с темой должна выглядеть вот так:** **В конце концов, иерархия директории с темой должна выглядеть вот так:**