Files
amnezia-client/client/ui/models/newsModel.cpp
Nethius c28452a5da feat: desktop updater (#825)
* added changelog drawer

* Created a scaffold for Linux installation

* implement Linux updating

* Add debug logs about installer in service

* Add client side of installation logic for Windows and MacOS

* Add service side of installation logic for Windows

* ru readme

* Update README_RU.md

* Add files via upload

* chore: added clang-format config files (#1293)

* Update README_RU.md

* Update README.md

* feature: added subscription expiration date for premium v2 (#1261)

* feature: added subscription expiration date for premium v2

* feature: added a check for the presence of the “services” field in the response body of the getServicesList() function

* feature: added prohibition to change location when connection is active

* bugfix: renamed public_key->end_date to public_key->expires_at according to the changes on the backend

* feature/xray user management (#972)

* feature: implement client management functionality for Xray

---------

Co-authored-by: aiamnezia <ai@amnezia.org>
Co-authored-by: vladimir.kuznetsov <nethiuswork@gmail.com>

* Fix formatting

* Add some logs

* Add logs from installattion shell on Windows

* Fix installation for Windows and MacOS

* Optimized code

* Move installer running to client side for Ubuntu

* Move installer launch logic to client side for Windows

* Clean service code

* Add linux_install script to resources

* Add logs for UpdateController

* Add draft for MacOS installation

* Disable updates checking for Android and iOS

* chore: fixed macos update script

* chore: remove duplicate lines

* chore: post merge fixes

* chore: add missing ifdef

* decrease version for testing

* chore: added changelog text processing depend on OS

* add .vscode to .gitignore

* Change updater downloading method to retrieving link from the gateway

* add Release date file creation to s3 deploy script

* Add release date downloading from endpoint

* update check refactoring

* feat: switch macOS auto-update from DMG to ZIP+PKG installer

- Update macOS artifact URL from .dmg to .zip
- Rewrite mac_installer.sh to extract ZIP and install PKG via osascript
- Increase download timeout to 30s for larger ZIP files

* fix: fix Android build

* feat: Change get request for updater link to post

* refactor: preparing NewsModel for update notifications

- Changed `updateModel` to `setNewsList` for better semantic meaning.
- Delegate model container updating to private method updateModel
- Updated the logic for marking news as read to use item IDs instead of a boolean flag.

* feat: Move update notification in news list

- Updated `UpdateController` to handle empty release dates in header text.
- Added `getVersion` method to `UpdateController` for version retrieval.
- Enhanced `NewsModel` to support update notifications with new methods for marking updates as skipped and setting update notifications.
- Updated QML pages to display update information and provide actions for updates and skipping them.
- Introduced `isUpdate` property in `NewsItem` to differentiate between regular news and updates.

* feat: Implement rate limit workaround for gateway requests

- Added a delay before contacting the gateway in both `UpdateController` and `ApiNewsController` to prevent rate limit issues caused by simultaneous requests.

* refactor: Convert synchronous network requests to asynchronous in UpdateController

- Updated `UpdateController` to use asynchronous network requests for fetching gateway URL, version info, changelog, and release date.
- Introduced `doGetAsync` method to handle asynchronous GET requests with error handling.
- Removed synchronous methods to improve responsiveness and prevent blocking the UI during network operations.
- Added a mechanism to prevent multiple concurrent update checks.

* chore: Decrease AmneziaVPN version to 4.8.10.0 in CMakeLists.txt for testing

* refactor: Improve update check handling to avoid rate limit issues

- Updated `CoreController` to initiate update checks after news fetching is complete.
- Removed synchronous waiting in `ApiNewsController` to streamline the fetching process.

* fix: fixed typo in IsReadRole

* fix: fix updater filenames

* chore: move updateController to core

* refactor: update to mvvm

* chore: tiny fix

---------

Co-authored-by: aiamnezia <ai@amnezia.org>
Co-authored-by: aiamnezia <ai@amnezia.com>
Co-authored-by: Pokamest Nikak <pokamest@gmail.com>
Co-authored-by: KsZnak <ksu@amnezia.org>
Co-authored-by: Cyril Anisimov <cyan84@gmail.com>
Co-authored-by: vkamn <vk@amnezia.org>
2026-05-04 12:37:19 +08:00

177 lines
4.5 KiB
C++

#include "ui/models/newsModel.h"
#include "core/repositories/secureAppSettingsRepository.h"
#include <QDir>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QQmlEngine>
#include <QStandardPaths>
#include <algorithm>
NewsModel::NewsModel(SecureAppSettingsRepository* appSettingsRepository, QObject *parent)
: QAbstractListModel(parent), m_appSettingsRepository(appSettingsRepository)
{
loadReadIds();
}
int NewsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_items.size();
}
QVariant NewsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size())
return QVariant();
const NewsItem &item = m_items.at(index.row());
switch (role) {
case IdRole: return item.id;
case TitleRole: return item.title;
case ContentRole: return item.content;
case TimestampRole: return item.timestamp.toLocalTime().toString(Qt::ISODate);
case IsReadRole: return m_readIds.contains(item.id);
case IsProcessedRole: return index.row() == m_processedIndex;
case IsUpdateRole: return item.isUpdate;
default: return QVariant();
}
}
QHash<int, QByteArray> NewsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[IdRole] = "id";
roles[TitleRole] = "title";
roles[ContentRole] = "content";
roles[TimestampRole] = "timestamp";
roles[IsReadRole] = "read";
roles[IsProcessedRole] = "isProcessed";
roles[IsUpdateRole] = "isUpdate";
return roles;
}
void NewsModel::markAsRead(int index)
{
if (index < 0 || index >= m_items.size())
return;
const QString &itemId = m_items.at(index).id;
if (itemId.isEmpty() || m_readIds.contains(itemId))
return;
m_readIds.insert(itemId);
saveReadIds();
QModelIndex idx = createIndex(index, 0);
emit dataChanged(idx, idx, { IsReadRole });
emit hasUnreadChanged();
}
void NewsModel::markUpdateAsSkipped()
{
if (!m_updateItem.has_value())
return;
const QString updateId = m_updateItem->id;
if (updateId.isEmpty())
return;
for (int i = 0; i < m_items.size(); ++i) {
if (m_items.at(i).id == updateId) {
markAsRead(i);
break;
}
}
}
int NewsModel::processedIndex() const
{
return m_processedIndex;
}
void NewsModel::setProcessedIndex(int index)
{
if (index < 0 || index >= m_items.size() || m_processedIndex == index)
return;
m_processedIndex = index;
emit processedIndexChanged(index);
}
void NewsModel::setNewsList(const QJsonArray &serverItems)
{
QVector<NewsItem> updatedItems;
updatedItems.reserve(serverItems.size());
for (const QJsonValue &value : serverItems) {
if (!value.isObject())
continue;
const QJsonObject object = value.toObject();
NewsItem item;
item.id = object.value("id").toString();
if (item.id.isEmpty())
continue;
item.title = object.value("title").toString();
item.content = object.value("content").toString();
item.timestamp = QDateTime::fromString(object.value("timestamp").toString(), Qt::ISODate);
item.isUpdate = false;
updatedItems.append(item);
}
m_apiItems = updatedItems;
updateModel();
}
void NewsModel::setUpdateNotification(const QString &id, const QString &title, const QString &content)
{
if (id.isEmpty())
return;
NewsItem updateItem;
updateItem.id = id;
updateItem.title = title;
updateItem.content = content;
updateItem.timestamp = QDateTime::currentDateTimeUtc();
updateItem.isUpdate = true;
m_updateItem = updateItem;
updateModel();
}
void NewsModel::updateModel()
{
beginResetModel();
m_items = m_apiItems;
std::sort(m_items.begin(), m_items.end(), [](const NewsItem &a, const NewsItem &b) { return a.timestamp > b.timestamp; });
if (m_updateItem.has_value()) {
m_items.prepend(*m_updateItem);
}
endResetModel();
emit hasUnreadChanged();
}
bool NewsModel::hasUnread() const
{
for (const NewsItem &item : m_items) {
if (!m_readIds.contains(item.id))
return true;
}
return false;
}
void NewsModel::loadReadIds()
{
QStringList ids = m_appSettingsRepository->getReadNewsIds();
m_readIds = QSet<QString>(ids.begin(), ids.end());
}
void NewsModel::saveReadIds() const
{
m_appSettingsRepository->setReadNewsIds(QStringList(m_readIds.begin(), m_readIds.end()));
}