Skip to content

Commit

Permalink
FEAT(client): Add function to pin channels when filtering
Browse files Browse the repository at this point in the history
Previously, channel could only be explicitly hidden from
the tree view when the channel filter was enabled.

This commit adds the additional option to pin channels
which will be shown in filtered mode, even if there are
no clients connected to them. This is accompanied by
a new method of saving the details in the client sqlite
database: The table 'filtered_channel' was altered to
contain an enum value. Whenever the current user is
in a channel, the channel and all parent channels are
always visible. Otherwise, the channel can be either
hidden, pinned, or behave according to the current
global UI setting when filtering.

Closes mumble-voip#1830

Co-Authored-By: Stefan Hacker <[email protected]>
  • Loading branch information
Hartmnt and hacst committed Nov 14, 2022
1 parent 8535e08 commit b7ac0fb
Show file tree
Hide file tree
Showing 15 changed files with 258 additions and 103 deletions.
90 changes: 87 additions & 3 deletions src/Channel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
#include <QtCore/QStack>

#ifdef MUMBLE
# include <queue>
# include "PluginManager.h"
# include "Global.h"
# include "Database.h"
# include "ServerHandler.h"

QHash< int, Channel * > Channel::c_qhChannels;
QReadWriteLock Channel::c_qrwlChannels;
Expand All @@ -30,11 +33,11 @@ Channel::Channel(int id, const QString &name, QObject *p) : QObject(p) {
cParent->addChannel(this);
#ifdef MUMBLE
uiPermissions = 0;
bFiltered = false;
m_filterMode = ChannelFilterMode::NORMAL;

hasEnterRestrictions.store(false);
localUserCanEnter.store(true);
#endif
#endif // MUMBLE
}

Channel::~Channel() {
Expand Down Expand Up @@ -84,7 +87,88 @@ void Channel::remove(Channel *c) {
QWriteLocker lock(&c_qrwlChannels);
c_qhChannels.remove(c->iId);
}
#endif

void Channel::setFilterMode(ChannelFilterMode filterMode) {
m_filterMode = filterMode;
ServerHandlerPtr sh = Global::get().sh;
if (sh) {
Global::get().db->setChannelFilterMode(sh->qbaDigest, iId, m_filterMode);
}
}

void Channel::clearFilterMode() {
if (m_filterMode != ChannelFilterMode::NORMAL) {
setFilterMode(ChannelFilterMode::NORMAL);
}
}

bool Channel::isFiltered() const {
if (!Global::get().s.bFilterActive) {
// Channel filtering is disabled
return false;
}

const Channel *userChannel = nullptr;

if (Global::get().uiSession != 0) {
const ClientUser *user = ClientUser::get(Global::get().uiSession);
if (user) {
userChannel = user->cChannel;
}
}

bool hasUser = false;
bool hasHide = false;
bool hasPin = false;

// Iterate tree down
std::queue< const Channel * > channelSearch;
channelSearch.push(this);

while (!channelSearch.empty()) {
const Channel *c = channelSearch.front();
channelSearch.pop();

// Never hide channel, if user resides in this channel or a subchannel
if (userChannel && c == userChannel) {
return false;
}

if (c->m_filterMode == ChannelFilterMode::PIN) {
hasPin = true;
}

if (!c->qlUsers.isEmpty()) {
hasUser = true;
}

for (const Channel *currentSubChannel : c->qlChannels) {
channelSearch.push(currentSubChannel);
}
}

// Iterate tree up
const Channel *c = this;
while (c) {
if (c->m_filterMode == ChannelFilterMode::HIDE) {
hasHide = true;
break;
}
c = c->cParent;
}

if (hasPin) {
return false;
}

if (hasHide) {
return true;
}

return Global::get().s.bFilterHidesEmptyChannels && !hasUser;
}

#endif // MUMBLE

bool Channel::lessThan(const Channel *first, const Channel *second) {
if ((first->iPosition != second->iPosition) && (first->cParent == second->cParent))
Expand Down
8 changes: 7 additions & 1 deletion src/Channel.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#ifdef MUMBLE
# include <atomic>
# include "ChannelFilterMode.h"
#endif

class User;
Expand Down Expand Up @@ -71,7 +72,12 @@ class Channel : public QObject {

#ifdef MUMBLE
unsigned int uiPermissions;
bool bFiltered;

ChannelFilterMode m_filterMode;

void setFilterMode(ChannelFilterMode filterMode);
void clearFilterMode();
bool isFiltered() const;

static QHash< int, Channel * > c_qhChannels;
static QReadWriteLock c_qrwlChannels;
Expand Down
1 change: 1 addition & 0 deletions src/mumble/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ set(MUMBLE_SOURCES
"Cert.cpp"
"Cert.h"
"Cert.ui"
"ChannelFilterMode.h"
"ClientUser.cpp"
"ClientUser.h"
"ConfigDialog.cpp"
Expand Down
21 changes: 21 additions & 0 deletions src/mumble/ChannelFilterMode.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright 2022 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#ifndef MUMBLE_CHANNELFILTERMODE_H_
#define MUMBLE_CHANNELFILTERMODE_H_

/// Visibility modifiers used when applying the channel filter
/// The channel the user is in will be always visible
/// This enum is used in the client DB, only append new entries
enum class ChannelFilterMode {
/// The default channel filtering behavior
NORMAL,
/// Channel is filtered, if the channel filter is active
HIDE,
/// Channel is not filtered, even if it is empty
PIN
};

#endif
38 changes: 26 additions & 12 deletions src/mumble/Database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ Database::Database(const QString &dbname) {
"`server_cert_digest` TEXT NOT NULL, `channel_id` INTEGER NOT NULL)"));
execQueryAndLogFailure(query, QLatin1String("CREATE UNIQUE INDEX IF NOT EXISTS `filtered_channels_entry` ON "
"`filtered_channels`(`server_cert_digest`, `channel_id`)"));
query.exec(QLatin1String("ALTER TABLE `filtered_channels` ADD COLUMN `filter_mode` INTEGER DEFAULT ")
+ QString::number(
static_cast< int >(ChannelFilterMode::HIDE))); // Upgrade path, failing this query is not noteworthy

execQueryAndLogFailure(query, QLatin1String("CREATE TABLE IF NOT EXISTS `pingcache` (`id` INTEGER PRIMARY KEY "
"AUTOINCREMENT, `hostname` TEXT, `port` INTEGER, `ping` INTEGER)"));
Expand Down Expand Up @@ -396,30 +399,41 @@ void Database::setLocalMuted(const QString &hash, bool muted) {
execQueryAndLogFailure(query);
}

bool Database::isChannelFiltered(const QByteArray &server_cert_digest, const int channel_id) {
ChannelFilterMode Database::getChannelFilterMode(const QByteArray &server_cert_digest, const int channel_id) {
QSqlQuery query(db);

query.prepare(QLatin1String(
"SELECT `channel_id` FROM `filtered_channels` WHERE `server_cert_digest` = ? AND `channel_id` = ?"));
"SELECT `filter_mode` FROM `filtered_channels` WHERE `server_cert_digest` = ? AND `channel_id` = ?"));
query.addBindValue(server_cert_digest);
query.addBindValue(channel_id);
execQueryAndLogFailure(query);

return query.next();
if (query.first()) {
return static_cast< ChannelFilterMode >(query.value(0).toInt());
}

return ChannelFilterMode::NORMAL;
}

void Database::setChannelFiltered(const QByteArray &server_cert_digest, const int channel_id, const bool hidden) {
void Database::setChannelFilterMode(const QByteArray &server_cert_digest, const int channel_id,
const ChannelFilterMode filterMode) {
QSqlQuery query(db);

if (hidden)
query.prepare(
QLatin1String("INSERT INTO `filtered_channels` (`server_cert_digest`, `channel_id`) VALUES (?, ?)"));
else
query.prepare(
QLatin1String("DELETE FROM `filtered_channels` WHERE `server_cert_digest` = ? AND `channel_id` = ?"));
switch (filterMode) {
case ChannelFilterMode::NORMAL:
query.prepare(
QLatin1String("DELETE FROM `filtered_channels` WHERE `server_cert_digest` = ? AND `channel_id` = ?"));
break;
case ChannelFilterMode::PIN:
case ChannelFilterMode::HIDE:
query.prepare(QLatin1String("INSERT OR REPLACE INTO `filtered_channels` (`server_cert_digest`, "
"`channel_id`, `filter_mode`) VALUES (?, ?, ?)"));
query.bindValue(2, static_cast< int >(filterMode));
break;
}

query.addBindValue(server_cert_digest);
query.addBindValue(channel_id);
query.bindValue(0, server_cert_digest);
query.bindValue(1, channel_id);

execQueryAndLogFailure(query);
}
Expand Down
5 changes: 3 additions & 2 deletions src/mumble/Database.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#ifndef MUMBLE_MUMBLE_DATABASE_H_
#define MUMBLE_MUMBLE_DATABASE_H_

#include "Channel.h"
#include "Settings.h"
#include "UnresolvedServerAddress.h"
#include <QSqlDatabase>
Expand Down Expand Up @@ -54,8 +55,8 @@ class Database : public QObject {
QString getUserLocalNickname(const QString &hash);
void setUserLocalNickname(const QString &hash, const QString &nickname);

bool isChannelFiltered(const QByteArray &server_cert_digest, const int channel_id);
void setChannelFiltered(const QByteArray &server_cert_digest, const int channel_id, bool hidden);
ChannelFilterMode getChannelFilterMode(const QByteArray &server_cert_digest, int channel_id);
void setChannelFilterMode(const QByteArray &server_cert_digest, int channel_id, ChannelFilterMode filterMode);

QMap< UnresolvedServerAddress, unsigned int > getPingCache();
void setPingCache(const QMap< UnresolvedServerAddress, unsigned int > &cache);
Expand Down
37 changes: 30 additions & 7 deletions src/mumble/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -752,7 +752,7 @@ void MainWindow::updateTrayIcon() {

void MainWindow::updateUserModel() {
UserModel *um = static_cast< UserModel * >(qtvUsers->model());
um->toggleChannelFiltered(nullptr); // Force a UI refresh
um->forceVisualUpdate();
}

void MainWindow::updateTransmitModeComboBox(Settings::AudioTransmit newMode) {
Expand Down Expand Up @@ -2262,7 +2262,8 @@ void MainWindow::qmChannel_aboutToShow() {
// hiding the root is nonsense
if (c && c->cParent) {
qmChannel->addSeparator();
qmChannel->addAction(qaChannelFilter);
qmChannel->addAction(qaChannelHide);
qmChannel->addAction(qaChannelPin);
}

#ifndef Q_OS_MAC
Expand Down Expand Up @@ -2305,8 +2306,10 @@ void MainWindow::qmChannel_aboutToShow() {
}
}

if (c)
qaChannelFilter->setChecked(c->bFiltered);
if (c) {
qaChannelHide->setChecked(c->m_filterMode == ChannelFilterMode::HIDE);
qaChannelPin->setChecked(c->m_filterMode == ChannelFilterMode::PIN);
}

qaChannelAdd->setEnabled(add);
qaChannelRemove->setEnabled(remove);
Expand Down Expand Up @@ -2350,12 +2353,31 @@ void MainWindow::on_qaChannelListen_triggered() {
}
}

void MainWindow::on_qaChannelFilter_triggered() {
void MainWindow::on_qaChannelHide_triggered() {
Channel *c = getContextMenuChannel();

if (c) {
UserModel *um = static_cast< UserModel * >(qtvUsers->model());
if (qaChannelHide->isChecked()) {
c->setFilterMode(ChannelFilterMode::HIDE);
} else {
c->setFilterMode(ChannelFilterMode::NORMAL);
}
um->forceVisualUpdate(c);
}
}

void MainWindow::on_qaChannelPin_triggered() {
Channel *c = getContextMenuChannel();

if (c) {
UserModel *um = static_cast< UserModel * >(qtvUsers->model());
um->toggleChannelFiltered(c);
if (qaChannelPin->isChecked()) {
c->setFilterMode(ChannelFilterMode::PIN);
} else {
c->setFilterMode(ChannelFilterMode::NORMAL);
}
um->forceVisualUpdate(c);
}
}

Expand Down Expand Up @@ -2576,7 +2598,8 @@ void MainWindow::updateMenuPermissions() {
qaChannelUnlinkAll->setEnabled(p & (ChanACL::Write | ChanACL::LinkChannel));
qaChannelCopyURL->setEnabled(target.channel);
qaChannelSendMessage->setEnabled(p & (ChanACL::Write | ChanACL::TextMessage));
qaChannelFilter->setEnabled(true);
qaChannelHide->setEnabled(target.channel);
qaChannelPin->setEnabled(target.channel);

bool chatBarEnabled = false;
if (Global::get().uiSession) {
Expand Down
3 changes: 2 additions & 1 deletion src/mumble/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ public slots:
void on_qaChannelUnlink_triggered();
void on_qaChannelUnlinkAll_triggered();
void on_qaChannelSendMessage_triggered();
void on_qaChannelFilter_triggered();
void on_qaChannelHide_triggered();
void on_qaChannelPin_triggered();
void on_qaChannelCopyURL_triggered();
void on_qaAudioReset_triggered();
void on_qaAudioMute_triggered();
Expand Down
10 changes: 9 additions & 1 deletion src/mumble/MainWindow.ui
Original file line number Diff line number Diff line change
Expand Up @@ -897,14 +897,22 @@ the channel's context menu.</string>
<string>&amp;Join Channel</string>
</property>
</action>
<action name="qaChannelFilter">
<action name="qaChannelHide">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>&amp;Hide Channel when Filtering</string>
</property>
</action>
<action name="qaChannelPin">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>&amp;Pin Channel when Filtering</string>
</property>
</action>
<action name="qaUserCommentView">
<property name="text">
<string>View Comment...</string>
Expand Down
16 changes: 5 additions & 11 deletions src/mumble/Messages.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -886,8 +886,9 @@ void MainWindow::msgChannelState(const MumbleProto::ChannelState &msg) {
p = nullptr; // No need to move it later

ServerHandlerPtr sh = Global::get().sh;
if (sh)
c->bFiltered = Global::get().db->isChannelFiltered(sh->qbaDigest, c->iId);
if (sh) {
c->m_filterMode = Global::get().db->getChannelFilterMode(sh->qbaDigest, c->iId);
}

} else {
qWarning("Server attempted state change on nonexistent channel");
Expand Down Expand Up @@ -970,21 +971,14 @@ void MainWindow::msgChannelState(const MumbleProto::ChannelState &msg) {
}

if (updateUI) {
// Passing nullptr to this function will make it do not much except fire a dataChanged event
// which leads to the UI being updated (reflecting the changes that just took effect).
this->pmModel->toggleChannelFiltered(nullptr);
this->pmModel->forceVisualUpdate();
}
}

void MainWindow::msgChannelRemove(const MumbleProto::ChannelRemove &msg) {
Channel *c = Channel::get(msg.channel_id());
if (c && (c->iId != 0)) {
if (c->bFiltered) {
ServerHandlerPtr sh = Global::get().sh;
if (sh)
Global::get().db->setChannelFiltered(sh->qbaDigest, c->iId, false);
c->bFiltered = false;
}
c->clearFilterMode();

if (Global::get().mw->m_searchDialog) {
QMetaObject::invokeMethod(Global::get().mw->m_searchDialog, "on_channelRemoved", Qt::QueuedConnection,
Expand Down
Loading

0 comments on commit b7ac0fb

Please sign in to comment.