save, work on toasts
This commit is contained in:
parent
33deafe194
commit
e3e1929436
16 changed files with 135 additions and 37 deletions
|
@ -99,6 +99,8 @@ set(SOURCES
|
||||||
gui/notifications/notificationseditor.h
|
gui/notifications/notificationseditor.h
|
||||||
gui/notifications/singlenotificationeditor.cpp
|
gui/notifications/singlenotificationeditor.cpp
|
||||||
gui/notifications/singlenotificationeditor.h
|
gui/notifications/singlenotificationeditor.h
|
||||||
|
gui/notifications/articlelistnotification.cpp
|
||||||
|
gui/notifications/articlelistnotification.h
|
||||||
gui/reusable/baselineedit.cpp
|
gui/reusable/baselineedit.cpp
|
||||||
gui/reusable/baselineedit.h
|
gui/reusable/baselineedit.h
|
||||||
gui/reusable/basetreeview.cpp
|
gui/reusable/basetreeview.cpp
|
||||||
|
@ -440,9 +442,10 @@ set(UI_FILES
|
||||||
gui/dialogs/formupdate.ui
|
gui/dialogs/formupdate.ui
|
||||||
gui/notifications/notificationseditor.ui
|
gui/notifications/notificationseditor.ui
|
||||||
gui/notifications/singlenotificationeditor.ui
|
gui/notifications/singlenotificationeditor.ui
|
||||||
|
gui/notifications/articlelistnotification.ui
|
||||||
|
gui/notifications/toastnotification.ui
|
||||||
gui/reusable/networkproxydetails.ui
|
gui/reusable/networkproxydetails.ui
|
||||||
gui/itemdetails.ui
|
gui/itemdetails.ui
|
||||||
gui/notifications/toastnotification.ui
|
|
||||||
gui/richtexteditor/mrichtextedit.ui
|
gui/richtexteditor/mrichtextedit.ui
|
||||||
gui/newspaperpreviewer.ui
|
gui/newspaperpreviewer.ui
|
||||||
gui/reusable/searchtextwidget.ui
|
gui/reusable/searchtextwidget.ui
|
||||||
|
|
|
@ -1090,20 +1090,7 @@ void FormMain::showAddAccountDialog() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void FormMain::reportABug() {
|
void FormMain::reportABug() {
|
||||||
qApp
|
qApp->web()->openUrlInExternalBrowser(QSL(APP_URL_ISSUES_NEW));
|
||||||
->showGuiMessage(Notification::Event::GeneralEvent,
|
|
||||||
GuiMessage(QDateTime::currentDateTime().toString(),
|
|
||||||
"Quisque ullamcorper ut purus nec tempus. Vivamus eros dolor, sagittis ultrices augue "
|
|
||||||
"ut, posuere fringilla lorem. Donec posuere, enim sit amet fermentum dignissim, tellus "
|
|
||||||
"lectus laoreet lectus, vestibulum laoreet felis tortor eget nunc. Curabitur sagittis "
|
|
||||||
"quam in scelerisque placerat. Vivamus vel porta tortor. Vivamus nec volutpat sem",
|
|
||||||
QSystemTrayIcon::MessageIcon::Information),
|
|
||||||
GuiMessageDestination(),
|
|
||||||
GuiAction("test", []() {
|
|
||||||
qDebugNN << "aa";
|
|
||||||
}));
|
|
||||||
|
|
||||||
// qApp->web()->openUrlInExternalBrowser(QSL(APP_URL_ISSUES_NEW));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FormMain::donate() {
|
void FormMain::donate() {
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
// For license of this file, see <project-root-folder>/LICENSE.md.
|
||||||
|
|
||||||
|
#include "gui/notifications/articlelistnotification.h"
|
||||||
|
|
||||||
|
ArticleListNotification::ArticleListNotification(QWidget* parent) : BaseToastNotification(parent) {
|
||||||
|
m_ui.setupUi(this);
|
||||||
|
|
||||||
|
setupCloseButton(m_ui.m_btnClose);
|
||||||
|
setupTimedClosing();
|
||||||
|
}
|
20
src/librssguard/gui/notifications/articlelistnotification.h
Normal file
20
src/librssguard/gui/notifications/articlelistnotification.h
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// For license of this file, see <project-root-folder>/LICENSE.md.
|
||||||
|
|
||||||
|
#ifndef ARTICLELISTNOTIFICATION_H
|
||||||
|
#define ARTICLELISTNOTIFICATION_H
|
||||||
|
|
||||||
|
#include "gui/notifications/basetoastnotification.h"
|
||||||
|
|
||||||
|
#include "ui_articlelistnotification.h"
|
||||||
|
|
||||||
|
class ArticleListNotification : public BaseToastNotification {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ArticleListNotification(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ui::ArticleListNotification m_ui;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // ARTICLELISTNOTIFICATION_H
|
|
@ -26,7 +26,7 @@ BaseToastNotification::BaseToastNotification(QWidget* parent) : QDialog(parent)
|
||||||
#endif
|
#endif
|
||||||
Qt::WindowType::FramelessWindowHint | Qt::WindowType::WindowStaysOnTopHint | Qt::WindowType::WindowSystemMenuHint);
|
Qt::WindowType::FramelessWindowHint | Qt::WindowType::WindowStaysOnTopHint | Qt::WindowType::WindowSystemMenuHint);
|
||||||
|
|
||||||
setStyleSheet(QSL("BaseToastNotification { border: 1px solid black; }"));
|
setStyleSheet(QSL("BaseToastNotification { border: 1px solid %1; }").arg(palette().windowText().color().name()));
|
||||||
installEventFilter(this);
|
installEventFilter(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,6 @@ class BaseToastNotification : public QDialog {
|
||||||
explicit BaseToastNotification(QWidget* parent = nullptr);
|
explicit BaseToastNotification(QWidget* parent = nullptr);
|
||||||
virtual ~BaseToastNotification();
|
virtual ~BaseToastNotification();
|
||||||
|
|
||||||
// If true, then notification is always moved as close to top as possible.
|
|
||||||
virtual bool alwaysOnTop() const = 0;
|
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
virtual void reject();
|
virtual void reject();
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ ToastNotification::ToastNotification(Notification::Event event,
|
||||||
const GuiMessage& msg,
|
const GuiMessage& msg,
|
||||||
const GuiAction& action,
|
const GuiAction& action,
|
||||||
QWidget* parent)
|
QWidget* parent)
|
||||||
: BaseToastNotification(parent) {
|
: BaseToastNotification() {
|
||||||
m_ui.setupUi(this);
|
m_ui.setupUi(this);
|
||||||
|
|
||||||
setupHeading();
|
setupHeading();
|
||||||
|
@ -31,10 +31,6 @@ ToastNotification::ToastNotification(Notification::Event event,
|
||||||
loadNotification(event, msg, action);
|
loadNotification(event, msg, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ToastNotification::alwaysOnTop() const {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ToastNotification::loadNotification(Notification::Event event, const GuiMessage& msg, const GuiAction& action) {
|
void ToastNotification::loadNotification(Notification::Event event, const GuiMessage& msg, const GuiAction& action) {
|
||||||
m_ui.m_lblTitle->setText(msg.m_title);
|
m_ui.m_lblTitle->setText(msg.m_title);
|
||||||
m_ui.m_lblBody->setText(msg.m_message);
|
m_ui.m_lblBody->setText(msg.m_message);
|
||||||
|
|
|
@ -18,8 +18,6 @@ class ToastNotification : public BaseToastNotification {
|
||||||
const GuiAction& action,
|
const GuiAction& action,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
virtual bool alwaysOnTop() const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupHeading();
|
void setupHeading();
|
||||||
void loadNotification(Notification::Event event, const GuiMessage& msg, const GuiAction& action);
|
void loadNotification(Notification::Event event, const GuiMessage& msg, const GuiAction& action);
|
||||||
|
|
|
@ -4,14 +4,16 @@
|
||||||
|
|
||||||
#include "3rd-party/boolinq/boolinq.h"
|
#include "3rd-party/boolinq/boolinq.h"
|
||||||
#include "gui/notifications/basetoastnotification.h"
|
#include "gui/notifications/basetoastnotification.h"
|
||||||
|
|
||||||
|
#include "gui/notifications/articlelistnotification.h"
|
||||||
#include "gui/notifications/toastnotification.h"
|
#include "gui/notifications/toastnotification.h"
|
||||||
|
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QScreen>
|
#include <QScreen>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
|
|
||||||
ToastNotificationsManager::ToastNotificationsManager(QObject* parent)
|
ToastNotificationsManager::ToastNotificationsManager(NotificationPosition position, int screen, QObject* parent)
|
||||||
: QObject(parent), m_position(NotificationPosition::BottomRight), m_screen(-1) {}
|
: QObject(parent), m_position(position), m_screen(screen), m_articleListNotification(nullptr) {}
|
||||||
|
|
||||||
ToastNotificationsManager::~ToastNotificationsManager() {
|
ToastNotificationsManager::~ToastNotificationsManager() {
|
||||||
clear();
|
clear();
|
||||||
|
@ -39,7 +41,7 @@ void ToastNotificationsManager::setPosition(NotificationPosition position) {
|
||||||
|
|
||||||
void ToastNotificationsManager::clear() {
|
void ToastNotificationsManager::clear() {
|
||||||
for (BaseToastNotification* notif : m_activeNotifications) {
|
for (BaseToastNotification* notif : m_activeNotifications) {
|
||||||
closeNotification(notif);
|
closeNotification(notif, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_activeNotifications.clear();
|
m_activeNotifications.clear();
|
||||||
|
@ -74,12 +76,48 @@ void ToastNotificationsManager::showNotification(Notification::Event event,
|
||||||
m_activeNotifications.prepend(notif);
|
m_activeNotifications.prepend(notif);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToastNotificationsManager::showNotification(const QList<Message>& new_messages) {}
|
void ToastNotificationsManager::showNotification(const QList<Message>& new_messages) {
|
||||||
|
if (m_articleListNotification == nullptr) {
|
||||||
|
m_articleListNotification = new ArticleListNotification();
|
||||||
|
hookNotification(m_articleListNotification);
|
||||||
|
}
|
||||||
|
|
||||||
void ToastNotificationsManager::closeNotification(BaseToastNotification* notif) {
|
if (!m_activeNotifications.isEmpty() && m_activeNotifications.first() != m_articleListNotification) {
|
||||||
|
// Article notification is somewhere in list, clear first to move it to first positon.
|
||||||
|
closeNotification(m_articleListNotification, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* screen = moveToProperScreen(m_articleListNotification);
|
||||||
|
|
||||||
|
// Insert new notification into free space.
|
||||||
|
m_articleListNotification->show();
|
||||||
|
|
||||||
|
auto notif_new_pos = cornerForNewNotification(screen->availableGeometry());
|
||||||
|
|
||||||
|
// Make sure notification is finally resized.
|
||||||
|
m_articleListNotification->adjustSize();
|
||||||
|
qApp->processEvents();
|
||||||
|
|
||||||
|
// Move notification, at this point we already need to know its precise size.
|
||||||
|
moveNotificationToCorner(m_articleListNotification, notif_new_pos);
|
||||||
|
|
||||||
|
// Remove out-of-bounds old notifications and shift existing
|
||||||
|
// ones to make space for new notifications.
|
||||||
|
removeOutOfBoundsNotifications(m_articleListNotification->height());
|
||||||
|
makeSpaceForNotification(m_articleListNotification->height());
|
||||||
|
|
||||||
|
m_activeNotifications.prepend(m_articleListNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToastNotificationsManager::closeNotification(BaseToastNotification* notif, bool delete_from_memory) {
|
||||||
auto notif_idx = m_activeNotifications.indexOf(notif);
|
auto notif_idx = m_activeNotifications.indexOf(notif);
|
||||||
|
|
||||||
notif->deleteLater();
|
if (delete_from_memory) {
|
||||||
|
notif->deleteLater();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
notif->hide();
|
||||||
|
}
|
||||||
|
|
||||||
m_activeNotifications.removeAll(notif);
|
m_activeNotifications.removeAll(notif);
|
||||||
|
|
||||||
|
@ -121,7 +159,9 @@ QPoint ToastNotificationsManager::cornerForNewNotification(QRect screen_rect) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToastNotificationsManager::hookNotification(BaseToastNotification* notif) {
|
void ToastNotificationsManager::hookNotification(BaseToastNotification* notif) {
|
||||||
connect(notif, &BaseToastNotification::closeRequested, this, &ToastNotificationsManager::closeNotification);
|
connect(notif, &BaseToastNotification::closeRequested, this, [this](BaseToastNotification* notif) {
|
||||||
|
closeNotification(notif, false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToastNotificationsManager::moveNotificationToCorner(BaseToastNotification* notif, QPoint corner) {
|
void ToastNotificationsManager::moveNotificationToCorner(BaseToastNotification* notif, QPoint corner) {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
class BaseToastNotification;
|
class BaseToastNotification;
|
||||||
class ToastNotification;
|
class ToastNotification;
|
||||||
|
class ArticleListNotification;
|
||||||
class QScreen;
|
class QScreen;
|
||||||
|
|
||||||
class ToastNotificationsManager : public QObject {
|
class ToastNotificationsManager : public QObject {
|
||||||
|
@ -22,7 +23,9 @@ class ToastNotificationsManager : public QObject {
|
||||||
BottomRight = 3
|
BottomRight = 3
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit ToastNotificationsManager(QObject* parent = nullptr);
|
explicit ToastNotificationsManager(ToastNotificationsManager::NotificationPosition position,
|
||||||
|
int screen,
|
||||||
|
QObject* parent = nullptr);
|
||||||
virtual ~ToastNotificationsManager();
|
virtual ~ToastNotificationsManager();
|
||||||
|
|
||||||
QList<BaseToastNotification*> activeNotifications() const;
|
QList<BaseToastNotification*> activeNotifications() const;
|
||||||
|
@ -41,7 +44,7 @@ class ToastNotificationsManager : public QObject {
|
||||||
void showNotification(const QList<Message>& new_messages);
|
void showNotification(const QList<Message>& new_messages);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void closeNotification(BaseToastNotification* notif);
|
void closeNotification(BaseToastNotification* notif, bool delete_from_memory);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QScreen* activeScreen() const;
|
QScreen* activeScreen() const;
|
||||||
|
@ -60,6 +63,8 @@ class ToastNotificationsManager : public QObject {
|
||||||
// List of all displayed notifications, newest notifications are in the beginning of the list
|
// List of all displayed notifications, newest notifications are in the beginning of the list
|
||||||
// and oldest at the end.
|
// and oldest at the end.
|
||||||
QList<BaseToastNotification*> m_activeNotifications;
|
QList<BaseToastNotification*> m_activeNotifications;
|
||||||
|
|
||||||
|
ArticleListNotification* m_articleListNotification;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // TOASTNOTIFICATIONSMANAGER_H
|
#endif // TOASTNOTIFICATIONSMANAGER_H
|
||||||
|
|
|
@ -18,6 +18,21 @@ SettingsNotifications::SettingsNotifications(Settings* settings, QWidget* parent
|
||||||
|
|
||||||
connect(m_ui.m_checkEnableNotifications, &QCheckBox::toggled, this, &SettingsNotifications::dirtifySettings);
|
connect(m_ui.m_checkEnableNotifications, &QCheckBox::toggled, this, &SettingsNotifications::dirtifySettings);
|
||||||
connect(m_ui.m_editor, &NotificationsEditor::someNotificationChanged, this, &SettingsNotifications::dirtifySettings);
|
connect(m_ui.m_editor, &NotificationsEditor::someNotificationChanged, this, &SettingsNotifications::dirtifySettings);
|
||||||
|
|
||||||
|
connect(m_ui.m_rbCustomNotifications, &QRadioButton::toggled, this, &SettingsNotifications::dirtifySettings);
|
||||||
|
connect(m_ui.m_rbCustomNotifications, &QRadioButton::toggled, this, &SettingsNotifications::requireRestart);
|
||||||
|
|
||||||
|
connect(m_ui.m_rbNativeNotifications, &QRadioButton::toggled, this, &SettingsNotifications::dirtifySettings);
|
||||||
|
connect(m_ui.m_rbNativeNotifications, &QRadioButton::toggled, this, &SettingsNotifications::requireRestart);
|
||||||
|
|
||||||
|
connect(m_ui.m_cbCustomNotificationsPosition,
|
||||||
|
&QComboBox::currentIndexChanged,
|
||||||
|
this,
|
||||||
|
&SettingsNotifications::dirtifySettings);
|
||||||
|
connect(m_ui.m_cbCustomNotificationsPosition,
|
||||||
|
&QComboBox::currentIndexChanged,
|
||||||
|
this,
|
||||||
|
&SettingsNotifications::requireRestart);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SettingsNotifications::loadSettings() {
|
void SettingsNotifications::loadSettings() {
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<item row="2" column="0" colspan="2">
|
<item row="2" column="0" colspan="2">
|
||||||
<widget class="QGroupBox" name="m_gbNotificationsType">
|
<widget class="QGroupBox" name="m_gbNotificationsType">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Notifications type</string>
|
<string>Balloon notifications type</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QFormLayout" name="formLayout_2">
|
<layout class="QFormLayout" name="formLayout_2">
|
||||||
<item row="0" column="0" colspan="2">
|
<item row="0" column="0" colspan="2">
|
||||||
|
@ -45,6 +45,9 @@
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Native notifications (tray icon must be enabled)</string>
|
<string>Native notifications (tray icon must be enabled)</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0" colspan="2">
|
<item row="1" column="0" colspan="2">
|
||||||
|
|
|
@ -110,7 +110,12 @@ Application::Application(const QString& id, int& argc, char** argv, const QStrin
|
||||||
m_database = new DatabaseFactory(this);
|
m_database = new DatabaseFactory(this);
|
||||||
m_downloadManager = nullptr;
|
m_downloadManager = nullptr;
|
||||||
m_notifications = new NotificationFactory(this);
|
m_notifications = new NotificationFactory(this);
|
||||||
m_toastNotifications = new ToastNotificationsManager(this);
|
m_toastNotifications =
|
||||||
|
new ToastNotificationsManager(settings()
|
||||||
|
->value(GROUP(GUI), SETTING(GUI::ToastNotificationsPosition))
|
||||||
|
.value<ToastNotificationsManager::NotificationPosition>(),
|
||||||
|
settings()->value(GROUP(GUI), SETTING(GUI::ToastNotificationsScreen)).toInt(),
|
||||||
|
this);
|
||||||
m_shouldRestart = false;
|
m_shouldRestart = false;
|
||||||
|
|
||||||
#if defined(Q_OS_WIN)
|
#if defined(Q_OS_WIN)
|
||||||
|
|
|
@ -255,6 +255,19 @@ void FeedReader::removeMessageFilterToFeedAssignment(Feed* feed, MessageFilter*
|
||||||
}
|
}
|
||||||
|
|
||||||
void FeedReader::updateAllFeeds() {
|
void FeedReader::updateAllFeeds() {
|
||||||
|
qApp
|
||||||
|
->showGuiMessage(Notification::Event::GeneralEvent,
|
||||||
|
GuiMessage(QDateTime::currentDateTime().toString(),
|
||||||
|
"Quisque ullamcorper ut purus nec tempus. Vivamus eros dolor, sagittis ultrices augue "
|
||||||
|
"ut, posuere fringilla lorem. Donec posuere, enim sit amet fermentum dignissim, tellus "
|
||||||
|
"lectus laoreet lectus, vestibulum laoreet felis tortor eget nunc. Curabitur sagittis "
|
||||||
|
"quam in scelerisque placerat. Vivamus vel porta tortor. Vivamus nec volutpat sem",
|
||||||
|
QSystemTrayIcon::MessageIcon::Information),
|
||||||
|
GuiMessageDestination(),
|
||||||
|
GuiAction("test", []() {
|
||||||
|
qDebugNN << "aa";
|
||||||
|
}));
|
||||||
|
|
||||||
updateFeeds(m_feedsModel->rootItem()->getSubTreeFeeds());
|
updateFeeds(m_feedsModel->rootItem()->getSubTreeFeeds());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -292,6 +292,9 @@ DKEY GUI::ToastNotificationsPosition = "toast_notifications_position";
|
||||||
DVALUE(ToastNotificationsManager::NotificationPosition)
|
DVALUE(ToastNotificationsManager::NotificationPosition)
|
||||||
GUI::ToastNotificationsPositionDef = ToastNotificationsManager::NotificationPosition::BottomRight;
|
GUI::ToastNotificationsPositionDef = ToastNotificationsManager::NotificationPosition::BottomRight;
|
||||||
|
|
||||||
|
DKEY GUI::ToastNotificationsScreen = "toast_notifications_screen";
|
||||||
|
DVALUE(int) GUI::ToastNotificationsScreenDef = -1;
|
||||||
|
|
||||||
DKEY GUI::HideMainWindowWhenMinimized = "hide_when_minimized";
|
DKEY GUI::HideMainWindowWhenMinimized = "hide_when_minimized";
|
||||||
DVALUE(bool) GUI::HideMainWindowWhenMinimizedDef = false;
|
DVALUE(bool) GUI::HideMainWindowWhenMinimizedDef = false;
|
||||||
|
|
||||||
|
|
|
@ -225,6 +225,9 @@ namespace GUI {
|
||||||
KEY ToastNotificationsPosition;
|
KEY ToastNotificationsPosition;
|
||||||
VALUE(ToastNotificationsManager::NotificationPosition) ToastNotificationsPositionDef;
|
VALUE(ToastNotificationsManager::NotificationPosition) ToastNotificationsPositionDef;
|
||||||
|
|
||||||
|
KEY ToastNotificationsScreen;
|
||||||
|
VALUE(int) ToastNotificationsScreenDef;
|
||||||
|
|
||||||
KEY MessageViewState;
|
KEY MessageViewState;
|
||||||
VALUE(QString) MessageViewStateDef;
|
VALUE(QString) MessageViewStateDef;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue