mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +02:00
Improve channel gifts viewing.
This commit is contained in:
parent
27bba8250a
commit
80db076f38
9 changed files with 156 additions and 86 deletions
|
@ -3298,6 +3298,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
"lng_gift_visible_hint" = "This gift is visible on your page.";
|
"lng_gift_visible_hint" = "This gift is visible on your page.";
|
||||||
"lng_gift_hidden_hint_channel" = "This gift is hidden from visitors of your channel.";
|
"lng_gift_hidden_hint_channel" = "This gift is hidden from visitors of your channel.";
|
||||||
"lng_gift_visible_hint_channel" = "This gift is visible in your channel's Gifts.";
|
"lng_gift_visible_hint_channel" = "This gift is visible in your channel's Gifts.";
|
||||||
|
"lng_gift_in_blockchain" = "This gift is in TON blockchain. {link}";
|
||||||
|
"lng_gift_in_blockchain_link" = "View >";
|
||||||
"lng_gift_visible_hide" = "Hide >";
|
"lng_gift_visible_hide" = "Hide >";
|
||||||
"lng_gift_show_on_page" = "Display on my Page";
|
"lng_gift_show_on_page" = "Display on my Page";
|
||||||
"lng_gift_show_on_channel" = "Display in channel's Gifts";
|
"lng_gift_show_on_channel" = "Display in channel's Gifts";
|
||||||
|
@ -3309,6 +3311,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
"lng_gift_channel_title" = "Send a Gift";
|
"lng_gift_channel_title" = "Send a Gift";
|
||||||
"lng_gift_channel_about" = "Select a gift to show appreciation for {name}.";
|
"lng_gift_channel_about" = "Select a gift to show appreciation for {name}.";
|
||||||
"lng_gift_unique_owner" = "Owner";
|
"lng_gift_unique_owner" = "Owner";
|
||||||
|
"lng_gift_unique_address_copied" = "Address copied to clipboard.";
|
||||||
"lng_gift_unique_status" = "Status";
|
"lng_gift_unique_status" = "Status";
|
||||||
"lng_gift_unique_status_non" = "Non-Unique";
|
"lng_gift_unique_status_non" = "Non-Unique";
|
||||||
"lng_gift_unique_status_upgrade" = "upgrade";
|
"lng_gift_unique_status_upgrade" = "upgrade";
|
||||||
|
|
|
@ -808,6 +808,7 @@ std::optional<Data::StarGift> FromTL(
|
||||||
.id = data.vid().v,
|
.id = data.vid().v,
|
||||||
.slug = qs(data.vslug()),
|
.slug = qs(data.vslug()),
|
||||||
.title = qs(data.vtitle()),
|
.title = qs(data.vtitle()),
|
||||||
|
.ownerAddress = qs(data.vowner_address().value_or_empty()),
|
||||||
.ownerName = qs(data.vowner_name().value_or_empty()),
|
.ownerName = qs(data.vowner_name().value_or_empty()),
|
||||||
.ownerId = (data.vowner_id()
|
.ownerId = (data.vowner_id()
|
||||||
? peerFromMTP(*data.vowner_id())
|
? peerFromMTP(*data.vowner_id())
|
||||||
|
|
|
@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "info/profile/info_profile_badge.h"
|
#include "info/profile/info_profile_badge.h"
|
||||||
#include "info/profile/info_profile_values.h"
|
#include "info/profile/info_profile_values.h"
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
|
#include "main/main_app_config.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "mainwidget.h"
|
#include "mainwidget.h"
|
||||||
#include "payments/payments_checkout_process.h"
|
#include "payments/payments_checkout_process.h"
|
||||||
|
@ -131,6 +132,23 @@ constexpr auto kRarityTooltipDuration = 3 * crl::time(1000);
|
||||||
: tr::lng_premium_gift_duration_years;
|
: tr::lng_premium_gift_duration_years;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeMaybeMultilineTokenValue(
|
||||||
|
not_null<Ui::TableLayout*> table,
|
||||||
|
const QString &token,
|
||||||
|
Settings::CreditsEntryBoxStyleOverrides st) {
|
||||||
|
constexpr auto kOneLineCount = 24;
|
||||||
|
const auto oneLine = token.length() <= kOneLineCount;
|
||||||
|
return object_ptr<Ui::FlatLabel>(
|
||||||
|
table,
|
||||||
|
rpl::single(
|
||||||
|
Ui::Text::Wrapped({ token }, EntityType::Code, {})),
|
||||||
|
(oneLine
|
||||||
|
? table->st().defaultValue
|
||||||
|
: st.tableValueMultiline
|
||||||
|
? *st.tableValueMultiline
|
||||||
|
: st::giveawayGiftCodeValueMultiline));
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakePeerTableValue(
|
[[nodiscard]] object_ptr<Ui::RpWidget> MakePeerTableValue(
|
||||||
not_null<Ui::TableLayout*> table,
|
not_null<Ui::TableLayout*> table,
|
||||||
std::shared_ptr<ChatHelpers::Show> show,
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
@ -1223,6 +1241,15 @@ void ResolveGiveawayInfo(
|
||||||
crl::guard(controller, show));
|
crl::guard(controller, show));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString TonAddressUrl(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const QString &address) {
|
||||||
|
const auto prefix = session->appConfig().get<QString>(
|
||||||
|
u"ton_blockchain_explorer_url"_q,
|
||||||
|
u"https://tonviewer.com/"_q);
|
||||||
|
return prefix + address;
|
||||||
|
}
|
||||||
|
|
||||||
void AddStarGiftTable(
|
void AddStarGiftTable(
|
||||||
std::shared_ptr<ChatHelpers::Show> show,
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
not_null<Ui::VerticalLayout*> container,
|
not_null<Ui::VerticalLayout*> container,
|
||||||
|
@ -1335,10 +1362,26 @@ void AddStarGiftTable(
|
||||||
MakePeerWithStatusValue(table, show, ownerId, handleChange),
|
MakePeerWithStatusValue(table, show, ownerId, handleChange),
|
||||||
st::giveawayGiftCodePeerMargin);
|
st::giveawayGiftCodePeerMargin);
|
||||||
} else if (unique) {
|
} else if (unique) {
|
||||||
AddTableRow(
|
if (!unique->ownerName.isEmpty()) {
|
||||||
table,
|
AddTableRow(
|
||||||
tr::lng_gift_unique_owner(),
|
table,
|
||||||
rpl::single(TextWithEntities{ unique->ownerName }));
|
tr::lng_gift_unique_owner(),
|
||||||
|
rpl::single(TextWithEntities{ unique->ownerName }));
|
||||||
|
} else if (auto address = unique->ownerAddress; !address.isEmpty()) {
|
||||||
|
auto label = MakeMaybeMultilineTokenValue(table, address, st);
|
||||||
|
label->setClickHandlerFilter([=](const auto &...) {
|
||||||
|
TextUtilities::SetClipboardText(
|
||||||
|
TextForMimeData::Simple(address));
|
||||||
|
show->showToast(
|
||||||
|
tr::lng_gift_unique_address_copied(tr::now));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
AddTableRow(
|
||||||
|
table,
|
||||||
|
tr::lng_gift_unique_owner(),
|
||||||
|
std::move(label),
|
||||||
|
st::giveawayGiftCodeValueMargin);
|
||||||
|
}
|
||||||
} else if (giftToChannel) {
|
} else if (giftToChannel) {
|
||||||
AddTableRow(
|
AddTableRow(
|
||||||
table,
|
table,
|
||||||
|
@ -1724,17 +1767,7 @@ void AddCreditsHistoryEntryTable(
|
||||||
Ui::Text::WithEntities));
|
Ui::Text::WithEntities));
|
||||||
}
|
}
|
||||||
if (!entry.id.isEmpty()) {
|
if (!entry.id.isEmpty()) {
|
||||||
constexpr auto kOneLineCount = 24;
|
auto label = MakeMaybeMultilineTokenValue(table, entry.id, st);
|
||||||
const auto oneLine = entry.id.length() <= kOneLineCount;
|
|
||||||
auto label = object_ptr<Ui::FlatLabel>(
|
|
||||||
table,
|
|
||||||
rpl::single(
|
|
||||||
Ui::Text::Wrapped({ entry.id }, EntityType::Code, {})),
|
|
||||||
(oneLine
|
|
||||||
? table->st().defaultValue
|
|
||||||
: st.tableValueMultiline
|
|
||||||
? *st.tableValueMultiline
|
|
||||||
: st::giveawayGiftCodeValueMultiline));
|
|
||||||
label->setClickHandlerFilter([=](const auto &...) {
|
label->setClickHandlerFilter([=](const auto &...) {
|
||||||
TextUtilities::SetClipboardText(
|
TextUtilities::SetClipboardText(
|
||||||
TextForMimeData::Simple(entry.id));
|
TextForMimeData::Simple(entry.id));
|
||||||
|
|
|
@ -25,6 +25,10 @@ struct GiveawayResults;
|
||||||
struct SubscriptionEntry;
|
struct SubscriptionEntry;
|
||||||
} // namespace Data
|
} // namespace Data
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Session;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
namespace Settings {
|
namespace Settings {
|
||||||
struct CreditsEntryBoxStyleOverrides;
|
struct CreditsEntryBoxStyleOverrides;
|
||||||
} // namespace Settings
|
} // namespace Settings
|
||||||
|
@ -62,6 +66,10 @@ void ResolveGiveawayInfo(
|
||||||
std::optional<Data::GiveawayStart> start,
|
std::optional<Data::GiveawayStart> start,
|
||||||
std::optional<Data::GiveawayResults> results);
|
std::optional<Data::GiveawayResults> results);
|
||||||
|
|
||||||
|
[[nodiscard]] QString TonAddressUrl(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const QString &address);
|
||||||
|
|
||||||
void AddStarGiftTable(
|
void AddStarGiftTable(
|
||||||
std::shared_ptr<ChatHelpers::Show> show,
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
not_null<Ui::VerticalLayout*> container,
|
not_null<Ui::VerticalLayout*> container,
|
||||||
|
|
|
@ -40,6 +40,7 @@ struct UniqueGift {
|
||||||
CollectibleId id = 0;
|
CollectibleId id = 0;
|
||||||
QString slug;
|
QString slug;
|
||||||
QString title;
|
QString title;
|
||||||
|
QString ownerAddress;
|
||||||
QString ownerName;
|
QString ownerName;
|
||||||
PeerId ownerId = 0;
|
PeerId ownerId = 0;
|
||||||
int number = 0;
|
int number = 0;
|
||||||
|
|
|
@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "history/view/media/history_view_premium_gift.h"
|
#include "history/view/media/history_view_premium_gift.h"
|
||||||
|
|
||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
|
#include "api/api_credits.h" // InputSavedStarGiftId
|
||||||
#include "api/api_premium.h"
|
#include "api/api_premium.h"
|
||||||
#include "base/unixtime.h"
|
#include "base/unixtime.h"
|
||||||
#include "boxes/gift_premium_box.h" // ResolveGiftCode
|
#include "boxes/gift_premium_box.h" // ResolveGiftCode
|
||||||
|
@ -219,6 +220,9 @@ bool PremiumGift::buttonMinistars() {
|
||||||
}
|
}
|
||||||
|
|
||||||
ClickHandlerPtr PremiumGift::createViewLink() {
|
ClickHandlerPtr PremiumGift::createViewLink() {
|
||||||
|
if (auto link = OpenStarGiftLink(_parent->data())) {
|
||||||
|
return link;
|
||||||
|
}
|
||||||
const auto from = _gift->from();
|
const auto from = _gift->from();
|
||||||
const auto itemId = _parent->data()->fullId();
|
const auto itemId = _parent->data()->fullId();
|
||||||
const auto peer = _parent->history()->peer;
|
const auto peer = _parent->history()->peer;
|
||||||
|
@ -232,16 +236,7 @@ ClickHandlerPtr PremiumGift::createViewLink() {
|
||||||
}
|
}
|
||||||
const auto selfId = controller->session().userPeerId();
|
const auto selfId = controller->session().userPeerId();
|
||||||
const auto sent = (from->id == selfId);
|
const auto sent = (from->id == selfId);
|
||||||
if (starGift()) {
|
if (creditsPrize()) {
|
||||||
const auto item = controller->session().data().message(itemId);
|
|
||||||
if (item) {
|
|
||||||
controller->show(Box(
|
|
||||||
Settings::StarGiftViewBox,
|
|
||||||
controller,
|
|
||||||
data,
|
|
||||||
item));
|
|
||||||
}
|
|
||||||
} else if (creditsPrize()) {
|
|
||||||
controller->show(Box(
|
controller->show(Box(
|
||||||
Settings::CreditsPrizeBox,
|
Settings::CreditsPrizeBox,
|
||||||
controller,
|
controller,
|
||||||
|
@ -265,49 +260,6 @@ ClickHandlerPtr PremiumGift::createViewLink() {
|
||||||
ResolveGiftCode(controller, data.slug, fromId, toId);
|
ResolveGiftCode(controller, data.slug, fromId, toId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (const auto upgradeTo = data.upgradeMsgId) {
|
|
||||||
const auto requesting = std::make_shared<bool>();
|
|
||||||
return std::make_shared<LambdaClickHandler>([=](
|
|
||||||
ClickContext context) {
|
|
||||||
const auto my = context.other.value<ClickHandlerContext>();
|
|
||||||
const auto weak = my.sessionWindow;
|
|
||||||
const auto controller = weak.get();
|
|
||||||
if (!controller || *requesting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
*requesting = true;
|
|
||||||
controller->session().api().request(MTPpayments_GetSavedStarGift(
|
|
||||||
MTP_vector<MTPInputSavedStarGift>(
|
|
||||||
1,
|
|
||||||
MTP_inputSavedStarGiftUser(MTP_int(upgradeTo)))
|
|
||||||
)).done([=](const MTPpayments_SavedStarGifts &result) {
|
|
||||||
*requesting = false;
|
|
||||||
if (const auto window = weak.get()) {
|
|
||||||
const auto &data = result.data();
|
|
||||||
window->session().data().processUsers(data.vusers());
|
|
||||||
window->session().data().processChats(data.vchats());
|
|
||||||
const auto self = window->session().user();
|
|
||||||
const auto &list = data.vgifts().v;
|
|
||||||
if (list.empty()) {
|
|
||||||
showForWeakWindow(weak);
|
|
||||||
} else if (auto parsed = Api::FromTL(self, list[0])) {
|
|
||||||
window->show(Box(
|
|
||||||
Settings::SavedStarGiftBox,
|
|
||||||
window,
|
|
||||||
self,
|
|
||||||
*parsed));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).fail([=](const MTP::Error &error) {
|
|
||||||
*requesting = false;
|
|
||||||
if (const auto window = weak.get()) {
|
|
||||||
window->showToast(error.type());
|
|
||||||
}
|
|
||||||
showForWeakWindow(weak);
|
|
||||||
}).send();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
|
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
|
||||||
showForWeakWindow(
|
showForWeakWindow(
|
||||||
context.other.value<ClickHandlerContext>().sessionWindow);
|
context.other.value<ClickHandlerContext>().sessionWindow);
|
||||||
|
@ -447,4 +399,76 @@ void PremiumGift::ensureStickerCreated() const {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClickHandlerPtr OpenStarGiftLink(not_null<HistoryItem*> item) {
|
||||||
|
const auto media = item->media();
|
||||||
|
const auto gift = media ? media->gift() : nullptr;
|
||||||
|
if (!gift || gift->type != Data::GiftType::StarGift) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const auto data = *gift;
|
||||||
|
const auto itemId = item->fullId();
|
||||||
|
const auto openInsteadId = data.upgradeMsgId
|
||||||
|
? Data::SavedStarGiftId::User(data.upgradeMsgId)
|
||||||
|
: (data.channel && data.channelSavedId)
|
||||||
|
? Data::SavedStarGiftId::Chat(data.channel, data.channelSavedId)
|
||||||
|
: Data::SavedStarGiftId();
|
||||||
|
const auto requesting = std::make_shared<bool>();
|
||||||
|
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
|
||||||
|
const auto my = context.other.value<ClickHandlerContext>();
|
||||||
|
const auto weak = my.sessionWindow;
|
||||||
|
const auto controller = weak.get();
|
||||||
|
if (!controller) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto quick = [=](not_null<Window::SessionController*> window) {
|
||||||
|
const auto item = window->session().data().message(itemId);
|
||||||
|
if (item) {
|
||||||
|
window->show(Box(
|
||||||
|
Settings::StarGiftViewBox,
|
||||||
|
window,
|
||||||
|
data,
|
||||||
|
item));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!openInsteadId) {
|
||||||
|
quick(controller);
|
||||||
|
return;
|
||||||
|
} else if (*requesting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*requesting = true;
|
||||||
|
controller->session().api().request(MTPpayments_GetSavedStarGift(
|
||||||
|
MTP_vector<MTPInputSavedStarGift>(
|
||||||
|
1,
|
||||||
|
Api::InputSavedStarGiftId(openInsteadId))
|
||||||
|
)).done([=](const MTPpayments_SavedStarGifts &result) {
|
||||||
|
*requesting = false;
|
||||||
|
if (const auto window = weak.get()) {
|
||||||
|
const auto &data = result.data();
|
||||||
|
window->session().data().processUsers(data.vusers());
|
||||||
|
window->session().data().processChats(data.vchats());
|
||||||
|
const auto owner = openInsteadId.chat()
|
||||||
|
? openInsteadId.chat()
|
||||||
|
: window->session().user();
|
||||||
|
const auto &list = data.vgifts().v;
|
||||||
|
if (list.empty()) {
|
||||||
|
quick(window);
|
||||||
|
} else if (auto parsed = Api::FromTL(owner, list[0])) {
|
||||||
|
window->show(Box(
|
||||||
|
Settings::SavedStarGiftBox,
|
||||||
|
window,
|
||||||
|
owner,
|
||||||
|
*parsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
*requesting = false;
|
||||||
|
if (const auto window = weak.get()) {
|
||||||
|
window->showToast(error.type());
|
||||||
|
quick(window);
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace HistoryView
|
} // namespace HistoryView
|
||||||
|
|
|
@ -68,4 +68,6 @@ private:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] ClickHandlerPtr OpenStarGiftLink(not_null<HistoryItem*> item);
|
||||||
|
|
||||||
} // namespace HistoryView
|
} // namespace HistoryView
|
||||||
|
|
|
@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
#include "data/data_star_gift.h"
|
#include "data/data_star_gift.h"
|
||||||
#include "history/view/media/history_view_media_generic.h"
|
#include "history/view/media/history_view_media_generic.h"
|
||||||
|
#include "history/view/media/history_view_premium_gift.h"
|
||||||
#include "history/view/history_view_cursor_state.h"
|
#include "history/view/history_view_cursor_state.h"
|
||||||
#include "history/view/history_view_element.h"
|
#include "history/view/history_view_element.h"
|
||||||
#include "history/history.h"
|
#include "history/history.h"
|
||||||
|
@ -457,24 +458,7 @@ auto GenerateUniqueGiftMedia(
|
||||||
gift->backdrop.textColor));
|
gift->backdrop.textColor));
|
||||||
|
|
||||||
const auto itemId = parent->data()->fullId();
|
const auto itemId = parent->data()->fullId();
|
||||||
auto link = std::make_shared<LambdaClickHandler>([=](
|
auto link = OpenStarGiftLink(parent->data());
|
||||||
ClickContext context) {
|
|
||||||
const auto my = context.other.value<ClickHandlerContext>();
|
|
||||||
if (const auto controller = my.sessionWindow.get()) {
|
|
||||||
const auto owner = &controller->session().data();
|
|
||||||
if (const auto item = owner->message(itemId)) {
|
|
||||||
if (const auto media = item->media()) {
|
|
||||||
if (const auto gift = media->gift()) {
|
|
||||||
controller->show(Box(
|
|
||||||
Settings::StarGiftViewBox,
|
|
||||||
controller,
|
|
||||||
*gift,
|
|
||||||
item));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
push(std::make_unique<ButtonPart>(
|
push(std::make_unique<ButtonPart>(
|
||||||
tr::lng_sticker_premium_view(tr::now),
|
tr::lng_sticker_premium_view(tr::now),
|
||||||
st::chatUniqueButtonPadding,
|
st::chatUniqueButtonPadding,
|
||||||
|
|
|
@ -1604,6 +1604,20 @@ void GenericCreditsEntryBox(
|
||||||
toggleVisibility(!e.savedToProfile);
|
toggleVisibility(!e.savedToProfile);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
} else if (uniqueGift && !uniqueGift->ownerAddress.isEmpty()) {
|
||||||
|
const auto label = box->addRow(
|
||||||
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
box,
|
||||||
|
tr::lng_gift_in_blockchain(
|
||||||
|
lt_link,
|
||||||
|
tr::lng_gift_in_blockchain_link() | Ui::Text::ToLink(),
|
||||||
|
Ui::Text::WithEntities),
|
||||||
|
st::creditsBoxAboutDivider));
|
||||||
|
label->setClickHandlerFilter([=](const auto &...) {
|
||||||
|
UrlClickHandler::Open(
|
||||||
|
TonAddressUrl(session, uniqueGift->ownerAddress));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (s) {
|
if (s) {
|
||||||
const auto user = peer ? peer->asUser() : nullptr;
|
const auto user = peer ? peer->asUser() : nullptr;
|
||||||
|
|
Loading…
Add table
Reference in a new issue