This commit is contained in:
Jonathan Martin
2026-04-21 12:36:34 -07:00
parent 9168f5d309
commit 34a255d1f3
23 changed files with 120 additions and 46 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ variables:
BUILD_LINUX_CLI: 'y'
GIT_DEPTH: 5 # Only grab the last 5 commits when cloning
NEXUS_PATH_ROOT: 'https://nexus.int.windscribe.com/repository/client-desktop/client-desktop'
NEXUS_PATH_DEPS: '$NEXUS_PATH_ROOT/dependencies/current'
NEXUS_PATH_DEPS: '$NEXUS_PATH_ROOT/dependencies/2.22'
NEXUS_PATH_VCPKG_CACHE: '$NEXUS_PATH_ROOT/vcpkg_cache/current'
NEXUS_PATH_BRANCH_UPLOAD: '${NEXUS_PATH_ROOT}/branches/${CI_COMMIT_BRANCH}'
NEXUS_PATH_TAGGED_UPLOAD: '${NEXUS_PATH_ROOT}/tagged-builds'
+15
View File
@@ -1,3 +1,18 @@
2.22.5 (15/04/2026)
All:
* Fixed dropdown menus in Preferences scrolling to the wrong position. #1692
* Fixed El Salvador flag icon. #1692
* Fixed app does not consistently connect to the specific server that has the user's pinned IP. #1734
* Fixed purchased ALC locations cannot be selected. #1753
macOS:
* Fixed unnecessary VPN reconnection when switching between WiFi access points on the same network. #1708
* Fixed a crash when deleting a session. #1751
* Fixed a crash that could occur while reinstalling the app. #1752
* Fixed a crash when interacting with credential fields. #1748
Linux:
* Fixed unnecessary VPN reconnection when switching between WiFi access points on the same network. #1708
2.22.4 (10/04/2026)
All:
* Improved WireGuard PersistentKeepalive to use a value of 25 on all platforms. #1741
+1 -1
View File
@@ -5,7 +5,7 @@
set(WS_VERSION_MAJOR 2)
set(WS_VERSION_MINOR 22)
set(WS_VERSION_BUILD 4)
set(WS_VERSION_BUILD 5)
set(WS_BUILD_TYPE "guinea_pig")
# WS_APP_IDENTIFIER: Internal identifier, no spaces. Used for service names,
@@ -108,7 +108,6 @@ SessionStatus::SessionStatus(const std::string &json) : d(new SessionStatusData)
d->alc_ << v.toString();
}
}
d->staticIpsUpdateDevices_.clear();
if (jsonData.contains("sip"))
{
+4 -10
View File
@@ -133,18 +133,12 @@ int Utils::generateIntegerRandom(const int &min, const int &max)
bool Utils::isSubdomainsEqual(const QString &hostname1, const QString &hostname2)
{
// Compare only the subdomain portion so pinned IP matching still works when the server TLD changes.
int i1 = hostname1.indexOf('.');
int i2 = hostname2.indexOf('.');
if (i1 != -1 && i2 != -1)
{
QString sub1 = hostname1.mid(0, i1);
QString sub2 = hostname2.mid(0, i2);
return sub1 == sub2;
}
else
{
return false;
}
QString sub1 = (i1 != -1) ? hostname1.left(i1) : hostname1;
QString sub2 = (i2 != -1) ? hostname2.left(i2) : hostname2;
return !sub1.isEmpty() && sub1 == sub2;
}
std::wstring Utils::getDirPathFromFullPath(const std::wstring &fullPath)
@@ -1,18 +1,20 @@
#include "helperbackend_mac.h"
#include <QMetaObject>
#include <QMutexLocker>
#include "installhelper_mac.h"
HelperBackend_mac::HelperBackend_mac(QObject *parent, spdlog::logger *logger) :
IHelperBackend(parent), connection_(nullptr), logger_(logger)
IHelperBackend(parent), logger_(logger)
{
}
HelperBackend_mac::~HelperBackend_mac()
{
curState_ = State::kInit;
if (connection_) {
xpc_connection_set_event_handler(connection_, ^(xpc_object_t) {});
xpc_connection_cancel(connection_);
dispatch_sync(queue_, ^{});
}
}
@@ -21,7 +23,8 @@ void HelperBackend_mac::startInstallHelper(bool bForceDeleteOld)
bool isUserCanceled;
if (InstallHelper_mac::installHelper(bForceDeleteOld, isUserCanceled, logger_)) {
// start XPC connection
connection_ = xpc_connection_create_mach_service(WS_MAC_HELPER_BUNDLE_ID, NULL, 0);
queue_ = dispatch_queue_create("com.windscribe.helper.xpc", DISPATCH_QUEUE_SERIAL);
connection_ = xpc_connection_create_mach_service(WS_MAC_HELPER_BUNDLE_ID, queue_, 0);
if (!connection_)
return;
@@ -1,7 +1,9 @@
#pragma once
#include "ihelperbackend.h"
#include <atomic>
#include <QMutex>
#include <dispatch/dispatch.h>
#include <xpc/xpc.h>
#include <spdlog/spdlog.h>
@@ -21,8 +23,9 @@ public:
std::string sendCmd(int cmdId, const std::string &data) override;
private:
State curState_ = State::kInit;
std::atomic<State> curState_ = State::kInit;
mutable QMutex mutex_;
xpc_connection_t connection_;
dispatch_queue_t queue_ = nullptr;
xpc_connection_t connection_ = nullptr;
spdlog::logger *logger_;
};
@@ -124,8 +124,9 @@ void MutableLocationInfo::selectNodeByIp(const QString &addr)
bool MutableLocationInfo::selectNodeByHostname(const QString &hostname)
{
// Match by subdomain only so pinned IPs still work when the server TLD changes.
for (int i = 0; i < nodes_.count(); i++) {
if (nodes_[i]->getHostname() == hostname) {
if (Utils::isSubdomainsEqual(nodes_[i]->getHostname(), hostname)) {
qCDebug(LOG_BASIC) << "Selected node by hostname: " << i;
selectedNode_ = i;
return true;
@@ -72,8 +72,18 @@ void NetworkDetectionManager_linux::updateNetworkInfo(bool bWithEmitSignal)
types::NetworkInterface newNetworkInterface = NetworkUtils_linux::networkInterfaceByName(ifname);
if (newNetworkInterface != networkInterface_) {
bool significantChange = false;
if (newNetworkInterface.interfaceName != networkInterface_.interfaceName) {
significantChange = true;
} else if (newNetworkInterface.networkOrSsid != networkInterface_.networkOrSsid) {
significantChange = true;
} else {
qCInfo(LOG_BASIC) << "Minor interface change (e.g. same-SSID AP roam), skipping reconnect";
}
networkInterface_ = newNetworkInterface;
if (bWithEmitSignal) {
if (bWithEmitSignal && significantChange) {
emit networkChanged(networkInterface_);
}
}
@@ -42,8 +42,11 @@ void NetworkDetectionManager_mac::onNetworkStateChanged()
if (networkInterface != lastNetworkInterface_)
{
bool significantChange = false;
if (networkInterface.interfaceName != lastNetworkInterface_.interfaceName)
{
significantChange = true;
if (networkInterface.interfaceIndex == -1)
{
qCInfo(LOG_BASIC) << "Primary Adapter down: " << lastNetworkInterface_.interfaceName;
@@ -62,6 +65,7 @@ void NetworkDetectionManager_mac::onNetworkStateChanged()
}
else if (networkInterface.networkOrSsid != lastNetworkInterface_.networkOrSsid)
{
significantChange = true;
qCInfo(LOG_BASIC) << "Primary Network Changed: "
<< networkInterface.interfaceName
<< " : " << networkInterface.networkOrSsid;
@@ -75,12 +79,14 @@ void NetworkDetectionManager_mac::onNetworkStateChanged()
}
else
{
qCInfo(LOG_BASIC) << "Unidentified interface change";
// Can happen when changing interfaces
qCInfo(LOG_BASIC) << "Minor interface change on" << networkInterface.interfaceName
<< "(e.g. same-SSID AP roam), skipping reconnect";
}
lastNetworkInterface_ = networkInterface;
emit networkChanged(networkInterface);
if (significantChange) {
emit networkChanged(networkInterface);
}
}
else if (wifiAdapterUp != lastWifiAdapterUp_)
{
+5
View File
@@ -415,7 +415,12 @@ void MainService::onPinIp()
}
// Get the current connecting hostname from backend
// Store only the subdomain portion so pinned IPs still work when the server TLD changes.
QString hostname = backend_->getCurrentConnectingHostname();
int dotIndex = hostname.indexOf('.');
if (dotIndex != -1) {
hostname = hostname.left(dotIndex);
}
// Add to favorites with hostname and IP via kPinnedIp role
QVariantList pinnedData;
@@ -517,7 +517,7 @@ void Backend::onEngineSessionDeleted()
void Backend::onEngineUpdateSessionStatus(const api_responses::SessionStatus &sessionStatus)
{
latestSessionStatus_ = sessionStatus;
locationsModelManager_->setFreeSessionStatus(!latestSessionStatus_.isPremium());
locationsModelManager_->setFreeSessionStatus(!latestSessionStatus_.isPremium(), latestSessionStatus_.getAlc());
updateAccountInfo();
emit sessionStatusChanged(latestSessionStatus_);
}
@@ -79,9 +79,9 @@ void LocationsModelManager::setLocationOrder(ORDER_LOCATION_TYPE orderLocationTy
filterLocationsProxyModel_->setLocationOrder(orderLocationType);
}
void LocationsModelManager::setFreeSessionStatus(bool isFreeSessionStatus)
void LocationsModelManager::setFreeSessionStatus(bool isFreeSessionStatus, const QStringList &alcLocations)
{
locationsModel_->setFreeSessionStatus(isFreeSessionStatus);
locationsModel_->setFreeSessionStatus(isFreeSessionStatus, alcLocations);
}
QModelIndex LocationsModelManager::getIndexByLocationId(const LocationID &id) const
@@ -21,7 +21,7 @@ public:
void updateDeviceName(const QString &staticIpDeviceName);
void changeConnectionSpeed(LocationID id, PingTime speed);
void setLocationOrder(ORDER_LOCATION_TYPE orderLocationType);
void setFreeSessionStatus(bool isFreeSessionStatus);
void setFreeSessionStatus(bool isFreeSessionStatus, const QStringList &alcLocations);
QAbstractItemModel *locationsModel() { return locationsModel_; }
QAbstractItemModel *sortedLocationsProxyModel() { return sortedLocationsProxyModel_; }
@@ -110,6 +110,15 @@ void FavoriteLocationsStorage::readFromSettings()
}
}
// Migrate any stored FQDNs to subdomain-only so pinned IPs still work when the server TLD changes.
for (auto it = favoriteLocations_.begin(); it != favoriteLocations_.end(); ++it) {
int dotIndex = it.value().pinnedHostname.indexOf('.');
if (dotIndex != -1) {
it.value().pinnedHostname = it.value().pinnedHostname.left(dotIndex);
isFavoriteLocationsSetModified_ = true;
}
}
// If we have migrated to a new version, rewrite the data back to storage
writeToSettings();
}
@@ -161,7 +170,13 @@ void FavoriteLocationsStorage::fromJson(const QJsonArray &arr)
continue;
LocationID loc(entry["type"].toInt(), entry["id"].toInt(), entry["city"].toString());
FavoriteData data;
data.pinnedHostname = entry["pinnedHostname"].toString();
// Store only the subdomain portion so pinned IPs still work when the server TLD changes.
QString hostname = entry["pinnedHostname"].toString();
int dotIndex = hostname.indexOf('.');
if (dotIndex != -1) {
hostname = hostname.left(dotIndex);
}
data.pinnedHostname = hostname;
data.pinnedIp = entry["pinnedIp"].toString();
favoriteLocations_.insert(loc, data);
}
@@ -227,11 +227,13 @@ void LocationsModel::changeConnectionSpeed(LocationID id, PingTime speed)
}
}
void LocationsModel::setFreeSessionStatus(bool isFreeSessionStatus)
void LocationsModel::setFreeSessionStatus(bool isFreeSessionStatus, const QStringList &alcLocations)
{
if (isFreeSessionStatus != isFreeSessionStatus_)
QSet<QString> alcSet(alcLocations.begin(), alcLocations.end());
if (isFreeSessionStatus != isFreeSessionStatus_ || alcSet != alcLocations_)
{
isFreeSessionStatus_ = isFreeSessionStatus;
alcLocations_ = alcSet;
emit dataChanged(index(0, 0), index(locations_.size() - 1, 0));
for (int i = 0; i < locations_.size(); ++i)
{
@@ -561,7 +563,7 @@ QVariant LocationsModel::dataForLocation(int row, int role) const
}
else if (role == kIsShowAsPremium)
{
return locations_[row]->location().isPremiumOnly && isFreeSessionStatus_;
return locations_[row]->location().isPremiumOnly && isFreeSessionStatus_ && !alcLocations_.contains(locations_[row]->location().shortName);
}
else if (role == kIs10Gbps)
{
@@ -686,7 +688,7 @@ QVariant LocationsModel::dataForCity(LocationItem *l, int row, int role) const
if (lid.isStaticIpsLocation())
return false;
else
return l->location().cities[row].isPremiumOnly && isFreeSessionStatus_;
return l->location().cities[row].isPremiumOnly && isFreeSessionStatus_ && !alcLocations_.contains(l->location().shortName);
}
else if (role == kIsFavorite)
{
@@ -49,7 +49,7 @@ public:
void updateBestLocation(const LocationID &bestLocation);
void updateCustomConfigLocation(const types::Location &location);
void changeConnectionSpeed(LocationID id, PingTime speed);
void setFreeSessionStatus(bool isFreeSessionStatus);
void setFreeSessionStatus(bool isFreeSessionStatus, const QStringList &alcLocations);
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
@@ -85,6 +85,7 @@ private:
int *root_; // Fake root node. The typename does not matter, only the pointer to identify the root node matters.
bool isFreeSessionStatus_;
QSet<QString> alcLocations_;
FavoriteLocationsStorage favoriteLocationsStorage_;
RenamedLocationsStorage renamedLocationsStorage_;
const char *BEST_LOCATION_NAME = QT_TR_NOOP("Best Location");
@@ -312,7 +312,7 @@ void TestLocationsModel::testFreeSessionStatusChange()
{
QModelIndex ind = locationsModel_->getIndexByLocationId(LocationID::createApiLocationId(63, "Vancouver", "Granville"));
QVERIFY(ind.data(gui_locations::kIsShowAsPremium).toBool() == false);
locationsModel_->setFreeSessionStatus(true);
locationsModel_->setFreeSessionStatus(true, QStringList());
QVERIFY(ind.data(gui_locations::kIsShowAsPremium).toBool() == true);
}
@@ -70,7 +70,7 @@ void CredentialLineEdit::clear()
void CredentialLineEdit::focusInEvent(QFocusEvent *event)
{
QTimer::singleShot(0, [this]() {
QTimer::singleShot(0, this, [this]() {
lineEdit_->setFocus();
});
}
@@ -179,9 +179,14 @@ void CityItemDelegate::paint(QPainter *painter, const ItemStyleOption &option, c
// Disabled/custom config error/latency
bool showAsPremium = index.data(kIsShowAsPremium).toBool();
// only show disabled locations to pro users
bool disabled = index.data(kIsShowAsPremium).toBool() ? false : index.data(kIsDisabled).toBool();
if (disabled) {
bool disabled = showAsPremium ? false : index.data(kIsDisabled).toBool();
if (showAsPremium) {
// no ping data available for premium locations when user is free
xOffset -= 16*G_SCALE;
xOffset -= LOCATION_ITEM_MARGIN*G_SCALE;
} else if (disabled) {
QSharedPointer<IndependentPixmap> consIcon = ImageResourcesSvg::instance().getIndependentPixmap("locations/UNDER_CONSTRUCTION_ICON");
painter->setOpacity(textOpacity);
xOffset -= consIcon->width();
@@ -235,7 +240,7 @@ void CityItemDelegate::paint(QPainter *painter, const ItemStyleOption &option, c
// no p2p icon + favorite icon
// You can't favorite a static ip or custom config location.
if (!lid.isStaticIpsLocation() && !lid.isCustomConfigsLocation()) {
if (!lid.isStaticIpsLocation() && !lid.isCustomConfigsLocation() && !showAsPremium) {
if (qFuzzyCompare(option.selectedOpacity(), 1.0)) {
QSharedPointer<IndependentPixmap> favIcon;
bool isHoveringFavorite = (option.hoverClickableId() == (int)ClickableRect::kFavorite);
+9 -4
View File
@@ -1088,7 +1088,12 @@ void MainWindow::onPinIp()
}
// Get the current connecting hostname from backend
// Store only the subdomain portion so pinned IPs still work when the server TLD changes.
QString hostname = backend_->getCurrentConnectingHostname();
int dotIndex = hostname.indexOf('.');
if (dotIndex != -1) {
hostname = hostname.left(dotIndex);
}
// Add to favorites with hostname and IP via kPinnedIp role
QVariantList pinnedData;
@@ -1186,7 +1191,7 @@ void MainWindow::onPreferencesLoginClick()
collapsePreferences();
if (backend_->getPreferencesHelper()->isExternalConfigMode()) {
QApplication::setOverrideCursor(Qt::WaitCursor);
setCursor(Qt::WaitCursor);
logoutReason_ = LOGOUT_GO_TO_LOGIN;
backend_->logout(false);
} else if (mainWindowController_->currentWindowAfterAnimation() == MainWindowController::WINDOW_ID_GENERAL_MESSAGE) {
@@ -1580,7 +1585,7 @@ void MainWindow::onUpgradeAccountCancel()
void MainWindow::onLogoutWindowAccept()
{
QApplication::setOverrideCursor(Qt::WaitCursor);
setCursor(Qt::WaitCursor);
setEnabled(false);
logoutReason_ = LOGOUT_FROM_MENU;
isExitingFromPreferences_ = false;
@@ -2389,7 +2394,7 @@ void MainWindow::onBackendLogoutFinished()
mainWindowController_->hideUpdateWidget();
setEnabled(true);
QApplication::restoreOverrideCursor();
unsetCursor();
}
void MainWindow::onBackendCleanupFinished()
@@ -2556,7 +2561,7 @@ void MainWindow::onBackendSessionDeleted()
{
qCInfo(LOG_BASIC) << "Handle deleted session";
QApplication::setOverrideCursor(Qt::WaitCursor);
setCursor(Qt::WaitCursor);
setEnabled(false);
logoutReason_ = LOGOUT_SESSION_EXPIRED;
selectedLocation_->clear();
@@ -355,14 +355,17 @@ void ComboBoxItem::onMenuOpened()
int item = menu_->indexOfItemByName(itemName);
int numItems = menu_->itemCount();
int navigateToIndex = 0;
bool hasScrollBar = false;
if (numItems <= menu_->visibleItems()) // all showing
{
// center on selected item
heightCentering -= item * menu_->itemHeight();
menu_->navigateItemToTop(0);
}
else // scrollbar on
{
hasScrollBar = true;
int offBy = 0;
if (item == numItems - 1) // last item
{
@@ -387,8 +390,7 @@ void ComboBoxItem::onMenuOpened()
offBy = 2;
}
menu_->navigateItemToTop(item - offBy);
menu_->activateItem(item);
navigateToIndex = item - offBy;
}
// Ensure popup visibility when close to the bottom of the current screen.
@@ -400,6 +402,14 @@ void ComboBoxItem::onMenuOpened()
menu_->move(point.x() - offsetX, heightCentering);
menu_->show();
// Navigate and activate after show() so that Qt's first-show layout pass
// does not clobber the scroll position (Qt 6.11 regression).
menu_->navigateItemToTop(navigateToIndex);
if (hasScrollBar) {
menu_->activateItem(item);
}
menu_->setFocus();
}
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 197 KiB