diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp index 9494155af..6128a7830 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.cpp @@ -118,6 +118,26 @@ MessagesSettingsPage::MessagesSettingsPage() highlightGroupBox = new QGroupBox; highlightGroupBox->setLayout(highlightNotice); + channelUnreadColor = new QLineEdit(); + channelUnreadColor->setText(SettingsCache::instance().chat().getChannelUnreadColor()); + updateChannelUnreadPreview(); + connect(channelUnreadColor, &QLineEdit::textChanged, this, &MessagesSettingsPage::updateChannelUnreadColor); + + channelMentionColor = new QLineEdit(); + channelMentionColor->setText(SettingsCache::instance().chat().getChannelMentionColor()); + updateChannelMentionPreview(); + connect(channelMentionColor, &QLineEdit::textChanged, this, &MessagesSettingsPage::updateChannelMentionColor); + + auto *channelGrid = new QGridLayout; + channelGrid->addWidget(&channelUnreadColorLabel, 0, 0); + channelGrid->addWidget(channelUnreadColor, 0, 1); + channelGrid->addWidget(&hexChannelUnreadLabel, 0, 2); + channelGrid->addWidget(&channelMentionColorLabel, 1, 0); + channelGrid->addWidget(channelMentionColor, 1, 1); + channelGrid->addWidget(&hexChannelMentionLabel, 1, 2); + channelGroupBox = new QGroupBox; + channelGroupBox->setLayout(channelGrid); + messageList = new QListWidget; int count = SettingsCache::instance().messages().getCount(); @@ -159,6 +179,7 @@ MessagesSettingsPage::MessagesSettingsPage() mainLayout->addWidget(messageGroupBox); mainLayout->addWidget(chatGroupBox); mainLayout->addWidget(highlightGroupBox); + mainLayout->addWidget(channelGroupBox); setLayout(mainLayout); @@ -195,6 +216,34 @@ void MessagesSettingsPage::updateHighlightColor(const QString &value) } } +void MessagesSettingsPage::updateChannelUnreadColor(const QString &value) +{ +#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) + QColor colorToSet = QColor::fromString("#" + value); +#else + QColor colorToSet; + colorToSet.setNamedColor("#" + value); +#endif + if (colorToSet.isValid()) { + SettingsCache::instance().chat().setChannelUnreadColor(value); + updateChannelUnreadPreview(); + } +} + +void MessagesSettingsPage::updateChannelMentionColor(const QString &value) +{ +#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) + QColor colorToSet = QColor::fromString("#" + value); +#else + QColor colorToSet; + colorToSet.setNamedColor("#" + value); +#endif + if (colorToSet.isValid()) { + SettingsCache::instance().chat().setChannelMentionColor(value); + updateChannelMentionPreview(); + } +} + void MessagesSettingsPage::updateTextColor(QT_STATE_CHANGED_T value) { SettingsCache::instance().chat().setChatMentionForeground(value); @@ -221,6 +270,18 @@ void MessagesSettingsPage::updateHighlightPreview() ";color: " + (SettingsCache::instance().chat().getChatHighlightForeground() ? "white" : "black") + ";}"); } +void MessagesSettingsPage::updateChannelUnreadPreview() +{ + channelUnreadColor->setStyleSheet("QLineEdit{background:#" + + SettingsCache::instance().chat().getChannelUnreadColor() + ";}"); +} + +void MessagesSettingsPage::updateChannelMentionPreview() +{ + channelMentionColor->setStyleSheet("QLineEdit{background:#" + + SettingsCache::instance().chat().getChannelMentionColor() + ";}"); +} + void MessagesSettingsPage::storeSettings() { SettingsCache::instance().messages().setCount(messageList->count()); @@ -267,6 +328,9 @@ void MessagesSettingsPage::retranslateUi() { chatGroupBox->setTitle(tr("Chat settings")); highlightGroupBox->setTitle(tr("Custom alert words")); + channelGroupBox->setTitle(tr("Room channel tabs")); + channelUnreadColorLabel.setText(tr("Unread messages color")); + channelMentionColorLabel.setText(tr("Mentioned messages color")); chatMentionCheckBox.setText(tr("Enable chat mentions")); chatMentionCompleterCheckbox.setText(tr("Enable mention completer")); messageGroupBox->setTitle(tr("In-game message macros")); @@ -284,6 +348,8 @@ void MessagesSettingsPage::retranslateUi() useGameTimeCheckBox.setText(tr("Use game time instead of local time in game logs")); hexLabel.setText(tr("(Color is hexadecimal)")); hexHighlightLabel.setText(tr("(Color is hexadecimal)")); + hexChannelUnreadLabel.setText(tr("(Color is hexadecimal)")); + hexChannelMentionLabel.setText(tr("(Color is hexadecimal)")); customAlertStringLabel.setText(tr("Separate words with a space, alphanumeric characters only")); customAlertString->setPlaceholderText(tr("Word1 Word2 Word3")); aAdd->setText(tr("Add New Message")); diff --git a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h index e85e6cca7..112ff4e29 100644 --- a/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/messages_settings_page.h @@ -27,6 +27,8 @@ private slots: void actRemove(); void updateColor(const QString &value); void updateHighlightColor(const QString &value); + void updateChannelUnreadColor(const QString &value); + void updateChannelMentionColor(const QString &value); void updateTextColor(QT_STATE_CHANGED_T value); void updateTextHighlightColor(QT_STATE_CHANGED_T value); @@ -49,18 +51,27 @@ private: QCheckBox useGameTimeCheckBox; QGroupBox *chatGroupBox; QGroupBox *highlightGroupBox; + QGroupBox *channelGroupBox; QGroupBox *messageGroupBox; QLineEdit *mentionColor; QLineEdit *highlightColor; + QLineEdit *channelUnreadColor; + QLineEdit *channelMentionColor; QLineEdit *customAlertString; QLabel hexLabel; QLabel hexHighlightLabel; + QLabel channelUnreadColorLabel; + QLabel channelMentionColorLabel; + QLabel hexChannelUnreadLabel; + QLabel hexChannelMentionLabel; QLabel customAlertStringLabel; QLabel explainMessagesLabel; void storeSettings(); void updateMentionPreview(); void updateHighlightPreview(); + void updateChannelUnreadPreview(); + void updateChannelMentionPreview(); }; #endif // COCKATRICE_MESSAGES_SETTINGS_PAGE_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.cpp b/cockatrice/src/interface/widgets/tabs/tab_room.cpp index 67106ba34..e5f2020ca 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_room.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -38,9 +39,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -52,6 +55,7 @@ #include #include #include +#include #include #include #include @@ -61,12 +65,45 @@ #include #include +namespace +{ +// Empty id refers to the implicit Main channel. +const QString MAIN_CHANNEL = QString(); + +/** + * @brief Privilege rank mirroring Server_Room::userPrivilegeLevel. + * + * Higher = more privileged. Used to evaluate channel write access locally so + * the input can reflect read-only channels; the server re-checks on send. + */ +int userPrivilegeRank(uint32_t userLevel) +{ + if (userLevel & ServerInfo_User::IsDeveloper) { + return static_cast(ServerInfo_RoomChannel::Developer); + } + if (userLevel & ServerInfo_User::IsAdmin) { + return static_cast(ServerInfo_RoomChannel::Admin); + } + if (userLevel & ServerInfo_User::IsModerator) { + return static_cast(ServerInfo_RoomChannel::Moderator); + } + if (userLevel & ServerInfo_User::IsJudge) { + return static_cast(ServerInfo_RoomChannel::Judge); + } + if (userLevel & ServerInfo_User::IsRegistered) { + return static_cast(ServerInfo_RoomChannel::Registered); + } + return static_cast(ServerInfo_RoomChannel::Public); +} +} // namespace + TabRoom::TabRoom(TabSupervisor *_tabSupervisor, AbstractClient *_client, ServerInfo_User *_ownUser, const ServerInfo_Room &info) : Tab(_tabSupervisor), client(_client), roomId(info.room_id()), roomName(QString::fromStdString(info.name())), - ownUser(_ownUser), userListProxy(_tabSupervisor->getUserListManager()) + ownUser(_ownUser), userListProxy(_tabSupervisor->getUserListManager()), activeChannelId(MAIN_CHANNEL), + chatCollapsed(false) { const int gameTypeListSize = info.gametype_list_size(); for (int i = 0; i < gameTypeListSize; ++i) { @@ -86,14 +123,61 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, const auto gameInviteLinkProvider = [this]() { return tabSupervisor->getGameInviteLinksForRoom(roomId); }; userList->setGameInviteLinkProvider(gameInviteLinkProvider); - chatView = new ChatView(tabSupervisor, nullptr, true, this); - connect(chatView, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup); - connect(chatView, &ChatView::messageClickedSignal, this, &TabRoom::focusTab); - connect(chatView, &ChatView::openMessageDialog, this, &TabRoom::openMessageDialog); - connect(chatView, &ChatView::cockatriceLinkActivated, this, &TabRoom::cockatriceLinkActivated); - connect(chatView, &ChatView::showCardInfoPopup, this, &TabRoom::showCardInfoPopup); - connect(chatView, &ChatView::deleteCardInfoPopup, this, &TabRoom::deleteCardInfoPopup); - connect(chatView, &ChatView::addMentionTag, this, &TabRoom::addMentionTag); + chatViewStack = new QStackedWidget; + channelTabBar = new QTabBar; + channelTabBar->setShape(QTabBar::RoundedSouth); + channelTabBar->setExpanding(true); + channelTabBar->setUsesScrollButtons(true); + + channelViews.insert(MAIN_CHANNEL, makeChatView()); + channelIds << MAIN_CHANNEL; + channelDisplayNames.insert(MAIN_CHANNEL, tr("Main")); + channelUnreadCounts.insert(MAIN_CHANNEL, 0); + channelMentioned.insert(MAIN_CHANNEL, false); + channelWritable.insert(MAIN_CHANNEL, true); + channelTabBar->addTab(channelDisplayNames.value(MAIN_CHANNEL)); + chatViewStack->addWidget(channelViews.value(MAIN_CHANNEL)); + + const uint32_t ownLevel = ownUser != nullptr ? ownUser->user_level() : 0; + const int ownRank = userPrivilegeRank(ownLevel); + + const int channelCount = info.channel_list_size(); + for (int i = 0; i < channelCount; ++i) { + const ServerInfo_RoomChannel &channel = info.channel_list(i); + const QString channelId = QString::fromStdString(channel.id()); + if (channelId.isEmpty()) { + continue; + } + QString displayName = QString::fromStdString(channel.display_name()); + if (displayName.isEmpty()) { + displayName = channelId; + } + const int writeLevel = channel.has_write_access_level() ? static_cast(channel.write_access_level()) + : static_cast(channel.access_level()); + ChatView *channelView = makeChatView(); + const QString banner = QString::fromStdString(channel.banner_message()); + if (!banner.isEmpty()) { + channelView->appendMessage(banner); + } + channelViews.insert(channelId, channelView); + channelIds << channelId; + channelDisplayNames.insert(channelId, displayName); + channelUnreadCounts.insert(channelId, 0); + channelMentioned.insert(channelId, false); + channelWritable.insert(channelId, ownRank >= writeLevel); + channelTabBar->addTab(displayName); + chatViewStack->addWidget(channelViews.value(channelId)); + } + + chatView = channelViews.value(MAIN_CHANNEL); + chatViewStack->setCurrentWidget(chatView); + channelTabBar->setCurrentIndex(0); + // Old servers never send a channel catalog, so the tab bar would only show + // a single "Main" tab where a misclick hides the chat. Only show it when + // there is more than one channel to switch between. + channelTabBar->setVisible(channelIds.size() > 1); + connect(channelTabBar, &QTabBar::currentChanged, this, &TabRoom::onChannelChanged); + connect(channelTabBar, &QTabBar::tabBarClicked, this, &TabRoom::onChannelTabClicked); connect(&SettingsCache::instance().chat(), &ChatSettings::chatMentionCompleterChanged, this, &TabRoom::actCompleterChanged); sayLabel = new QLabel; @@ -101,6 +185,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, sayEdit->setMaxLength(MAX_TEXT_LENGTH); sayLabel->setBuddy(sayEdit); connect(sayEdit, &LineEditCompleter::returnPressed, this, &TabRoom::sendMessage); + updateSayEnabled(); auto *chatSettingsMenu = new QMenu(this); @@ -122,19 +207,27 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, sayHbox->addWidget(sayEdit); sayHbox->addWidget(chatSettingsButton); + sayWidget = new QWidget; + sayWidget->setLayout(sayHbox); + auto *chatVbox = new QVBoxLayout; - chatVbox->addWidget(chatView); - chatVbox->addLayout(sayHbox); + chatVbox->addWidget(chatViewStack); + chatVbox->addWidget(sayWidget); + chatVbox->addWidget(channelTabBar); chatGroupBox = new QGroupBox; chatGroupBox->setLayout(chatVbox); - auto *splitter = new QSplitter(Qt::Vertical); - splitter->addWidget(gameSelector); - splitter->addWidget(chatGroupBox); + chatSplitter = new QSplitter(Qt::Vertical); + chatSplitter->addWidget(gameSelector); + chatSplitter->addWidget(chatGroupBox); + // Splitter children are collapsible by default, so a zero size would hide + // the whole chat group box including the tab bar. Keep it clamped to its + // minimum size (title + tab bar) when collapsed. + chatSplitter->setCollapsible(1, false); auto *hbox = new QHBoxLayout; - hbox->addWidget(splitter, 3); + hbox->addWidget(chatSplitter, 3); hbox->addWidget(userListPanel, 1); aLeaveRoom = new QAction(this); @@ -189,7 +282,9 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor, void TabRoom::retranslateUi() { gameSelector->retranslateUi(); - chatView->retranslateUi(); + for (auto it = channelViews.constBegin(); it != channelViews.constEnd(); ++it) { + it.value()->retranslateUi(); + } userListPanel->retranslateUi(); sayLabel->setText(tr("&Say:")); chatGroupBox->setTitle(tr("Chat")); @@ -197,6 +292,11 @@ void TabRoom::retranslateUi() aLeaveRoom->setText(tr("&Leave room")); aClearChat->setText(tr("&Clear chat")); aOpenChatSettings->setText(tr("Chat Settings...")); + channelDisplayNames.insert(MAIN_CHANNEL, tr("Main")); + for (const QString &channelId : channelIds) { + updateChannelTab(channelId); + } + updateSayEnabled(); } void TabRoom::focusTab() @@ -233,6 +333,11 @@ void TabRoom::tabActivated() if (!sayEdit->hasFocus()) { sayEdit->setFocus(); } + if (!chatCollapsed) { + channelUnreadCounts[activeChannelId] = 0; + channelMentioned[activeChannelId] = false; + updateChannelTab(activeChannelId); + } } QString TabRoom::sanitizeHtml(QString dirty) const @@ -240,29 +345,232 @@ QString TabRoom::sanitizeHtml(QString dirty) const return dirty.replace("&", "&").replace("<", "<").replace(">", ">"); } +void TabRoom::updateSayEnabled() +{ + const bool writable = channelWritable.value(activeChannelId, true); + sayLabel->setEnabled(writable); + sayEdit->setEnabled(writable); + sayEdit->setPlaceholderText(writable ? QString() : tr("This channel is read-only.")); +} + void TabRoom::sendMessage() { + if (!channelWritable.value(activeChannelId, true)) { + return; + } if (sayEdit->text().isEmpty()) { return; } else if (sayEdit->hasVisibleCompleterPopup()) { sayEdit->hideCompleterPopups(); return; } else { - Command_RoomSay cmd; - cmd.set_message(sayEdit->text().toStdString()); + ChatView *targetView = channelViews.value(activeChannelId, chatView); + auto onFinished = [this, targetView](const Response &response) { + if (response.response_code() == Response::RespChatFlood) { + targetView->appendMessage(tr("You are flooding the chat. Please wait a couple of seconds.")); + } else if (response.response_code() == Response::RespFunctionNotAllowed || + response.response_code() == Response::RespInvalidCommand) { + targetView->appendMessage(tr("Your message could not be sent: the channel is not available or you " + "are not allowed to talk there.")); + } + }; - PendingCommand *pend = prepareRoomCommand(cmd); - connect(pend, &PendingCommand::finished, this, &TabRoom::sayFinished); - sendRoomCommand(pend); + if (activeChannelId.isEmpty()) { + Command_RoomSay cmd; + cmd.set_message(sayEdit->text().toStdString()); + + PendingCommand *pend = prepareRoomCommand(cmd); + connect(pend, &PendingCommand::finished, this, onFinished); + sendRoomCommand(pend); + } else { + Command_RoomChannelSay cmd; + cmd.set_channel_id(activeChannelId.toStdString()); + cmd.set_message(sayEdit->text().toStdString()); + + PendingCommand *pend = prepareRoomCommand(cmd); + connect(pend, &PendingCommand::finished, this, onFinished); + sendRoomCommand(pend); + } sayEdit->clear(); } } -void TabRoom::sayFinished(const Response &response) +void TabRoom::onChannelChanged(int index) { - if (response.response_code() == Response::RespChatFlood) { - chatView->appendMessage(tr("You are flooding the chat. Please wait a couple of seconds.")); + if (index < 0 || index >= channelIds.size()) { + return; } + activeChannelId = channelIds.at(index); + ChatView *view = channelViews.value(activeChannelId); + if (view) { + chatView = view; + chatViewStack->setCurrentWidget(view); + } + // Viewing a channel clears its unread state + channelUnreadCounts[activeChannelId] = 0; + channelMentioned[activeChannelId] = false; + updateChannelTab(activeChannelId); + updateSayEnabled(); + if (chatCollapsed) { + setChatCollapsed(false); + } +} + +void TabRoom::onChannelTabClicked(int index) +{ + if (index < 0 || index >= channelIds.size()) { + return; + } + // Clicking the active tab toggles collapse; clicking another tab switches to it + if (channelTabBar->currentIndex() == index && channelIds.at(index) == activeChannelId) { + setChatCollapsed(!chatCollapsed); + } +} + +void TabRoom::setChatCollapsed(bool collapsed) +{ + chatCollapsed = collapsed; + chatViewStack->setVisible(!collapsed); + sayWidget->setVisible(!collapsed); + if (collapsed) { + // Minimize the chat section of the splitter down to just the tab bar + savedSplitterSizes = chatSplitter->sizes(); + chatSplitter->setSizes({1, 0}); + } else { + // Expanding onto the active channel marks it read + channelUnreadCounts[activeChannelId] = 0; + channelMentioned[activeChannelId] = false; + updateChannelTab(activeChannelId); + chatViewStack->setCurrentWidget(chatView); + if (!savedSplitterSizes.isEmpty()) { + chatSplitter->setSizes(savedSplitterSizes); + } + } +} + +QColor TabRoom::parseColorHex(const QString &hex, const QColor &fallback) +{ +#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) + QColor color = QColor::fromString("#" + hex); +#else + QColor color; + color.setNamedColor("#" + hex); +#endif + return color.isValid() ? color : fallback; +} + +void TabRoom::updateChannelTab(const QString &channelId) +{ + int index = channelIds.indexOf(channelId); + if (index < 0) { + return; + } + QString baseName = channelDisplayNames.value(channelId, channelId.isEmpty() ? tr("Main") : channelId); + int unread = channelUnreadCounts.value(channelId, 0); + bool mentioned = channelMentioned.value(channelId, false); + + QString label = baseName; + if (unread > 0) { + label += tr(" (%1)").arg(unread); + } + channelTabBar->setTabText(index, label); + + if (mentioned) { + channelTabBar->setTabTextColor( + index, parseColorHex(SettingsCache::instance().chat().getChannelMentionColor(), QColor(194, 31, 47))); + } else if (unread > 0) { + channelTabBar->setTabTextColor( + index, parseColorHex(SettingsCache::instance().chat().getChannelUnreadColor(), QColor(26, 123, 0))); + } else { + channelTabBar->setTabTextColor(index, QColor()); + } +} + +bool TabRoom::messageContainsMention(const QString &message, const ServerInfo_User &senderInfo) const +{ + if (ownUser == nullptr) { + return false; + } + QString ownName = QString::fromStdString(ownUser->name()); + if (ownName.isEmpty()) { + return false; + } + // Match ChatView::checkMention / checkWord: whole-word, case-insensitive. + // "@zach" must not match "@zachary", and highlight words match full words. + auto containsWholeWord = [](const QString &haystack, const QString &needle) { + if (needle.isEmpty()) { + return false; + } + QRegularExpression re(QStringLiteral("\\b%1\\b").arg(QRegularExpression::escape(needle)), + QRegularExpression::CaseInsensitiveOption); + return re.match(haystack).hasMatch(); + }; + + if (containsWholeWord(message, "@" + ownName)) { + return true; + } + // ChatView only highlights @all for moderator/admin senders + // (ChatView::isModeratorSendingGlobal). Gating here keeps the tab + // mention color from drifting ahead of the message view. + const uint32_t senderLevel = senderInfo.user_level(); + const bool senderIsPrivileged = + (senderLevel & ServerInfo_User::IsModerator) || (senderLevel & ServerInfo_User::IsAdmin); + if (senderIsPrivileged && containsWholeWord(message, "@all")) { + return true; + } + const QStringList words = SettingsCache::instance().chat().getHighlightWords().split(' ', Qt::SkipEmptyParts); + for (const QString &word : words) { + if (containsWholeWord(message, word)) { + return true; + } + } + return false; +} + +void TabRoom::markChannelMessage(const QString &channelId, const QString &message, const ServerInfo_User &senderInfo) +{ + bool roomVisible = (tabSupervisor->currentWidget() == this); + bool viewing = (channelId == activeChannelId) && !chatCollapsed && roomVisible; + if (viewing) { + return; + } + channelUnreadCounts[channelId] = channelUnreadCounts.value(channelId, 0) + 1; + if (messageContainsMention(message, senderInfo)) { + channelMentioned[channelId] = true; + } + updateChannelTab(channelId); +} + +bool TabRoom::isRoomMessageIgnored(const QString &senderName, ServerInfo_User &userInfoOut) const +{ + if (userListProxy->isUserIgnored(senderName)) { + return true; + } + + UserListTWI *twi = userList->getUsers().value(senderName); + if (twi) { + userInfoOut = twi->getUserInfo(); + if (SettingsCache::instance().chat().getIgnoreUnregisteredUsers() && + !UserLevelFlags(userInfoOut.user_level()).testFlag(ServerInfo_User::IsRegistered)) { + return true; + } + } else { + userInfoOut = ServerInfo_User{}; + } + return false; +} + +ChatView *TabRoom::makeChatView() +{ + ChatView *view = new ChatView(tabSupervisor, nullptr, true, this); + connect(view, &ChatView::showMentionPopup, this, &TabRoom::actShowMentionPopup); + connect(view, &ChatView::messageClickedSignal, this, &TabRoom::focusTab); + connect(view, &ChatView::openMessageDialog, this, &TabRoom::openMessageDialog); + connect(view, &ChatView::cockatriceLinkActivated, this, &TabRoom::cockatriceLinkActivated); + connect(view, &ChatView::showCardInfoPopup, this, &TabRoom::showCardInfoPopup); + connect(view, &ChatView::deleteCardInfoPopup, this, &TabRoom::deleteCardInfoPopup); + connect(view, &ChatView::addMentionTag, this, &TabRoom::addMentionTag); + return view; } void TabRoom::actClearChat() @@ -298,6 +606,9 @@ void TabRoom::processRoomEvent(const RoomEvent &event) case RoomEvent::ROOM_SAY: processRoomSayEvent(event.GetExtension(Event_RoomSay::ext)); break; + case RoomEvent::ROOM_CHANNEL_SAY: + processRoomChannelSayEvent(event.GetExtension(Event_RoomChannelSay::ext)); + break; case RoomEvent::REMOVE_MESSAGES: processRemoveMessagesEvent(event.GetExtension(Event_RemoveMessages::ext)); break; @@ -335,18 +646,9 @@ void TabRoom::processRoomSayEvent(const Event_RoomSay &event) QString senderName = QString::fromStdString(event.name()); QString message = QString::fromStdString(event.message()); - if (userListProxy->isUserIgnored(senderName)) { - return; - } - - UserListTWI *twi = userList->getUsers().value(senderName); ServerInfo_User userInfo = {}; - if (twi) { - userInfo = twi->getUserInfo(); - if (SettingsCache::instance().chat().getIgnoreUnregisteredUsers() && - !UserLevelFlags(userInfo.user_level()).testFlag(ServerInfo_User::IsRegistered)) { - return; - } + if (isRoomMessageIgnored(senderName, userInfo)) { + return; } if (event.message_type() == Event_RoomSay::ChatHistory && !SettingsCache::instance().chat().getRoomHistory()) { @@ -360,7 +662,49 @@ void TabRoom::processRoomSayEvent(const Event_RoomSay &event) "] " + message; } - chatView->appendMessage(message, event.message_type(), userInfo, true); + ChatView *mainView = channelViews.value(MAIN_CHANNEL); + if (mainView == nullptr) { + return; + } + + mainView->appendMessage(message, event.message_type(), userInfo, true); + if (event.message_type() != Event_RoomSay::ChatHistory) { + markChannelMessage(MAIN_CHANNEL, message, userInfo); + } + emit userEvent(false); +} + +void TabRoom::processRoomChannelSayEvent(const Event_RoomChannelSay &event) +{ + const QString channelId = QString::fromStdString(event.channel_id()); + ChatView *view = channelViews.value(channelId); + if (view == nullptr) { + return; + } + + QString senderName = QString::fromStdString(event.name()); + QString message = QString::fromStdString(event.message()); + + ServerInfo_User userInfo = {}; + if (isRoomMessageIgnored(senderName, userInfo)) { + return; + } + + if (senderName.isEmpty()) { + if (!SettingsCache::instance().chat().getRoomHistory()) { + return; + } + message = + "[" + + QString(QDateTime::fromMSecsSinceEpoch(event.time_of()).toLocalTime().toString("d MMM yyyy HH:mm:ss")) + + "] " + message; + } + + view->appendMessage(message, {}, userInfo, true); + if (!senderName.isEmpty()) { + // Empty senders mark replayed history, which must not count as unread + markChannelMessage(channelId, message, userInfo); + } emit userEvent(false); } @@ -368,7 +712,9 @@ void TabRoom::processRemoveMessagesEvent(const Event_RemoveMessages &event) { QString userName = QString::fromStdString(event.name()); int amount = event.amount(); - chatView->redactMessages(userName, amount); + for (ChatView *view : channelViews) { + view->redactMessages(userName, amount); + } } void TabRoom::refreshShortcuts() diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h index 031056145..8f7c8f918 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.h +++ b/cockatrice/src/interface/widgets/tabs/tab_room.h @@ -10,6 +10,7 @@ #include "tab.h" +#include #include #include #include @@ -18,8 +19,12 @@ class UserListProxy; class QAction; +class QTabBar; +class QWidget; class QGroupBox; +class QSplitter; class QMenu; +class QStackedWidget; class QStringListModel; class TabSupervisor; @@ -42,6 +47,7 @@ class Event_ListGames; class Event_JoinRoom; class Event_LeaveRoom; class Event_RoomSay; +class Event_RoomChannelSay; class Event_RemoveMessages; class GameSelector; class Response; @@ -64,10 +70,23 @@ private: UserListWidget *userList; const UserListProxy *userListProxy; ChatView *chatView; + QStackedWidget *chatViewStack; + QTabBar *channelTabBar; + QWidget *sayWidget; + QMap channelViews; + QMap channelDisplayNames; + QMap channelUnreadCounts; + QMap channelMentioned; + QMap channelWritable; + QStringList channelIds; + QString activeChannelId; + bool chatCollapsed; QLabel *sayLabel; LineEditCompleter *sayEdit; QStringListModel *mentionModel; QGroupBox *chatGroupBox; + QSplitter *chatSplitter; + QList savedSplitterSizes; QMenu *roomMenu; QAction *aLeaveRoom; @@ -85,7 +104,9 @@ signals: void gameListUpdated(); private slots: void sendMessage(); - void sayFinished(const Response &response); + void onChannelChanged(int index); + void onChannelTabClicked(int index); + void setChatCollapsed(bool collapsed); void actClearChat(); void actOpenChatSettings(); void addMentionTag(QString mentionTag); @@ -98,9 +119,18 @@ private slots: void processJoinRoomEvent(const Event_JoinRoom &event); void processLeaveRoomEvent(const Event_LeaveRoom &event); void processRoomSayEvent(const Event_RoomSay &event); + void processRoomChannelSayEvent(const Event_RoomChannelSay &event); void processRemoveMessagesEvent(const Event_RemoveMessages &event); void refreshShortcuts(); + ChatView *makeChatView(); + void updateChannelTab(const QString &channelId); + void updateSayEnabled(); + void markChannelMessage(const QString &channelId, const QString &message, const ServerInfo_User &senderInfo); + [[nodiscard]] bool messageContainsMention(const QString &message, const ServerInfo_User &senderInfo) const; + [[nodiscard]] bool isRoomMessageIgnored(const QString &senderName, ServerInfo_User &userInfoOut) const; + static QColor parseColorHex(const QString &hex, const QColor &fallback); + protected slots: void closeEvent(QCloseEvent *event) override; diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h index 04dad9243..03f98f7bd 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_chat_settings_provider.h @@ -12,6 +12,8 @@ public: [[nodiscard]] virtual bool getChatMentionCompleter() const = 0; [[nodiscard]] virtual QString getChatMentionColor() const = 0; [[nodiscard]] virtual QString getChatHighlightColor() const = 0; + [[nodiscard]] virtual QString getChannelUnreadColor() const = 0; + [[nodiscard]] virtual QString getChannelMentionColor() const = 0; [[nodiscard]] virtual bool getChatMentionForeground() const = 0; [[nodiscard]] virtual bool getChatHighlightForeground() const = 0; [[nodiscard]] virtual bool getIgnoreUnregisteredUsers() const = 0; diff --git a/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp b/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp index d9ae0ddac..10567f2ff 100644 --- a/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/chat_settings.cpp @@ -31,6 +31,16 @@ QString ChatSettings::getChatHighlightColor() const return getValue("highlightColor", QString(), QString(), "A6120D").toString(); } +QString ChatSettings::getChannelUnreadColor() const +{ + return getValue("channelUnreadColor", QString(), QString(), "1A7B00").toString(); +} + +QString ChatSettings::getChannelMentionColor() const +{ + return getValue("channelMentionColor", QString(), QString(), "A6120D").toString(); +} + bool ChatSettings::getChatMentionForeground() const { return getValue("mentionForeground", QString(), QString(), true).toBool(); @@ -107,6 +117,16 @@ void ChatSettings::setChatHighlightColor(const QString &_chatHighlightColor) setValue(_chatHighlightColor, "highlightColor"); } +void ChatSettings::setChannelUnreadColor(const QString &_channelUnreadColor) +{ + setValue(_channelUnreadColor, "channelUnreadColor"); +} + +void ChatSettings::setChannelMentionColor(const QString &_channelMentionColor) +{ + setValue(_channelMentionColor, "channelMentionColor"); +} + void ChatSettings::setChatMentionForeground(bool _chatMentionForeground) { setValue(_chatMentionForeground, "mentionForeground"); diff --git a/libcockatrice_settings/libcockatrice/settings/chat_settings.h b/libcockatrice_settings/libcockatrice/settings/chat_settings.h index e00da4527..d107dfb67 100644 --- a/libcockatrice_settings/libcockatrice/settings/chat_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/chat_settings.h @@ -19,6 +19,8 @@ public: [[nodiscard]] bool getChatMentionCompleter() const override; [[nodiscard]] QString getChatMentionColor() const override; [[nodiscard]] QString getChatHighlightColor() const override; + [[nodiscard]] QString getChannelUnreadColor() const override; + [[nodiscard]] QString getChannelMentionColor() const override; [[nodiscard]] bool getChatMentionForeground() const override; [[nodiscard]] bool getChatHighlightForeground() const override; [[nodiscard]] bool getIgnoreUnregisteredUsers() const override; @@ -35,6 +37,8 @@ public: void setChatMentionCompleter(bool _chatMentionCompleter); void setChatMentionColor(const QString &_chatMentionColor); void setChatHighlightColor(const QString &_chatHighlightColor); + void setChannelUnreadColor(const QString &_channelUnreadColor); + void setChannelMentionColor(const QString &_channelMentionColor); void setChatMentionForeground(bool _chatMentionForeground); void setChatHighlightForeground(bool _chatHighlightForeground); void setIgnoreUnregisteredUsers(bool _ignoreUnregisteredUsers); diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index 2ba9f3fbb..0d4cdbafe 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -285,6 +285,32 @@ TEST_F(SettingsDefaultsTest, Chat_HighlightColor_Default) ASSERT_EQ(s.getChatHighlightColor(), QString("A6120D")); } +TEST_F(SettingsDefaultsTest, Chat_ChannelUnreadColor_Default) +{ + ChatSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getChannelUnreadColor(), QString("1A7B00")); +} + +TEST_F(SettingsDefaultsTest, Chat_ChannelUnreadColor_SetAndGet) +{ + ChatSettings s(settingsPath, nullptr); + s.setChannelUnreadColor(QString("0000FF")); + ASSERT_EQ(s.getChannelUnreadColor(), QString("0000FF")); +} + +TEST_F(SettingsDefaultsTest, Chat_ChannelMentionColor_Default) +{ + ChatSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getChannelMentionColor(), QString("A6120D")); +} + +TEST_F(SettingsDefaultsTest, Chat_ChannelMentionColor_SetAndGet) +{ + ChatSettings s(settingsPath, nullptr); + s.setChannelMentionColor(QString("FF0000")); + ASSERT_EQ(s.getChannelMentionColor(), QString("FF0000")); +} + TEST_F(SettingsDefaultsTest, Chat_MentionForeground_Default) { ChatSettings s(settingsPath, nullptr);