mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +02:00
Nice unlock media stars, unlock done tooltip.
This commit is contained in:
parent
479b63c33a
commit
6c1e7357c6
9 changed files with 111 additions and 19 deletions
|
@ -79,10 +79,14 @@ struct PaidMediaData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto sender = item->originalSender();
|
||||||
|
const auto broadcast = (sender && sender->isBroadcast())
|
||||||
|
? sender
|
||||||
|
: message->peer.get();
|
||||||
return {
|
return {
|
||||||
.invoice = invoice,
|
.invoice = invoice,
|
||||||
.item = item,
|
.item = item,
|
||||||
.peer = message->peer,
|
.peer = broadcast,
|
||||||
.photos = photos,
|
.photos = photos,
|
||||||
.videos = videos,
|
.videos = videos,
|
||||||
};
|
};
|
||||||
|
@ -270,21 +274,11 @@ void SendCreditsBox(
|
||||||
loadingAnimation->showOn(state->confirmButtonBusy.value());
|
loadingAnimation->showOn(state->confirmButtonBusy.value());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto emojiMargin = QMargins(
|
|
||||||
0,
|
|
||||||
-st::moderateBoxExpandInnerSkip,
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
const auto buttonEmoji = Ui::Text::SingleCustomEmoji(
|
|
||||||
session->data().customEmojiManager().registerInternalEmoji(
|
|
||||||
st::settingsPremiumIconStar,
|
|
||||||
emojiMargin,
|
|
||||||
true));
|
|
||||||
auto buttonText = tr::lng_credits_box_out_confirm(
|
auto buttonText = tr::lng_credits_box_out_confirm(
|
||||||
lt_count,
|
lt_count,
|
||||||
rpl::single(form->invoice.amount) | tr::to_count(),
|
rpl::single(form->invoice.amount) | tr::to_count(),
|
||||||
lt_emoji,
|
lt_emoji,
|
||||||
rpl::single(buttonEmoji),
|
rpl::single(CreditsEmoji(session)),
|
||||||
Ui::Text::RichLangValue);
|
Ui::Text::RichLangValue);
|
||||||
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
|
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
|
||||||
button,
|
button,
|
||||||
|
@ -359,4 +353,12 @@ void SendCreditsBox(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextWithEntities CreditsEmoji(not_null<Main::Session*> session) {
|
||||||
|
return Ui::Text::SingleCustomEmoji(
|
||||||
|
session->data().customEmojiManager().registerInternalEmoji(
|
||||||
|
st::settingsPremiumIconStar,
|
||||||
|
QMargins{ 0, -st::moderateBoxExpandInnerSkip, 0, 0 },
|
||||||
|
true));
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
|
@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
|
||||||
class HistoryItem;
|
class HistoryItem;
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Session;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
namespace Payments {
|
namespace Payments {
|
||||||
struct CreditsFormData;
|
struct CreditsFormData;
|
||||||
} // namespace Payments
|
} // namespace Payments
|
||||||
|
@ -22,4 +26,7 @@ void SendCreditsBox(
|
||||||
std::shared_ptr<Payments::CreditsFormData> data,
|
std::shared_ptr<Payments::CreditsFormData> data,
|
||||||
Fn<void()> sent);
|
Fn<void()> sent);
|
||||||
|
|
||||||
|
[[nodiscard]] TextWithEntities CreditsEmoji(
|
||||||
|
not_null<Main::Session*> session);
|
||||||
|
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
|
@ -25,10 +25,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "history/view/controls/history_view_characters_limit.h"
|
#include "history/view/controls/history_view_characters_limit.h"
|
||||||
#include "history/view/history_view_schedule_box.h"
|
#include "history/view/history_view_schedule_box.h"
|
||||||
#include "core/mime_type.h"
|
#include "core/mime_type.h"
|
||||||
|
#include "core/ui_integration.h"
|
||||||
#include "base/event_filter.h"
|
#include "base/event_filter.h"
|
||||||
#include "base/call_delayed.h"
|
#include "base/call_delayed.h"
|
||||||
#include "boxes/premium_limits_box.h"
|
#include "boxes/premium_limits_box.h"
|
||||||
#include "boxes/premium_preview_box.h"
|
#include "boxes/premium_preview_box.h"
|
||||||
|
#include "boxes/send_credits_box.h"
|
||||||
#include "ui/effects/scroll_content_shadow.h"
|
#include "ui/effects/scroll_content_shadow.h"
|
||||||
#include "ui/widgets/checkbox.h"
|
#include "ui/widgets/checkbox.h"
|
||||||
#include "ui/widgets/scroll_area.h"
|
#include "ui/widgets/scroll_area.h"
|
||||||
|
@ -749,13 +751,26 @@ void SendFilesBox::refreshPriceTag() {
|
||||||
p.drawRoundedRect(raw->rect(), radius, radius);
|
p.drawRoundedRect(raw->rect(), radius, radius);
|
||||||
}, raw->lifetime());
|
}, raw->lifetime());
|
||||||
|
|
||||||
|
const auto session = &_show->session();
|
||||||
auto price = _price.value() | rpl::map([=](uint64 amount) {
|
auto price = _price.value() | rpl::map([=](uint64 amount) {
|
||||||
return QChar(0x2B50) + Lang::FormatCountDecimal(amount);
|
auto result = Ui::Text::Colorized(Ui::CreditsEmoji(session));
|
||||||
|
result.append(Lang::FormatCountDecimal(amount));
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
|
auto text = tr::lng_paid_price(
|
||||||
|
lt_price,
|
||||||
|
std::move(price),
|
||||||
|
Ui::Text::WithEntities);
|
||||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||||
raw,
|
raw,
|
||||||
tr::lng_paid_price(lt_price, std::move(price)),
|
QString(),
|
||||||
st::paidTagLabel);
|
st::paidTagLabel);
|
||||||
|
std::move(text) | rpl::start_with_next([=](TextWithEntities &&text) {
|
||||||
|
label->setMarkedText(text, Core::MarkedTextContext{
|
||||||
|
.session = session,
|
||||||
|
.customEmojiRepaint = [=] { label->update(); },
|
||||||
|
});
|
||||||
|
}, label->lifetime());
|
||||||
label->show();
|
label->show();
|
||||||
label->sizeValue() | rpl::start_with_next([=](QSize size) {
|
label->sizeValue() | rpl::start_with_next([=](QSize size) {
|
||||||
const auto inner = QRect(QPoint(), size);
|
const auto inner = QRect(QPoint(), size);
|
||||||
|
@ -1633,11 +1648,16 @@ void SendFilesBox::send(
|
||||||
auto caption = (_caption && !_caption->isHidden())
|
auto caption = (_caption && !_caption->isHidden())
|
||||||
? _caption->getTextWithAppliedMarkdown()
|
? _caption->getTextWithAppliedMarkdown()
|
||||||
: TextWithTags();
|
: TextWithTags();
|
||||||
options.invertCaption = _invertCaption;
|
|
||||||
options.price = hasPrice() ? _price.current() : 0;
|
|
||||||
if (!validateLength(caption.text)) {
|
if (!validateLength(caption.text)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
options.invertCaption = _invertCaption;
|
||||||
|
options.price = hasPrice() ? _price.current() : 0;
|
||||||
|
if (options.price > 0) {
|
||||||
|
for (auto &file : _list.files) {
|
||||||
|
file.spoiler = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
_confirmedCallback(
|
_confirmedCallback(
|
||||||
std::move(_list),
|
std::move(_list),
|
||||||
_sendWay.current(),
|
_sendWay.current(),
|
||||||
|
|
|
@ -1417,6 +1417,9 @@ paidAmountAbout: FlatLabel(defaultFlatLabel) {
|
||||||
}
|
}
|
||||||
paidTagLabel: FlatLabel(defaultFlatLabel) {
|
paidTagLabel: FlatLabel(defaultFlatLabel) {
|
||||||
textFg: radialFg;
|
textFg: radialFg;
|
||||||
|
palette: TextPalette(defaultTextPalette) {
|
||||||
|
linkFg: creditsBg1;
|
||||||
|
}
|
||||||
style: semiboldTextStyle;
|
style: semiboldTextStyle;
|
||||||
}
|
}
|
||||||
paidTagPadding: margins(16px, 6px, 16px, 6px);
|
paidTagPadding: margins(16px, 6px, 16px, 6px);
|
||||||
|
|
|
@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "api/api_views.h"
|
#include "api/api_views.h"
|
||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
#include "ui/text/format_values.h"
|
#include "ui/text/format_values.h"
|
||||||
|
#include "ui/text/text_utilities.h"
|
||||||
#include "ui/painter.h"
|
#include "ui/painter.h"
|
||||||
#include "core/click_handler_types.h"
|
#include "core/click_handler_types.h"
|
||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
|
@ -25,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "history/view/media/history_view_theme_document.h"
|
#include "history/view/media/history_view_theme_document.h"
|
||||||
#include "history/history_item.h"
|
#include "history/history_item.h"
|
||||||
#include "history/history.h"
|
#include "history/history.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
#include "media/streaming/media_streaming_utility.h"
|
#include "media/streaming/media_streaming_utility.h"
|
||||||
|
@ -34,6 +36,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "styles/style_chat.h"
|
#include "styles/style_chat.h"
|
||||||
|
|
||||||
namespace HistoryView {
|
namespace HistoryView {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kMediaUnlockedTooltipDuration = 5 * crl::time(1000);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void PaintInterpolatedIcon(
|
void PaintInterpolatedIcon(
|
||||||
QPainter &p,
|
QPainter &p,
|
||||||
|
@ -191,10 +198,36 @@ QSize CountPhotoMediaSize(
|
||||||
media.scaled(media.width(), newWidth, Qt::KeepAspectRatio));
|
media.scaled(media.width(), newWidth, Qt::KeepAspectRatio));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ShowPaidMediaUnlockedToast(
|
||||||
|
not_null<Window::SessionController*> controller,
|
||||||
|
not_null<HistoryItem*> item) {
|
||||||
|
const auto media = item->media();
|
||||||
|
const auto invoice = media ? media->invoice() : nullptr;
|
||||||
|
if (!invoice || !invoice->isPaidMedia) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto sender = item->originalSender();
|
||||||
|
const auto broadcast = (sender && sender->isBroadcast())
|
||||||
|
? sender
|
||||||
|
: item->history()->peer.get();
|
||||||
|
auto text = tr::lng_credits_media_done_title(
|
||||||
|
tr::now,
|
||||||
|
Ui::Text::Bold
|
||||||
|
).append('\n').append(tr::lng_credits_media_done_text(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
invoice->amount,
|
||||||
|
lt_chat,
|
||||||
|
Ui::Text::Bold(broadcast->name()),
|
||||||
|
Ui::Text::RichLangValue));
|
||||||
|
controller->showToast(std::move(text), kMediaUnlockedTooltipDuration);
|
||||||
|
}
|
||||||
|
|
||||||
ClickHandlerPtr MakePaidMediaLink(not_null<HistoryItem*> item) {
|
ClickHandlerPtr MakePaidMediaLink(not_null<HistoryItem*> item) {
|
||||||
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
|
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
|
||||||
const auto my = context.other.value<ClickHandlerContext>();
|
const auto my = context.other.value<ClickHandlerContext>();
|
||||||
const auto controller = my.sessionWindow.get();
|
const auto controller = my.sessionWindow.get();
|
||||||
|
const auto weak = my.sessionWindow;
|
||||||
const auto itemId = item->fullId();
|
const auto itemId = item->fullId();
|
||||||
const auto session = &item->history()->session();
|
const auto session = &item->history()->session();
|
||||||
using Result = Payments::CheckoutResult;
|
using Result = Payments::CheckoutResult;
|
||||||
|
@ -203,6 +236,9 @@ ClickHandlerPtr MakePaidMediaLink(not_null<HistoryItem*> item) {
|
||||||
return;
|
return;
|
||||||
} else if (const auto item = session->data().message(itemId)) {
|
} else if (const auto item = session->data().message(itemId)) {
|
||||||
session->api().views().pollExtendedMedia(item, true);
|
session->api().views().pollExtendedMedia(item, true);
|
||||||
|
if (const auto strong = weak.get()) {
|
||||||
|
ShowPaidMediaUnlockedToast(strong, item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Payments::CheckoutProcess::Start(
|
Payments::CheckoutProcess::Start(
|
||||||
|
|
|
@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#include "history/view/media/history_view_photo.h"
|
#include "history/view/media/history_view_photo.h"
|
||||||
|
|
||||||
|
#include "boxes/send_credits_box.h"
|
||||||
#include "history/history_item_components.h"
|
#include "history/history_item_components.h"
|
||||||
#include "history/history_item.h"
|
#include "history/history_item.h"
|
||||||
#include "history/history.h"
|
#include "history/history.h"
|
||||||
|
@ -24,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "ui/image/image.h"
|
#include "ui/image/image.h"
|
||||||
#include "ui/effects/spoiler_mess.h"
|
#include "ui/effects/spoiler_mess.h"
|
||||||
#include "ui/chat/chat_style.h"
|
#include "ui/chat/chat_style.h"
|
||||||
|
#include "ui/text/text_utilities.h"
|
||||||
#include "ui/grouped_layout.h"
|
#include "ui/grouped_layout.h"
|
||||||
#include "ui/cached_round_corners.h"
|
#include "ui/cached_round_corners.h"
|
||||||
#include "ui/painter.h"
|
#include "ui/painter.h"
|
||||||
|
@ -38,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/data_auto_download.h"
|
#include "data/data_auto_download.h"
|
||||||
#include "data/data_web_page.h"
|
#include "data/data_web_page.h"
|
||||||
#include "core/application.h"
|
#include "core/application.h"
|
||||||
|
#include "core/ui_integration.h"
|
||||||
#include "styles/style_chat.h"
|
#include "styles/style_chat.h"
|
||||||
#include "styles/style_chat_helpers.h"
|
#include "styles/style_chat_helpers.h"
|
||||||
|
|
||||||
|
@ -65,6 +68,7 @@ struct Photo::PriceTag {
|
||||||
QImage cache;
|
QImage cache;
|
||||||
QColor darken;
|
QColor darken;
|
||||||
QColor fg;
|
QColor fg;
|
||||||
|
QColor star;
|
||||||
ClickHandlerPtr link;
|
ClickHandlerPtr link;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -438,9 +442,11 @@ void Photo::drawPriceTag(
|
||||||
const auto st = context.st;
|
const auto st = context.st;
|
||||||
const auto darken = st->msgDateImgBg()->c;
|
const auto darken = st->msgDateImgBg()->c;
|
||||||
const auto fg = st->msgDateImgFg()->c;
|
const auto fg = st->msgDateImgFg()->c;
|
||||||
|
const auto star = st->creditsBg1()->c;
|
||||||
if (_priceTag->cache.isNull()
|
if (_priceTag->cache.isNull()
|
||||||
|| _priceTag->darken != darken
|
|| _priceTag->darken != darken
|
||||||
|| _priceTag->fg != fg) {
|
|| _priceTag->fg != fg
|
||||||
|
|| _priceTag->star != star) {
|
||||||
auto bg = generateBackground();
|
auto bg = generateBackground();
|
||||||
if (bg.isNull()) {
|
if (bg.isNull()) {
|
||||||
bg = QImage(2, 2, QImage::Format_ARGB32_Premultiplied);
|
bg = QImage(2, 2, QImage::Format_ARGB32_Premultiplied);
|
||||||
|
@ -448,12 +454,21 @@ void Photo::drawPriceTag(
|
||||||
}
|
}
|
||||||
|
|
||||||
auto text = Ui::Text::String();
|
auto text = Ui::Text::String();
|
||||||
text.setText(
|
const auto session = &history()->session();
|
||||||
|
auto price = Ui::Text::Colorized(Ui::CreditsEmoji(session));
|
||||||
|
price.append(Lang::FormatCountDecimal(_priceTag->price));
|
||||||
|
text.setMarkedText(
|
||||||
st::semiboldTextStyle,
|
st::semiboldTextStyle,
|
||||||
tr::lng_paid_price(
|
tr::lng_paid_price(
|
||||||
tr::now,
|
tr::now,
|
||||||
lt_price,
|
lt_price,
|
||||||
QChar(0x2B50) + Lang::FormatCountDecimal(_priceTag->price)));
|
price,
|
||||||
|
Ui::Text::WithEntities),
|
||||||
|
kMarkupTextOptions,
|
||||||
|
Core::MarkedTextContext{
|
||||||
|
.session = session,
|
||||||
|
.customEmojiRepaint = [] {},
|
||||||
|
});
|
||||||
const auto width = text.maxWidth();
|
const auto width = text.maxWidth();
|
||||||
const auto inner = QRect(0, 0, width, text.minHeight());
|
const auto inner = QRect(0, 0, width, text.minHeight());
|
||||||
const auto outer = inner.marginsAdded(st::paidTagPadding);
|
const auto outer = inner.marginsAdded(st::paidTagPadding);
|
||||||
|
@ -476,6 +491,7 @@ void Photo::drawPriceTag(
|
||||||
bg);
|
bg);
|
||||||
p.fillRect(QRect(QPoint(), size), darken);
|
p.fillRect(QRect(QPoint(), size), darken);
|
||||||
p.setPen(fg);
|
p.setPen(fg);
|
||||||
|
p.setTextPalette(st->priceTagTextPalette());
|
||||||
text.draw(p, -outer.x(), -outer.y(), width);
|
text.draw(p, -outer.x(), -outer.y(), width);
|
||||||
p.end();
|
p.end();
|
||||||
|
|
||||||
|
|
|
@ -106,6 +106,9 @@ outTextPaletteSelected: TextPalette(outTextPalette) {
|
||||||
monoFg: msgOutMonoFgSelected;
|
monoFg: msgOutMonoFgSelected;
|
||||||
spoilerFg: msgOutDateFgSelected;
|
spoilerFg: msgOutDateFgSelected;
|
||||||
}
|
}
|
||||||
|
priceTagTextPalette: TextPalette(defaultTextPalette) {
|
||||||
|
linkFg: creditsBg1;
|
||||||
|
}
|
||||||
fwdTextUserpicPadding: margins(0px, 1px, 3px, 0px);
|
fwdTextUserpicPadding: margins(0px, 1px, 3px, 0px);
|
||||||
fwdTextStyle: TextStyle(semiboldTextStyle) {
|
fwdTextStyle: TextStyle(semiboldTextStyle) {
|
||||||
linkUnderline: kLinkUnderlineNever;
|
linkUnderline: kLinkUnderlineNever;
|
||||||
|
|
|
@ -171,6 +171,7 @@ ChatStyle::ChatStyle(rpl::producer<ColorIndicesCompressed> colorIndices) {
|
||||||
make(_historyPsaForwardPalette, st::historyPsaForwardPalette);
|
make(_historyPsaForwardPalette, st::historyPsaForwardPalette);
|
||||||
make(_imgReplyTextPalette, st::imgReplyTextPalette);
|
make(_imgReplyTextPalette, st::imgReplyTextPalette);
|
||||||
make(_serviceTextPalette, st::serviceTextPalette);
|
make(_serviceTextPalette, st::serviceTextPalette);
|
||||||
|
make(_priceTagTextPalette, st::priceTagTextPalette);
|
||||||
make(_historyRepliesInvertedIcon, st::historyRepliesInvertedIcon);
|
make(_historyRepliesInvertedIcon, st::historyRepliesInvertedIcon);
|
||||||
make(_historyViewsInvertedIcon, st::historyViewsInvertedIcon);
|
make(_historyViewsInvertedIcon, st::historyViewsInvertedIcon);
|
||||||
make(_historyViewsSendingIcon, st::historyViewsSendingIcon);
|
make(_historyViewsSendingIcon, st::historyViewsSendingIcon);
|
||||||
|
|
|
@ -349,6 +349,9 @@ public:
|
||||||
[[nodiscard]] const style::TextPalette &serviceTextPalette() const {
|
[[nodiscard]] const style::TextPalette &serviceTextPalette() const {
|
||||||
return _serviceTextPalette;
|
return _serviceTextPalette;
|
||||||
}
|
}
|
||||||
|
[[nodiscard]] const style::TextPalette &priceTagTextPalette() const {
|
||||||
|
return _priceTagTextPalette;
|
||||||
|
}
|
||||||
[[nodiscard]] const style::icon &historyRepliesInvertedIcon() const {
|
[[nodiscard]] const style::icon &historyRepliesInvertedIcon() const {
|
||||||
return _historyRepliesInvertedIcon;
|
return _historyRepliesInvertedIcon;
|
||||||
}
|
}
|
||||||
|
@ -516,6 +519,7 @@ private:
|
||||||
style::TextPalette _historyPsaForwardPalette;
|
style::TextPalette _historyPsaForwardPalette;
|
||||||
style::TextPalette _imgReplyTextPalette;
|
style::TextPalette _imgReplyTextPalette;
|
||||||
style::TextPalette _serviceTextPalette;
|
style::TextPalette _serviceTextPalette;
|
||||||
|
style::TextPalette _priceTagTextPalette;
|
||||||
style::icon _historyRepliesInvertedIcon = { Qt::Uninitialized };
|
style::icon _historyRepliesInvertedIcon = { Qt::Uninitialized };
|
||||||
style::icon _historyViewsInvertedIcon = { Qt::Uninitialized };
|
style::icon _historyViewsInvertedIcon = { Qt::Uninitialized };
|
||||||
style::icon _historyViewsSendingIcon = { Qt::Uninitialized };
|
style::icon _historyViewsSendingIcon = { Qt::Uninitialized };
|
||||||
|
|
Loading…
Add table
Reference in a new issue