mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-15 21:57:10 +02:00
Support entities in star gift messages.
This commit is contained in:
parent
8b11d2d5e7
commit
9ace04d2c9
11 changed files with 191 additions and 79 deletions
|
@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "api/api_premium.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "boxes/send_credits_box.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "chat_helpers/message_field.h"
|
||||
#include "chat_helpers/stickers_gift_box_pack.h"
|
||||
#include "chat_helpers/stickers_lottie.h"
|
||||
#include "core/ui_integration.h"
|
||||
|
@ -65,7 +67,7 @@ namespace {
|
|||
|
||||
constexpr auto kPriceTabAll = 0;
|
||||
constexpr auto kPriceTabLimited = -1;
|
||||
constexpr auto kGiftMessageLimit = 256;
|
||||
constexpr auto kGiftMessageLimit = 255;
|
||||
constexpr auto kSentToastDuration = 3 * crl::time(1000);
|
||||
|
||||
using namespace HistoryView;
|
||||
|
@ -83,7 +85,7 @@ struct GiftsDescriptor {
|
|||
|
||||
struct GiftDetails {
|
||||
GiftDescriptor descriptor;
|
||||
QString text;
|
||||
TextWithEntities text;
|
||||
bool anonymous = false;
|
||||
};
|
||||
|
||||
|
@ -160,14 +162,16 @@ auto GenerateGiftMedia(
|
|||
auto pushText = [&](
|
||||
TextWithEntities text,
|
||||
QMargins margins = {},
|
||||
const base::flat_map<uint16, ClickHandlerPtr> &links = {}) {
|
||||
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
|
||||
const std::any &context = {}) {
|
||||
if (text.empty()) {
|
||||
return;
|
||||
}
|
||||
push(std::make_unique<MediaGenericTextPart>(
|
||||
std::move(text),
|
||||
margins,
|
||||
links));
|
||||
links,
|
||||
context));
|
||||
};
|
||||
const auto sticker = [=] {
|
||||
using Tag = ChatHelpers::StickerLottieSize;
|
||||
|
@ -202,11 +206,18 @@ auto GenerateGiftMedia(
|
|||
lt_count,
|
||||
v::get<GiftTypeStars>(descriptor).convertStars,
|
||||
Ui::Text::RichLangValue);
|
||||
auto description = data.text.isEmpty()
|
||||
auto description = data.text.empty()
|
||||
? std::move(textFallback)
|
||||
: TextWithEntities{ data.text };
|
||||
: data.text;
|
||||
pushText(Ui::Text::Bold(title), st::giftBoxPreviewTitlePadding);
|
||||
pushText(std::move(description), st::giftBoxPreviewTextPadding);
|
||||
pushText(
|
||||
std::move(description),
|
||||
st::giftBoxPreviewTextPadding,
|
||||
{},
|
||||
Core::MarkedTextContext{
|
||||
.session = &parent->data()->history()->session(),
|
||||
.customEmojiRepaint = [parent] { parent->repaint(); },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -732,10 +743,6 @@ void SendGiftBox(
|
|||
});
|
||||
|
||||
const auto session = &window->session();
|
||||
const auto context = Core::MarkedTextContext{
|
||||
.session = session,
|
||||
.customEmojiRepaint = [] {},
|
||||
};
|
||||
auto cost = rpl::single([&] {
|
||||
return v::match(descriptor, [&](const GiftTypePremium &data) {
|
||||
if (data.currency == Ui::kCreditsCurrency) {
|
||||
|
@ -777,10 +784,40 @@ void SendGiftBox(
|
|||
kGiftMessageLimit);
|
||||
text->changes() | rpl::start_with_next([=] {
|
||||
auto now = state->details.current();
|
||||
now.text = text->getLastText();
|
||||
auto textWithTags = text->getTextWithAppliedMarkdown();
|
||||
now.text = TextWithEntities{
|
||||
std::move(textWithTags.text),
|
||||
TextUtilities::ConvertTextTagsToEntities(textWithTags.tags)
|
||||
};
|
||||
state->details = std::move(now);
|
||||
}, text->lifetime());
|
||||
|
||||
const auto allow = [=](not_null<DocumentData*> emoji) {
|
||||
return true;
|
||||
};
|
||||
InitMessageFieldHandlers({
|
||||
.session = &window->session(),
|
||||
.show = window->uiShow(),
|
||||
.field = text,
|
||||
.customEmojiPaused = [=] {
|
||||
using namespace Window;
|
||||
return window->isGifPausedAtLeastFor(GifPauseReason::Layer);
|
||||
},
|
||||
.allowPremiumEmoji = allow,
|
||||
.allowMarkdownTags = {
|
||||
Ui::InputField::kTagBold,
|
||||
Ui::InputField::kTagItalic,
|
||||
Ui::InputField::kTagUnderline,
|
||||
Ui::InputField::kTagStrikeOut,
|
||||
Ui::InputField::kTagSpoiler,
|
||||
}
|
||||
});
|
||||
Ui::Emoji::SuggestionsController::Init(
|
||||
box->getDelegate()->outerContainer(),
|
||||
text,
|
||||
&window->session(),
|
||||
{ .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
|
||||
|
||||
AddDivider(container);
|
||||
AddSkip(container);
|
||||
container->add(
|
||||
|
@ -825,7 +862,7 @@ void SendGiftBox(
|
|||
Payments::CheckoutProcess::Start(Payments::InvoiceStarGift{
|
||||
.giftId = gift.id,
|
||||
.randomId = state->randomId,
|
||||
.message = { details.text },
|
||||
.message = details.text,
|
||||
.user = peer->asUser(),
|
||||
.anonymous = details.anonymous,
|
||||
}, done, Payments::ProcessNonPanelPaymentFormFactory(window, done));
|
||||
|
|
|
@ -1267,13 +1267,16 @@ void SendFilesBox::setupCaption() {
|
|||
: (_limits & SendFilesAllow::EmojiWithoutPremium);
|
||||
};
|
||||
const auto show = _show;
|
||||
InitMessageFieldHandlers(
|
||||
&show->session(),
|
||||
show,
|
||||
_caption.data(),
|
||||
[=] { return show->paused(Window::GifPauseReason::Layer); },
|
||||
allow,
|
||||
&_st.files.caption);
|
||||
InitMessageFieldHandlers({
|
||||
.session = &show->session(),
|
||||
.show = show,
|
||||
.field = _caption.data(),
|
||||
.customEmojiPaused = [=] {
|
||||
return show->paused(Window::GifPauseReason::Layer);
|
||||
},
|
||||
.allowPremiumEmoji = allow,
|
||||
.fieldStyle = &_st.files.caption,
|
||||
});
|
||||
setupCaptionAutocomplete();
|
||||
Ui::Emoji::SuggestionsController::Init(
|
||||
getDelegate()->outerContainer(),
|
||||
|
|
|
@ -240,13 +240,12 @@ void ShareBox::prepareCommentField() {
|
|||
}, field->lifetime());
|
||||
|
||||
if (const auto show = uiShow(); show->valid()) {
|
||||
InitMessageFieldHandlers(
|
||||
_descriptor.session,
|
||||
Main::MakeSessionShow(show, _descriptor.session),
|
||||
field,
|
||||
nullptr,
|
||||
nullptr,
|
||||
_descriptor.stLabel);
|
||||
InitMessageFieldHandlers({
|
||||
.session = _descriptor.session,
|
||||
.show = Main::MakeSessionShow(show, _descriptor.session),
|
||||
.field = field,
|
||||
.fieldStyle = _descriptor.stLabel,
|
||||
});
|
||||
}
|
||||
field->setSubmitSettings(Core::App().settings().sendSubmitWay());
|
||||
|
||||
|
|
|
@ -423,18 +423,14 @@ Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
|
|||
};
|
||||
}
|
||||
|
||||
void InitMessageFieldHandlers(
|
||||
not_null<Main::Session*> session,
|
||||
std::shared_ptr<Main::SessionShow> show,
|
||||
not_null<Ui::InputField*> field,
|
||||
Fn<bool()> customEmojiPaused,
|
||||
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji,
|
||||
const style::InputField *fieldStyle) {
|
||||
const auto paused = [customEmojiPaused] {
|
||||
return customEmojiPaused && customEmojiPaused();
|
||||
void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args) {
|
||||
const auto paused = [passed = args.customEmojiPaused] {
|
||||
return passed && passed();
|
||||
};
|
||||
const auto field = args.field;
|
||||
const auto session = args.session;
|
||||
field->setTagMimeProcessor(
|
||||
FieldTagMimeProcessor(session, allowPremiumEmoji));
|
||||
FieldTagMimeProcessor(session, args.allowPremiumEmoji));
|
||||
field->setCustomTextContext([=](Fn<void()> repaint) {
|
||||
return std::any(Core::MarkedTextContext{
|
||||
.session = session,
|
||||
|
@ -448,12 +444,14 @@ void InitMessageFieldHandlers(
|
|||
field->setInstantReplaces(Ui::InstantReplaces::Default());
|
||||
field->setInstantReplacesEnabled(
|
||||
Core::App().settings().replaceEmojiValue());
|
||||
field->setMarkdownReplacesEnabled(true);
|
||||
if (show) {
|
||||
field->setMarkdownReplacesEnabled(rpl::single(Ui::MarkdownEnabledState{
|
||||
Ui::MarkdownEnabled{ std::move(args.allowMarkdownTags) }
|
||||
}));
|
||||
if (const auto &show = args.show) {
|
||||
field->setEditLinkCallback(
|
||||
DefaultEditLinkCallback(show, field, fieldStyle));
|
||||
DefaultEditLinkCallback(show, field, args.fieldStyle));
|
||||
field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
|
||||
InitSpellchecker(show, field, fieldStyle != nullptr);
|
||||
InitSpellchecker(show, field, args.fieldStyle != nullptr);
|
||||
}
|
||||
const auto style = field->lifetime().make_state<Ui::ChatStyle>(
|
||||
session->colorIndicesValue());
|
||||
|
@ -553,12 +551,15 @@ void InitMessageFieldHandlers(
|
|||
not_null<Ui::InputField*> field,
|
||||
ChatHelpers::PauseReason pauseReasonLevel,
|
||||
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
|
||||
InitMessageFieldHandlers(
|
||||
&controller->session(),
|
||||
controller->uiShow(),
|
||||
field,
|
||||
[=] { return controller->isGifPausedAtLeastFor(pauseReasonLevel); },
|
||||
allowPremiumEmoji);
|
||||
InitMessageFieldHandlers({
|
||||
.session = &controller->session(),
|
||||
.show = controller->uiShow(),
|
||||
.field = field,
|
||||
.customEmojiPaused = [=] {
|
||||
return controller->isGifPausedAtLeastFor(pauseReasonLevel);
|
||||
},
|
||||
.allowPremiumEmoji = std::move(allowPremiumEmoji),
|
||||
});
|
||||
}
|
||||
|
||||
void InitMessageFieldGeometry(not_null<Ui::InputField*> field) {
|
||||
|
@ -574,12 +575,15 @@ void InitMessageField(
|
|||
std::shared_ptr<ChatHelpers::Show> show,
|
||||
not_null<Ui::InputField*> field,
|
||||
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji) {
|
||||
InitMessageFieldHandlers(
|
||||
&show->session(),
|
||||
show,
|
||||
field,
|
||||
[=] { return show->paused(ChatHelpers::PauseReason::Any); },
|
||||
std::move(allowPremiumEmoji));
|
||||
InitMessageFieldHandlers({
|
||||
.session = &show->session(),
|
||||
.show = show,
|
||||
.field = field,
|
||||
.customEmojiPaused = [=] {
|
||||
return show->paused(ChatHelpers::PauseReason::Any);
|
||||
},
|
||||
.allowPremiumEmoji = std::move(allowPremiumEmoji),
|
||||
});
|
||||
InitMessageFieldGeometry(field);
|
||||
}
|
||||
|
||||
|
|
|
@ -54,13 +54,18 @@ Fn<bool(
|
|||
const style::InputField *fieldStyle = nullptr);
|
||||
Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
|
||||
std::shared_ptr<Ui::Show> show);
|
||||
void InitMessageFieldHandlers(
|
||||
not_null<Main::Session*> session,
|
||||
std::shared_ptr<Main::SessionShow> show, // may be null
|
||||
not_null<Ui::InputField*> field,
|
||||
Fn<bool()> customEmojiPaused,
|
||||
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji = nullptr,
|
||||
const style::InputField *fieldStyle = nullptr);
|
||||
|
||||
struct MessageFieldHandlersArgs {
|
||||
not_null<Main::Session*> session;
|
||||
std::shared_ptr<Main::SessionShow> show; // may be null
|
||||
not_null<Ui::InputField*> field;
|
||||
Fn<bool()> customEmojiPaused;
|
||||
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji;
|
||||
const style::InputField *fieldStyle = nullptr;
|
||||
base::flat_set<QString> allowMarkdownTags;
|
||||
};
|
||||
void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args);
|
||||
|
||||
void InitMessageFieldHandlers(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<Ui::InputField*> field,
|
||||
|
|
|
@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/dynamic_image.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
@ -222,10 +223,15 @@ QMargins MediaGeneric::inBubblePadding() const {
|
|||
MediaGenericTextPart::MediaGenericTextPart(
|
||||
TextWithEntities text,
|
||||
QMargins margins,
|
||||
const base::flat_map<uint16, ClickHandlerPtr> &links)
|
||||
const base::flat_map<uint16, ClickHandlerPtr> &links,
|
||||
const std::any &context)
|
||||
: _text(st::msgMinWidth)
|
||||
, _margins(margins) {
|
||||
_text.setMarkedText(st::defaultTextStyle, text);
|
||||
_text.setMarkedText(
|
||||
st::defaultTextStyle,
|
||||
text,
|
||||
kMarkupTextOptions,
|
||||
context);
|
||||
for (const auto &[index, link] : links) {
|
||||
_text.setLink(index, link);
|
||||
}
|
||||
|
@ -248,7 +254,10 @@ void MediaGenericTextPart::draw(
|
|||
.palette = &(service
|
||||
? context.st->serviceTextPalette()
|
||||
: context.messageStyle()->textPalette),
|
||||
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
||||
.now = context.now,
|
||||
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
|
||||
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -121,7 +121,8 @@ public:
|
|||
MediaGenericTextPart(
|
||||
TextWithEntities text,
|
||||
QMargins margins,
|
||||
const base::flat_map<uint16, ClickHandlerPtr> &links = {});
|
||||
const base::flat_map<uint16, ClickHandlerPtr> &links = {},
|
||||
const std::any &context = {});
|
||||
|
||||
void draw(
|
||||
Painter &p,
|
||||
|
|
|
@ -6,14 +6,20 @@ For license and copyright information please follow this link:
|
|||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "history/view/media/history_view_service_box.h"
|
||||
//
|
||||
#include "history/view/history_view_cursor_state.h"
|
||||
|
||||
#include "core/ui_integration.h"
|
||||
#include "history/view/media/history_view_sticker_player_abstract.h"
|
||||
#include "history/view/history_view_cursor_state.h"
|
||||
#include "history/view/history_view_element.h"
|
||||
#include "history/view/history_view_text_helper.h"
|
||||
#include "history/history.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_premium.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
@ -48,9 +54,15 @@ ServiceBox::ServiceBox(
|
|||
EntityType::StrikeOut,
|
||||
EntityType::Underline,
|
||||
EntityType::Italic,
|
||||
EntityType::Spoiler,
|
||||
EntityType::CustomEmoji,
|
||||
}),
|
||||
kMarkupTextOptions,
|
||||
_maxWidth)
|
||||
_maxWidth,
|
||||
Core::MarkedTextContext{
|
||||
.session = &parent->history()->session(),
|
||||
.customEmojiRepaint = [parent] { parent->customEmojiRepaint(); },
|
||||
})
|
||||
, _size(
|
||||
_content->width(),
|
||||
(st::msgServiceGiftBoxTopSkip
|
||||
|
@ -67,6 +79,7 @@ ServiceBox::ServiceBox(
|
|||
: (_content->buttonSkip() + st::msgServiceGiftBoxButtonHeight))
|
||||
+ st::msgServiceGiftBoxButtonMargins.bottom()))
|
||||
, _innerSize(_size - QSize(0, st::msgServiceGiftBoxTopSkip)) {
|
||||
InitElementTextPart(_parent, _subtitle);
|
||||
if (auto text = _content->button()) {
|
||||
_button.repaint = [=] { repaint(); };
|
||||
std::move(text) | rpl::start_with_next([=](QString value) {
|
||||
|
@ -116,7 +129,17 @@ void ServiceBox::draw(Painter &p, const PaintContext &context) const {
|
|||
_title.draw(p, st::msgPadding.left(), top, _maxWidth, style::al_top);
|
||||
top += _title.countHeight(_maxWidth) + padding.bottom();
|
||||
}
|
||||
_subtitle.draw(p, st::msgPadding.left(), top, _maxWidth, style::al_top);
|
||||
_parent->prepareCustomEmojiPaint(p, context, _subtitle);
|
||||
_subtitle.draw(p, {
|
||||
.position = QPoint(st::msgPadding.left(), top),
|
||||
.availableWidth = _maxWidth,
|
||||
.align = style::al_top,
|
||||
.palette = &context.st->serviceTextPalette(),
|
||||
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
||||
.now = context.now,
|
||||
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
|
||||
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
|
||||
});
|
||||
top += _subtitle.countHeight(_maxWidth) + padding.bottom();
|
||||
}
|
||||
|
||||
|
@ -180,8 +203,30 @@ void ServiceBox::draw(Painter &p, const PaintContext &context) const {
|
|||
|
||||
TextState ServiceBox::textState(QPoint point, StateRequest request) const {
|
||||
auto result = TextState(_parent);
|
||||
const auto content = contentRect();
|
||||
const auto lookupSubtitleLink = [&] {
|
||||
auto top = st::msgServiceGiftBoxTopSkip
|
||||
+ content.top()
|
||||
+ content.height();
|
||||
const auto &padding = st::msgServiceGiftBoxTitlePadding;
|
||||
top += padding.top();
|
||||
if (!_title.isEmpty()) {
|
||||
top += _title.countHeight(_maxWidth) + padding.bottom();
|
||||
}
|
||||
auto subtitleRequest = request.forText();
|
||||
subtitleRequest.align = style::al_top;
|
||||
const auto state = _subtitle.getState(
|
||||
point - QPoint(st::msgPadding.left(), top),
|
||||
_maxWidth,
|
||||
subtitleRequest);
|
||||
if (state.link) {
|
||||
result.link = state.link;
|
||||
}
|
||||
};
|
||||
if (_button.empty()) {
|
||||
if (QRect(QPoint(), _innerSize).contains(point)) {
|
||||
if (!_button.link) {
|
||||
lookupSubtitleLink();
|
||||
} else if (QRect(QPoint(), _innerSize).contains(point)) {
|
||||
result.link = _button.link;
|
||||
}
|
||||
} else {
|
||||
|
@ -189,11 +234,13 @@ TextState ServiceBox::textState(QPoint point, StateRequest request) const {
|
|||
if (rect.contains(point)) {
|
||||
result.link = _button.link;
|
||||
_button.lastPoint = point - rect.topLeft();
|
||||
} else if (contentRect().contains(point)) {
|
||||
} else if (content.contains(point)) {
|
||||
if (!_contentLink) {
|
||||
_contentLink = _content->createViewLink();
|
||||
}
|
||||
result.link = _contentLink;
|
||||
} else {
|
||||
lookupSubtitleLink();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@ -238,6 +285,10 @@ bool ServiceBox::customInfoLayout() const {
|
|||
return false;
|
||||
}
|
||||
|
||||
void ServiceBox::hideSpoilers() {
|
||||
_subtitle.setSpoilerRevealed(false, anim::type::instant);
|
||||
}
|
||||
|
||||
bool ServiceBox::hasHeavyPart() const {
|
||||
return _content->hasHeavyPart();
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ public:
|
|||
[[nodiscard]] bool hideServiceText() const override {
|
||||
return _content->hideServiceText();
|
||||
}
|
||||
void hideSpoilers() override;
|
||||
|
||||
bool hasHeavyPart() const override;
|
||||
void unloadHeavyPart() override;
|
||||
|
|
|
@ -1091,11 +1091,10 @@ void Notification::showReplyField() {
|
|||
_replyArea->setFocus();
|
||||
_replyArea->setMaxLength(MaxMessageSize);
|
||||
_replyArea->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
|
||||
InitMessageFieldHandlers(
|
||||
&_item->history()->session(),
|
||||
nullptr,
|
||||
_replyArea.data(),
|
||||
nullptr);
|
||||
InitMessageFieldHandlers({
|
||||
.session = &_item->history()->session(),
|
||||
.field = _replyArea.data(),
|
||||
});
|
||||
|
||||
// Catch mouse press event to activate the window.
|
||||
QCoreApplication::instance()->installEventFilter(this);
|
||||
|
|
|
@ -2215,11 +2215,14 @@ QPointer<Ui::BoxContent> ShowForwardMessagesBox(
|
|||
|
||||
field->submits(
|
||||
) | rpl::start_with_next([=] { submit({}); }, field->lifetime());
|
||||
InitMessageFieldHandlers(
|
||||
session,
|
||||
show,
|
||||
field,
|
||||
[=] { return show->paused(GifPauseReason::Layer); });
|
||||
InitMessageFieldHandlers({
|
||||
.session = session,
|
||||
.show = show,
|
||||
.field = field,
|
||||
.customEmojiPaused = [=] {
|
||||
return show->paused(GifPauseReason::Layer);
|
||||
},
|
||||
});
|
||||
field->setSubmitSettings(Core::App().settings().sendSubmitWay());
|
||||
|
||||
Ui::SendPendingMoveResizeEvents(comment);
|
||||
|
|
Loading…
Add table
Reference in a new issue