Compare commits

...

5 commits

Author SHA1 Message Date
BruebachL
b2cdf44bbd
[UserList] Show amount of online buddies (#7126)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* [UserList] Show amount of online buddies

Took 11 minutes

* [UserList] Replace early return with if-else in updateSectionDivider

RickyRister nit: the code is easier to follow with a standard if-else
branch instead of an early return for the Buddy section.

Took 1 minute

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-17 03:58:41 +02:00
BruebachL
60ee81cfbe
[Client] Route cockatrice:// link clicks from chat to the intent chain (#7136)
* [Client] Route cockatrice:// link clicks from chat to the intent chain

A cockatrice:// link clicked in chat is currently handed to the OS (or
does nothing in-process). Clicks now emit a cockatriceLinkActivated signal
that travels ChatView -> Tab -> TabSupervisor -> MainWindow, which feeds
the URL through the same IntentUrlParser the OS activation path uses, so
the join runs entirely in-process. card/user schemes and all other links
behave as before.

* [Client] Route cockatrice:// link clicks from the in-game chat to the intent chain

* [Client] Reuse one IntentUrlParser instance for cockatrice:// links

Took 59 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-17 01:03:37 +02:00
BruebachL
3de7882f0c
[UserList] Fix context menu crash by correctly parenting (#7145)
Took 5 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-17 00:53:43 +02:00
BruebachL
fe53f9c3eb
[Chat] Render game link buttons (#7135)
* [Chat] Render cockatrice://joingame links in chat as clickable buttons

Words starting with cockatrice:// become button-style anchors labelled with
the game description, id and server (falling back to id + server for links
built without a description). The description is spliced via the multi-arg
arg() overloads so a title containing "%…" cannot corrupt the label.

Keyboard link access is enabled so the anchors are reachable without a mouse.

* [Chat] Fix percent-encoding and scheme gating in game-link chat labels

Game descriptions containing '%' were rendered as '%25' in the chat
button label because QUrlQuery's default decode leaves %25 untouched.
Use QUrl::FullyDecoded for the description item, and restrict the
invite-button treatment to cockatrice://joingame links; any other
cockatrice:// scheme now falls through to plain text.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-17 00:26:33 +02:00
BruebachL
f466a25893
[Client] Keep the message draft and notify when the recipient is offline (#7142)
* [Client] Keep the message draft and notify when the recipient is offline

* Don't blindly assume a user is online.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-17 00:16:55 +02:00
13 changed files with 139 additions and 16 deletions

View file

@ -14,6 +14,8 @@
#include <QMouseEvent> #include <QMouseEvent>
#include <QScrollBar> #include <QScrollBar>
#include <QTimer> #include <QTimer>
#include <QUrl>
#include <QUrlQuery>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/server/remote/user_level.h> #include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/settings/chat_settings.h> #include <libcockatrice/settings/chat_settings.h>
@ -49,7 +51,7 @@ ChatView::ChatView(TabSupervisor *_tabSupervisor, AbstractGame *_game, bool _sho
viewport()->setCursor(Qt::IBeamCursor); viewport()->setCursor(Qt::IBeamCursor);
setReadOnly(true); setReadOnly(true);
setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse); setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard);
setOpenLinks(false); setOpenLinks(false);
connect(this, &ChatView::anchorClicked, this, &ChatView::openLink); connect(this, &ChatView::anchorClicked, this, &ChatView::openLink);
@ -219,6 +221,46 @@ void ChatView::appendUrlTag(QTextCursor &cursor, QString url)
cursor.setCharFormat(oldFormat); cursor.setCharFormat(oldFormat);
} }
void ChatView::appendGameLinkTag(QTextCursor &cursor, const QString &url)
{
const QUrl gameUrl(url);
const QUrlQuery query(gameUrl);
const QString hostname = query.queryItemValue("hostname");
// FullyDecoded undoes every %XX escape, so a description that itself
// contains "%" cannot end up displayed as "%25" in the label.
const QString description = query.queryItemValue("game", QUrl::FullyDecoded);
const int gameId = query.queryItemValue("gameid").toInt();
QString label;
if (gameId > 0 && !hostname.isEmpty()) {
// Links built before the description was embedded stay readable: the
// id + server fallback below is identical to the old anchor text.
if (!description.isEmpty()) {
// Multi-arg .arg() replaces all placeholders in a single pass, so a
// description containing "%…" cannot corrupt later placeholders.
label = tr("Join game \"%1\" (#%2) on %3").arg(description, QString::number(gameId), hostname);
} else {
label = tr("Join game #%1 on %2").arg(QString::number(gameId), hostname);
}
} else {
label = tr("Join game");
}
QTextCharFormat oldFormat = cursor.charFormat();
QTextCharFormat gameLinkFormat = oldFormat;
gameLinkFormat.setForeground(linkColor);
gameLinkFormat.setFontWeight(QFont::Bold);
gameLinkFormat.setAnchor(true);
gameLinkFormat.setAnchorHref(url);
QColor background = palette().highlight().color();
background.setAlpha(40);
gameLinkFormat.setBackground(background);
cursor.setCharFormat(gameLinkFormat);
cursor.insertText(label);
cursor.setCharFormat(oldFormat);
}
void ChatView::appendMessage(QString message, void ChatView::appendMessage(QString message,
RoomMessageTypeFlags messageType, RoomMessageTypeFlags messageType,
const ServerInfo_User &userInfo, const ServerInfo_User &userInfo,
@ -503,6 +545,17 @@ void ChatView::checkWord(QTextCursor &cursor, QString &message)
} }
} }
if (fullWordUpToSpaceOrEnd.startsWith("cockatrice://", Qt::CaseInsensitive)) {
// Only links to a game (cockatrice://joingame) become invite buttons;
// any other cockatrice:// scheme falls through to plain text below.
const QUrl gameLink(fullWordUpToSpaceOrEnd);
if (gameLink.host().compare("joingame", Qt::CaseInsensitive) == 0) {
appendGameLinkTag(cursor, fullWordUpToSpaceOrEnd);
cursor.insertText(rest, defaultFormat);
return;
}
}
// check word mentions // check word mentions
for (const QString &word : highlightedWords) { for (const QString &word : highlightedWords) {
if (fullWordUpToSpaceOrEnd.compare(word, Qt::CaseInsensitive) == 0) { if (fullWordUpToSpaceOrEnd.compare(word, Qt::CaseInsensitive) == 0) {
@ -724,6 +777,11 @@ void ChatView::mouseReleaseEvent(QMouseEvent *event)
void ChatView::openLink(const QUrl &link) void ChatView::openLink(const QUrl &link)
{ {
if (link.scheme() == "cockatrice") {
emit cockatriceLinkActivated(link.toString(QUrl::FullyEncoded));
return;
}
if ((link.scheme() == "card") || (link.scheme() == "user")) { if ((link.scheme() == "card") || (link.scheme() == "user")) {
return; return;
} }

View file

@ -71,6 +71,7 @@ private:
void scrollToBottom(); void scrollToBottom();
void appendCardTag(QTextCursor &cursor, const QString &cardName); void appendCardTag(QTextCursor &cursor, const QString &cardName);
void appendUrlTag(QTextCursor &cursor, QString url); void appendUrlTag(QTextCursor &cursor, QString url);
void appendGameLinkTag(QTextCursor &cursor, const QString &url);
static QColor getCustomMentionColor(); static QColor getCustomMentionColor();
static QColor getCustomHighlightColor(); static QColor getCustomHighlightColor();
void showSystemPopup(const QString &userName); void showSystemPopup(const QString &userName);
@ -121,6 +122,7 @@ signals:
void addMentionTag(QString mentionTag); void addMentionTag(QString mentionTag);
void messageClickedSignal(); void messageClickedSignal();
void showMentionPopup(const QString &userName); void showMentionPopup(const QString &userName);
void cockatriceLinkActivated(const QString &url);
}; };
#endif #endif

View file

@ -336,11 +336,12 @@ constexpr int UserInfo = Qt::UserRole + 2;
// rows (UserListTWI, which uses QTreeWidgetItem::Type) by this item type. // rows (UserListTWI, which uses QTreeWidgetItem::Type) by this item type.
constexpr int SectionItemType = QTreeWidgetItem::UserType + 1; constexpr int SectionItemType = QTreeWidgetItem::UserType + 1;
UserListItemDelegate::UserListItemDelegate(QTreeWidget *tree, UserListItemDelegate::UserListItemDelegate(UserListWidget *owner,
QTreeWidget *tree,
const QMap<QString, QPixmap> *avatarCache, const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache, const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap) const QMap<QString, CardArtParams> *cardArtParamsMap)
: QStyledItemDelegate(tree), tree(tree), avatarCache(avatarCache), cardArtCache(cardArtCache), : QStyledItemDelegate(tree), tree(tree), owner(owner), avatarCache(avatarCache), cardArtCache(cardArtCache),
cardArtParamsMap(cardArtParamsMap) cardArtParamsMap(cardArtParamsMap)
{ {
} }
@ -353,7 +354,7 @@ bool UserListItemDelegate::editorEvent(QEvent *event,
if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) { if ((event->type() == QEvent::MouseButtonPress) && index.isValid()) {
QMouseEvent *const mouseEvent = static_cast<QMouseEvent *>(event); QMouseEvent *const mouseEvent = static_cast<QMouseEvent *>(event);
if (mouseEvent->button() == Qt::RightButton) { if (mouseEvent->button() == Qt::RightButton) {
static_cast<UserListWidget *>(parent())->showContextMenu(mouseEvent->globalPosition().toPoint(), index); owner->showContextMenu(mouseEvent->globalPosition().toPoint(), index);
return true; return true;
} }
} }
@ -593,8 +594,8 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
userTree->setHeaderHidden(true); userTree->setHeaderHidden(true);
userTree->setRootIsDecorated(false); userTree->setRootIsDecorated(false);
userTree->setIconSize(QSize(20, 18)); userTree->setIconSize(QSize(20, 18));
itemDelegate = itemDelegate = new UserListItemDelegate(this, userTree, &avatarProvider->cache(), &cardArtProvider->cache(),
new UserListItemDelegate(userTree, &avatarProvider->cache(), &cardArtProvider->cache(), &cardArtParamsMap); &cardArtParamsMap);
userTree->setItemDelegate(itemDelegate); userTree->setItemDelegate(itemDelegate);
userTree->setAlternatingRowColors(true); userTree->setAlternatingRowColors(true);
userTree->hideColumn(1); userTree->hideColumn(1);
@ -1638,15 +1639,27 @@ void UserListWidget::updateSectionDivider(Section section)
return; return;
} }
int visible = 0; int visible = 0;
int online = 0;
for (int i = 0; i < divider->childCount(); ++i) { for (int i = 0; i < divider->childCount(); ++i) {
if (!divider->child(i)->isHidden()) { QTreeWidgetItem *child = divider->child(i);
if (!child->isHidden()) {
++visible; ++visible;
if (child->data(0, UserListRoles::Online).toBool()) {
++online;
}
} }
} }
// The tree draws no branches (rows are flush), so the divider carries its // The tree draws no branches (rows are flush), so the divider carries its
// own collapse arrow glyph. // own collapse arrow glyph.
const QString arrow = divider->isExpanded() ? QStringLiteral("\u25BE") : QStringLiteral("\u25B8"); const QString arrow = divider->isExpanded() ? QStringLiteral("\u25BE") : QStringLiteral("\u25B8");
if (section == Section::Buddy) {
// The buddy divider reports how many of the shown buddies are online,
// mirroring the "Buddies online: %1 / %2" title of the non-sectioned
// buddy list.
divider->setText(0, tr("%1 %2 (%3/%4)").arg(arrow, sectionTitle(section)).arg(online).arg(visible));
} else {
divider->setText(0, tr("%1 %2 (%3)").arg(arrow, sectionTitle(section)).arg(visible)); divider->setText(0, tr("%1 %2 (%3)").arg(arrow, sectionTitle(section)).arg(visible));
}
} }
void UserListWidget::handleSectionExpansion(QTreeWidgetItem *item, bool expanded) void UserListWidget::handleSectionExpansion(QTreeWidgetItem *item, bool expanded)

View file

@ -37,6 +37,7 @@ class QPlainTextEdit;
class Response; class Response;
class CommandContainer; class CommandContainer;
class UserContextMenu; class UserContextMenu;
class UserListWidget;
class QShowEvent; class QShowEvent;
class BanDialog : public QDialog class BanDialog : public QDialog
@ -105,12 +106,14 @@ public:
class UserListItemDelegate : public QStyledItemDelegate class UserListItemDelegate : public QStyledItemDelegate
{ {
QTreeWidget *tree; QTreeWidget *tree;
UserListWidget *owner;
const QMap<QString, QPixmap> *avatarCache; const QMap<QString, QPixmap> *avatarCache;
const QMap<QString, QPixmap> *cardArtCache; const QMap<QString, QPixmap> *cardArtCache;
const QMap<QString, CardArtParams> *cardArtParamsMap; const QMap<QString, CardArtParams> *cardArtParamsMap;
public: public:
explicit UserListItemDelegate(QTreeWidget *tree, explicit UserListItemDelegate(UserListWidget *owner,
QTreeWidget *tree,
const QMap<QString, QPixmap> *avatarCache, const QMap<QString, QPixmap> *avatarCache,
const QMap<QString, QPixmap> *cardArtCache, const QMap<QString, QPixmap> *cardArtCache,
const QMap<QString, CardArtParams> *cardArtParamsMap); const QMap<QString, CardArtParams> *cardArtParamsMap);

View file

@ -20,6 +20,7 @@ class Tab : public QMainWindow
signals: signals:
void userEvent(bool globalEvent = true); void userEvent(bool globalEvent = true);
void tabTextChanged(Tab *tab, const QString &newTabText); void tabTextChanged(Tab *tab, const QString &newTabText);
void cockatriceLinkActivated(const QString &url);
protected: protected:
TabSupervisor *tabSupervisor; TabSupervisor *tabSupervisor;

View file

@ -1292,6 +1292,7 @@ void TabGame::createMessageDock(bool bReplay)
qOverload<const QString &>(&CardInfoFrameWidget::setCard)); qOverload<const QString &>(&CardInfoFrameWidget::setCard));
connect(messageLog, &MessageLogWidget::showCardInfoPopup, this, &TabGame::showCardInfoPopup); connect(messageLog, &MessageLogWidget::showCardInfoPopup, this, &TabGame::showCardInfoPopup);
connect(messageLog, &MessageLogWidget::deleteCardInfoPopup, this, &TabGame::deleteCardInfoPopup); connect(messageLog, &MessageLogWidget::deleteCardInfoPopup, this, &TabGame::deleteCardInfoPopup);
connect(messageLog, &MessageLogWidget::cockatriceLinkActivated, this, &TabGame::cockatriceLinkActivated);
if (!bReplay) { if (!bReplay) {
connect(messageLog, &MessageLogWidget::openMessageDialog, this, &TabGame::openMessageDialog); connect(messageLog, &MessageLogWidget::openMessageDialog, this, &TabGame::openMessageDialog);

View file

@ -23,14 +23,16 @@
TabMessage::TabMessage(TabSupervisor *_tabSupervisor, TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
AbstractClient *_client, AbstractClient *_client,
const ServerInfo_User &_ownUserInfo, const ServerInfo_User &_ownUserInfo,
const ServerInfo_User &_otherUserInfo) const ServerInfo_User &_otherUserInfo,
bool _userOnline)
: Tab(_tabSupervisor), client(_client), ownUserInfo(new ServerInfo_User(_ownUserInfo)), : Tab(_tabSupervisor), client(_client), ownUserInfo(new ServerInfo_User(_ownUserInfo)),
otherUserInfo(new ServerInfo_User(_otherUserInfo)), userOnline(true) otherUserInfo(new ServerInfo_User(_otherUserInfo)), userOnline(_userOnline)
{ {
chatView = new ChatView(tabSupervisor, 0, true); chatView = new ChatView(tabSupervisor, 0, true);
connect(chatView, &ChatView::showCardInfoPopup, this, &TabMessage::showCardInfoPopup); connect(chatView, &ChatView::showCardInfoPopup, this, &TabMessage::showCardInfoPopup);
connect(chatView, &ChatView::deleteCardInfoPopup, this, &TabMessage::deleteCardInfoPopup); connect(chatView, &ChatView::deleteCardInfoPopup, this, &TabMessage::deleteCardInfoPopup);
connect(chatView, &ChatView::addMentionTag, this, &TabMessage::addMentionTag); connect(chatView, &ChatView::addMentionTag, this, &TabMessage::addMentionTag);
connect(chatView, &ChatView::cockatriceLinkActivated, this, &TabMessage::cockatriceLinkActivated);
sayEdit = new LineEditUnfocusable; sayEdit = new LineEditUnfocusable;
sayEdit->setMaxLength(MAX_TEXT_LENGTH); sayEdit->setMaxLength(MAX_TEXT_LENGTH);
connect(sayEdit, &LineEditUnfocusable::returnPressed, this, &TabMessage::sendMessage); connect(sayEdit, &LineEditUnfocusable::returnPressed, this, &TabMessage::sendMessage);
@ -96,7 +98,14 @@ void TabMessage::closeEvent(QCloseEvent *event)
void TabMessage::sendMessage() void TabMessage::sendMessage()
{ {
if (sayEdit->text().isEmpty() || !userOnline) { if (sayEdit->text().isEmpty()) {
return;
}
if (!userOnline) {
// Keep the draft: the user may be back momentarily, and the typed text
// should not be lost to a transient offline spell.
notifyUserOffline();
return; return;
} }
@ -105,17 +114,27 @@ void TabMessage::sendMessage()
cmd.set_message(sayEdit->text().toStdString()); cmd.set_message(sayEdit->text().toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd); PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(sayEdit->text());
connect(pend, &PendingCommand::finished, this, &TabMessage::messageSent); connect(pend, &PendingCommand::finished, this, &TabMessage::messageSent);
client->sendCommand(pend); client->sendCommand(pend);
sayEdit->clear(); sayEdit->clear();
} }
void TabMessage::messageSent(const Response &response) void TabMessage::messageSent(const Response &response,
const CommandContainer & /*commandContainer*/,
const QVariant &extraData)
{ {
if (response.response_code() == Response::RespInIgnoreList) { if (response.response_code() == Response::RespInIgnoreList) {
chatView->appendMessage(tr( chatView->appendMessage(tr(
"This user is ignoring you, they cannot see your messages in main chat and you cannot join their games.")); "This user is ignoring you, they cannot see your messages in main chat and you cannot join their games."));
} else if (response.response_code() == Response::RespNameNotFound) {
// The recipient went offline before the command reached the server: restore the draft.
userOnline = false;
if (sayEdit->text().isEmpty()) {
sayEdit->setText(extraData.toString());
}
notifyUserOffline();
} }
} }
@ -175,3 +194,8 @@ void TabMessage::processUserJoined(const ServerInfo_User &_userInfo)
userOnline = true; userOnline = true;
*otherUserInfo = _userInfo; *otherUserInfo = _userInfo;
} }
void TabMessage::notifyUserOffline()
{
chatView->appendMessage(tr("Message not sent — %1 is offline.").arg(QString::fromStdString(otherUserInfo->name())));
}

View file

@ -19,6 +19,7 @@ class LineEditUnfocusable;
class Event_UserMessage; class Event_UserMessage;
class Response; class Response;
class ServerInfo_User; class ServerInfo_User;
class CommandContainer;
class TabMessage : public Tab class TabMessage : public Tab
{ {
@ -39,7 +40,7 @@ signals:
void maximizeClient(); void maximizeClient();
private slots: private slots:
void sendMessage(); void sendMessage();
void messageSent(const Response &response); void messageSent(const Response &response, const CommandContainer &commandContainer, const QVariant &extraData);
void addMentionTag(QString mentionTag); void addMentionTag(QString mentionTag);
void messageClicked(); void messageClicked();
@ -50,7 +51,8 @@ public:
TabMessage(TabSupervisor *_tabSupervisor, TabMessage(TabSupervisor *_tabSupervisor,
AbstractClient *_client, AbstractClient *_client,
const ServerInfo_User &_ownUserInfo, const ServerInfo_User &_ownUserInfo,
const ServerInfo_User &_otherUserInfo); const ServerInfo_User &_otherUserInfo,
bool _userOnline);
~TabMessage() override; ~TabMessage() override;
void retranslateUi() override; void retranslateUi() override;
void tabActivated() override; void tabActivated() override;
@ -65,6 +67,7 @@ public:
private: private:
bool shouldShowSystemPopup(const Event_UserMessage &event); bool shouldShowSystemPopup(const Event_UserMessage &event);
void showSystemPopup(const Event_UserMessage &event); void showSystemPopup(const Event_UserMessage &event);
void notifyUserOffline();
}; };
#endif #endif

View file

@ -70,6 +70,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup); connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup);
connect(chatView, &ChatView::messageClickedSignal, this, &TabRoom::focusTab); connect(chatView, &ChatView::messageClickedSignal, this, &TabRoom::focusTab);
connect(chatView, &ChatView::openMessageDialog, this, &TabRoom::openMessageDialog); connect(chatView, &ChatView::openMessageDialog, this, &TabRoom::openMessageDialog);
connect(chatView, &ChatView::cockatriceLinkActivated, this, &TabRoom::cockatriceLinkActivated);
connect(chatView, &ChatView::showCardInfoPopup, this, &TabRoom::showCardInfoPopup); connect(chatView, &ChatView::showCardInfoPopup, this, &TabRoom::showCardInfoPopup);
connect(chatView, &ChatView::deleteCardInfoPopup, this, &TabRoom::deleteCardInfoPopup); connect(chatView, &ChatView::deleteCardInfoPopup, this, &TabRoom::deleteCardInfoPopup);
connect(chatView, &ChatView::addMentionTag, this, &TabRoom::addMentionTag); connect(chatView, &ChatView::addMentionTag, this, &TabRoom::addMentionTag);

View file

@ -406,6 +406,7 @@ int TabSupervisor::myAddTab(Tab *tab, QAction *manager)
{ {
connect(tab, &TabGame::userEvent, this, &TabSupervisor::tabUserEvent); connect(tab, &TabGame::userEvent, this, &TabSupervisor::tabUserEvent);
connect(tab, &TabGame::tabTextChanged, this, &TabSupervisor::updateTabText); connect(tab, &TabGame::tabTextChanged, this, &TabSupervisor::updateTabText);
connect(tab, &TabGame::cockatriceLinkActivated, this, &TabSupervisor::cockatriceLinkActivated);
QString tabText = tab->getTabText(); QString tabText = tab->getTabText();
int idx = addTab(tab, sanitizeTabName(tabText)); int idx = addTab(tab, sanitizeTabName(tabText));
@ -851,6 +852,7 @@ void TabSupervisor::addRoomTab(const ServerInfo_Room &info, bool setCurrent)
connect(tab, &TabRoom::maximizeClient, this, &TabSupervisor::maximizeMainWindow); connect(tab, &TabRoom::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
connect(tab, &TabRoom::roomClosing, this, &TabSupervisor::roomLeft); connect(tab, &TabRoom::roomClosing, this, &TabSupervisor::roomLeft);
connect(tab, &TabRoom::openMessageDialog, this, &TabSupervisor::addMessageTab); connect(tab, &TabRoom::openMessageDialog, this, &TabSupervisor::addMessageTab);
connect(tab, &TabRoom::cockatriceLinkActivated, this, &TabSupervisor::cockatriceLinkActivated);
myAddTab(tab); myAddTab(tab);
roomTabs.insert(info.room_id(), tab); roomTabs.insert(info.room_id(), tab);
if (setCurrent) { if (setCurrent) {
@ -904,8 +906,10 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
} }
ServerInfo_User otherUser; ServerInfo_User otherUser;
bool userOnline = false;
if (auto user = userListManager->getOnlineUser(receiverName)) { if (auto user = userListManager->getOnlineUser(receiverName)) {
otherUser = ServerInfo_User(*user); otherUser = ServerInfo_User(*user);
userOnline = true;
} else { } else {
otherUser.set_name(receiverName.toStdString()); otherUser.set_name(receiverName.toStdString());
} }
@ -919,9 +923,10 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
return tab; return tab;
} }
tab = new TabMessage(this, client, *userInfo, otherUser); tab = new TabMessage(this, client, *userInfo, otherUser, userOnline);
connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft); connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft);
connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow); connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
connect(tab, &TabMessage::cockatriceLinkActivated, this, &TabSupervisor::cockatriceLinkActivated);
myAddTab(tab); myAddTab(tab);
messageTabs.insert(receiverName, tab); messageTabs.insert(receiverName, tab);
if (focus) { if (focus) {

View file

@ -169,6 +169,7 @@ signals:
void localGameEnded(); void localGameEnded();
void adminLockChanged(bool lock); void adminLockChanged(bool lock);
void showWindowIfHidden(); void showWindowIfHidden();
void cockatriceLinkActivated(const QString &url);
public slots: public slots:
void openDeckInNewTab(const LoadedDeck &deckToOpen); void openDeckInNewTab(const LoadedDeck &deckToOpen);

View file

@ -40,6 +40,7 @@
#include "intents/intent_connect_to_server.h" #include "intents/intent_connect_to_server.h"
#include "intents/intent_login.h" #include "intents/intent_login.h"
#include "intents/intent_open_server_room_by_name.h" #include "intents/intent_open_server_room_by_name.h"
#include "intents/url_parser.h"
#include "logger.h" #include "logger.h"
#include "version_string.h" #include "version_string.h"
#include "widgets/dialogs/dlg_connect.h" #include "widgets/dialogs/dlg_connect.h"
@ -497,6 +498,7 @@ MainWindow::MainWindow(QWidget *parent)
pixmapCacheSizeChanged(SettingsCache::instance().cacheStorage().getPixmapCacheSize()); pixmapCacheSizeChanged(SettingsCache::instance().cacheStorage().getPixmapCacheSize());
connectionController = new ConnectionController(this, this); connectionController = new ConnectionController(this, this);
urlParser = new IntentUrlParser(this, this);
createActions(); createActions();
createMenus(); createMenus();
@ -508,6 +510,7 @@ MainWindow::MainWindow(QWidget *parent)
connect(tabSupervisor, &TabSupervisor::setMenu, this, &MainWindow::updateTabMenu); connect(tabSupervisor, &TabSupervisor::setMenu, this, &MainWindow::updateTabMenu);
connect(tabSupervisor, &TabSupervisor::localGameEnded, this, &MainWindow::localGameEnded); connect(tabSupervisor, &TabSupervisor::localGameEnded, this, &MainWindow::localGameEnded);
connect(tabSupervisor, &TabSupervisor::showWindowIfHidden, this, &MainWindow::showWindowIfHidden); connect(tabSupervisor, &TabSupervisor::showWindowIfHidden, this, &MainWindow::showWindowIfHidden);
connect(tabSupervisor, &TabSupervisor::cockatriceLinkActivated, this, &MainWindow::handleCockatriceLink);
connect(connectionController, &ConnectionController::tabSupervisorStartRequested, tabSupervisor, connect(connectionController, &ConnectionController::tabSupervisorStartRequested, tabSupervisor,
&TabSupervisor::start); &TabSupervisor::start);
connect(connectionController, &ConnectionController::tabSupervisorStopRequested, tabSupervisor, connect(connectionController, &ConnectionController::tabSupervisorStopRequested, tabSupervisor,
@ -861,6 +864,11 @@ void MainWindow::showWindowIfHidden()
show(); show();
} }
void MainWindow::handleCockatriceLink(const QString &url)
{
urlParser->handle(url);
}
void MainWindow::cardDatabaseLoadingFailed() void MainWindow::cardDatabaseLoadingFailed()
{ {
if (askedForDbUpdater) { if (askedForDbUpdater) {

View file

@ -56,6 +56,7 @@ class TabSupervisor;
class WndSets; class WndSets;
class DlgTipOfTheDay; class DlgTipOfTheDay;
struct ContextConnectToServer; struct ContextConnectToServer;
class IntentUrlParser;
class MainWindow : public QMainWindow class MainWindow : public QMainWindow
{ {
@ -84,6 +85,7 @@ private slots:
void actOpenSettingsFolder(); void actOpenSettingsFolder();
void actShow(); void actShow();
void showWindowIfHidden(); void showWindowIfHidden();
void handleCockatriceLink(const QString &url);
void cardUpdateError(QProcess::ProcessError err); void cardUpdateError(QProcess::ProcessError err);
void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus); void cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus);
@ -139,6 +141,7 @@ private:
*aOpenSettingsFolder; *aOpenSettingsFolder;
TabSupervisor *tabSupervisor; TabSupervisor *tabSupervisor;
IntentUrlParser *urlParser;
WndSets *wndSets; WndSets *wndSets;
ConnectionController *connectionController; ConnectionController *connectionController;
LocalServer *localServer; LocalServer *localServer;