js8call/JS8_UI/MessagePanel.cpp
Chris-AC9KH d37a71fd8b update logic in markMessageRead() function to not rely on a selection
change to clear the unread flag. This deals with an edge case where
there is only MSG in the inbox and it wouldn't clear the unread flag
until a second MSG came in
2026-04-06 07:52:01 -05:00

350 lines
10 KiB
C++

#include "MessagePanel.h"
#include "JS8_Include/EventFilter.h"
#include "JS8_Main/Radio.h"
#include "JS8_Widgets/DateTableWidgetItem.h"
#include "JS8_Widgets/SemiSortableHeader.h"
#include "ui_MessagePanel.h"
#include <QDateTime>
#include <QMenu>
#include <QTimeZone>
#include <algorithm>
#include "moc_MessagePanel.cpp"
namespace {
auto pathSegs(QString const &path) {
auto segs = path.split('>');
std::reverse(segs.begin(), segs.end());
return segs;
}
} // namespace
MessagePanel::MessagePanel(QString inboxPath, QWidget *parent)
: QWidget(parent), ui(new Ui::MessagePanel), inbox(new Inbox(inboxPath)) {
this->call = "%";
inbox->open();
ui->setupUi(this);
// connect selection model changed
connect(ui->messageTableWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this,
&MessagePanel::messageTableSelectionChanged);
// reply when key pressed in the reply box
ui->replytextEdit->installEventFilter(new EventFilter::EnterKeyPress(
[this](QKeyEvent *const event) {
if (event->modifiers() & Qt::ShiftModifier)
return false;
ui->replyPushButton->click();
return true;
},
this));
ui->messageTableWidget->setContextMenuPolicy(Qt::ActionsContextMenu);
auto deleteAction = new QAction("Delete", ui->messageTableWidget);
deleteAction->setShortcut(QKeySequence(Qt::Key_Delete));
deleteAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
connect(deleteAction, &QAction::triggered, this,
[this]() { deleteSelectedMessages(); });
ui->messageTableWidget->addAction(deleteAction);
auto *table = ui->messageTableWidget;
auto *hdr = new SemiSortableHeader(Qt::Horizontal, table);
hdr->addNonSortableColumn(6);
hdr->addNonSortableColumn(0);
hdr->setSectionResizeMode(QHeaderView::Interactive);
hdr->setStretchLastSection(true);
hdr->attachTo(table);
// Now that the custom header is installed; indicator state will stick
{
QSignalBlocker b(hdr);
hdr->setSortIndicator(2, Qt::DescendingOrder);
}
refresh();
}
MessagePanel::~MessagePanel() { delete ui; }
void MessagePanel::setCall(const QString &call) {
this->call = call;
setWindowTitle(QString("Messages: %1").arg(call == "%" ? "All" : call));
if (!inbox)
return;
refresh();
}
void MessagePanel::refresh() {
QList<QPair<int, Message>> msgs = inbox->fetchForCall(this->call);
populateMessages(msgs);
emit countsUpdated();
}
void MessagePanel::populateMessages(QList<QPair<int, Message>> msgs) {
SemiSortableHeader *hdr = static_cast<SemiSortableHeader *>(
ui->messageTableWidget->horizontalHeader());
// Remember sort state (avoid sorting by unread flag column 0)
const int rememberedCol = hdr->sortIndicatorSection() >= 0 ? hdr->sortIndicatorSection() : 2;
const int sortCol = (rememberedCol == 0 ? 2 : rememberedCol);
const Qt::SortOrder sortOrder = hdr->sortIndicatorOrder();
// Freeze behavior while populating
ui->messageTableWidget->setUpdatesEnabled(false);
const bool wasSorting = ui->messageTableWidget->isSortingEnabled();
ui->messageTableWidget->setSortingEnabled(false);
for (int i = ui->messageTableWidget->rowCount(); i >= 0; i--) {
ui->messageTableWidget->removeRow(i);
}
ui->messageTableWidget->setUpdatesEnabled(false);
{
foreach (auto pair, msgs) {
auto mid = pair.first;
auto msg = pair.second;
auto params = msg.params();
int row = ui->messageTableWidget->rowCount();
ui->messageTableWidget->insertRow(row);
int col = 0;
auto typeItem =
new QTableWidgetItem(msg.type() == "UNREAD" ? "\u2691" : "");
typeItem->setData(Qt::UserRole, msg.type());
typeItem->setTextAlignment(Qt::AlignCenter);
ui->messageTableWidget->setItem(row, col++, typeItem);
auto midItem = new QTableWidgetItem();
midItem->setData(Qt::EditRole, mid);
midItem->setData(Qt::DisplayRole, QString::number(mid));
midItem->setTextAlignment(Qt::AlignCenter);
ui->messageTableWidget->setItem(row, col++, midItem);
const auto dateStr = params.value("UTC").toString();
QDateTime ts = QDateTime::fromString(dateStr, "yyyy-MM-dd HH:mm:ss");
ts.setTimeZone(QTimeZone::utc());
auto *dateItem = new DateItem(ts.toString("ddd MMM d HH:mm:ss yyyy"));
dateItem->setData(Qt::UserRole, ts.toSecsSinceEpoch()); // sort key
dateItem->setTextAlignment(Qt::AlignCenter);
ui->messageTableWidget->setItem(row, col++, dateItem);
auto dial = (quint64)params.value("DIAL").toInt();
auto dialItem = new QTableWidgetItem();
dialItem->setData(Qt::EditRole, dial);
dialItem->setData(Qt::DisplayRole, QString("%1 MHz").arg(Radio::pretty_frequency_MHz_string(dial)));
dialItem->setTextAlignment(Qt::AlignCenter);
ui->messageTableWidget->setItem(row, col++, dialItem);
auto path = params.value("PATH").toString();
auto segs = pathSegs(path);
auto fromItem = new QTableWidgetItem(segs.join(" via "));
fromItem->setData(Qt::UserRole, path);
fromItem->setTextAlignment(Qt::AlignCenter);
ui->messageTableWidget->setItem(row, col++, fromItem);
auto to = params.value("TO").toString();
auto toItem = new QTableWidgetItem(to);
toItem->setData(Qt::UserRole, to);
toItem->setTextAlignment(Qt::AlignCenter);
ui->messageTableWidget->setItem(row, col++, toItem);
auto text = params.value("TEXT").toString();
auto textItem = new QTableWidgetItem(text);
textItem->setData(Qt::UserRole, text);
textItem->setTextAlignment(Qt::AlignVCenter);
ui->messageTableWidget->setItem(row, col++, textItem);
}
ui->messageTableWidget->resizeColumnToContents(0);
ui->messageTableWidget->resizeColumnToContents(1);
ui->messageTableWidget->resizeColumnToContents(2);
ui->messageTableWidget->resizeColumnToContents(3);
ui->messageTableWidget->resizeColumnToContents(4);
ui->messageTableWidget->resizeColumnToContents(5);
}
ui->messageTableWidget->setUpdatesEnabled(true);
// Unfreeze + restore sort once, at the end
ui->messageTableWidget->setSortingEnabled(wasSorting); // or true if you want arrows always
ui->messageTableWidget->setUpdatesEnabled(true);
if (hdr) {
QSignalBlocker b(hdr);
hdr->setSortIndicator(sortCol, sortOrder);
}
if (wasSorting) {
ui->messageTableWidget->sortItems(sortCol, sortOrder);
}
ui->messageTableWidget->viewport()->update();
emit countsUpdated();
}
void MessagePanel::deleteSelectedMessages() {
auto items = ui->messageTableWidget->selectedItems();
if (items.isEmpty()) {
return;
}
// Collect unique rows and their message IDs
QMap<int, int> rowsToDelete; // row -> message id
for (auto item : items) {
int row = item->row();
if (rowsToDelete.contains(row)) {
continue;
}
auto col = ui->messageTableWidget->item(row, 1);
if (!col) {
continue;
}
bool ok = false;
auto mid = col->data(Qt::EditRole).toInt(&ok);
if (!ok) {
continue;
}
rowsToDelete.insert(row, mid);
}
// Delete rows in reverse order to avoid index shifting
auto rows = rowsToDelete.keys();
std::sort(rows.begin(), rows.end(), std::greater<int>());
for (int row : rows) {
ui->messageTableWidget->removeRow(row);
deleteMessage(rowsToDelete[row]);
}
emit countsUpdated();
}
void MessagePanel::deleteMessage(int id) {
if (!inbox->open()) {
return;
}
inbox->del(id);
}
void MessagePanel::markMessageRead(int id) {
if (!inbox->open()) {
return;
}
auto msg = inbox->value(id);
if (msg.type() == "UNREAD") {
msg.setType("READ");
inbox->set(id, msg);
// Also clear the unread flag in the table UI for this message id
auto *table = ui->messageTableWidget;
if (table) {
for (int row = 0; row < table->rowCount(); ++row) {
auto *idItem = table->item(row, 1); // column 1 holds the message id
if (!idItem)
continue;
bool okId = false;
int midInRow = idItem->data(Qt::EditRole).toInt(&okId);
if (!okId)
continue;
if (midInRow == id) {
auto *flagItem = table->item(row, 0); // column 0 is the unread flag
if (flagItem) {
flagItem->setText("");
flagItem->setData(Qt::UserRole, "");
}
break;
}
}
}
emit countsUpdated();
}
}
QString MessagePanel::prepareReplyMessage(QString path, QString text) {
return QString("%1 MSG %2").arg(path).arg(text);
}
void MessagePanel::messageTableSelectionChanged(
const QItemSelection &selected,
const QItemSelection &/*deselected*/) {
auto items = ui->messageTableWidget->selectedItems();
if (items.isEmpty() || items.size() > ui->messageTableWidget->columnCount()) {
ui->messageTextEdit->clear();
return;
}
auto firstItem = items.first();
auto row = firstItem->row();
// message column
auto item = ui->messageTableWidget->item(
row, ui->messageTableWidget->columnCount() - 1);
if (!item) {
return;
}
auto text = item->data(Qt::UserRole).toString();
ui->messageTextEdit->setPlainText(text);
// Mark deselected as read in table
auto selectedItemIndexes = selected.indexes();
if (selectedItemIndexes.empty()) {
return;
}
auto selectedRowIndex = selectedItemIndexes.first();
auto selectedRow = selectedRowIndex.row();
auto readFlagItem = ui->messageTableWidget->item(selectedRow, 0);
readFlagItem->setText("");
readFlagItem->setData(Qt::UserRole, "");
// Mark message read in DB
auto col = ui->messageTableWidget->item(selectedRow, 1);
if (!col) {
return;
}
bool ok = false;
auto mid = col->data(Qt::EditRole).toInt(&ok);
if (!ok) {
return;
}
markMessageRead(mid);
}
void MessagePanel::on_replyPushButton_clicked() {
auto row = ui->messageTableWidget->currentRow();
// from column
auto item = ui->messageTableWidget->item(
row, ui->messageTableWidget->columnCount() - 3);
if (!item) {
return;
}
auto path = item->data(Qt::UserRole).toString();
auto text = "[MESSAGE]";
auto message = prepareReplyMessage(path, text);
emit replyMessage(message);
}