Merge pull request #3122 from samuel-asleep/master-clean

Implement deterministic colored default logos for Friend Nodes, Chann…
This commit is contained in:
csoler 2026-01-17 22:13:27 +01:00 committed by GitHub
commit b681603cb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 257 additions and 60 deletions

View File

@ -24,6 +24,7 @@
#include "PostedListWidgetWithModel.h"
#include "PostedUserNotify.h"
#include "gui/gxs/GxsGroupShareKey.h"
#include "gui/gxs/GxsIdDetails.h"
#include "gui/settings/rsharesettings.h"
#include "gui/common/GroupTreeWidget.h"
#include "util/misc.h"
@ -236,7 +237,7 @@ void PostedDialog::groupInfoToGroupItemInfo(const RsGxsGenericGroupData *groupDa
groupItemInfo.icon = image;
}
else
groupItemInfo.icon = FilesDefs::getIconFromQtResourcePath(":icons/png/postedlinks.png");
groupItemInfo.icon = GxsIdDetails::makeDefaultGroupIcon(postedGroupData->mMeta.mGroupId, ":icons/board.png", GxsIdDetails::ORIGINAL);
groupItemInfo.description = QString::fromUtf8(postedGroupData->mDescription.c_str());
}

View File

@ -30,6 +30,7 @@
#include "gui/WikiPoos/WikiEditDialog.h"
#include "gui/settings/rsharesettings.h"
#include "gui/gxs/WikiGroupDialog.h"
#include "gui/gxs/GxsIdDetails.h"
#include "util/DateTime.h"
#include <retroshare/rswiki.h>
@ -76,7 +77,7 @@
#define IMAGE_NEWFORUM ":/images/new_forum16.png"
#define IMAGE_FORUMAUTHD ":/images/konv_message2.png"
#define IMAGE_COPYLINK ":/images/copyrslink.png"
#define IMAGE_WIKI ":/icons/png/wiki.png"
#define IMAGE_WIKI ":icons/wiki.png"
#define IMAGE_EDIT ":/icons/png/pencil-edit-button.png"
@ -730,7 +731,7 @@ void WikiDialog::GroupMetaDataToGroupItemInfo(const RsGroupMetaData &groupInfo,
groupItemInfo.lastpost = DateTime::DateTimeFromTime_t(groupInfo.mLastPost);
groupItemInfo.subscribeFlags = groupInfo.mSubscribeFlags;
groupItemInfo.icon = QIcon(IMAGE_WIKI);
groupItemInfo.icon = GxsIdDetails::makeDefaultGroupIcon(groupInfo.mGroupId, IMAGE_WIKI, GxsIdDetails::ORIGINAL);
}

View File

@ -55,7 +55,7 @@ bool AvatarDefs::getAvatarFromSslId(const RsPeerId& sslId, QPixmap &avatar, cons
rsChats->getAvatarData(RsPeerId(sslId), data, size);
if (size == 0) {
if (!defaultImage.isEmpty()) {
avatar = FilesDefs::getPixmapFromQtResourcePath(defaultImage);
avatar = GxsIdDetails::makeDefaultGroupIconFromString(QString::fromStdString(sslId.toStdString()), ":icons/person.png", GxsIdDetails::LARGE);
}
return false;
}
@ -109,13 +109,15 @@ bool AvatarDefs::getAvatarFromGpgId(const RsPgpId& gpgId, QPixmap &avatar, const
}
}
if (size == 0) {
avatar = FilesDefs::getPixmapFromQtResourcePath(defaultImage);
return false;
}
if (size == 0) {
if (!defaultImage.isEmpty()) {
avatar = GxsIdDetails::makeDefaultGroupIconFromString(QString::fromStdString(gpgId.toStdString()), ":icons/person.png", GxsIdDetails::LARGE);
}
return false;
}
/* load image */
GxsIdDetails::loadPixmapFromData(data, size, avatar);
GxsIdDetails::loadPixmapFromData(data, size, avatar, GxsIdDetails::LARGE);
free(data);

View File

@ -1027,7 +1027,19 @@ QVariant RsFriendListModel::decorationRole(const EntryIndex& entry,int col) cons
}
if (!foundAvatar || sslAvatar.isNull()) {
sslAvatar = FilesDefs::getPixmapFromQtResourcePath(AVATAR_DEFAULT_IMAGE);
if (hn && !hn->child_node_indices.empty()) {
// Use first child node if available
std::string sslIdStr = mLocations[hn->child_node_indices[0]].node_info.id.toStdString();
sslAvatar = GxsIdDetails::makeDefaultGroupIconFromString(QString::fromStdString(sslIdStr), ":icons/person.png", GxsIdDetails::LARGE);
}
else if (bestNodeInformation != nullptr) {
// Fallback: Use the bestNodeInformation ID if no children exist
std::string bestSslIdStr = bestNodeInformation->node_info.id.toStdString();
sslAvatar = GxsIdDetails::makeDefaultGroupIconFromString(QString::fromStdString(bestSslIdStr), ":icons/person.png", GxsIdDetails::LARGE);
}
else {
sslAvatar = FilesDefs::getPixmapFromQtResourcePath(AVATAR_DEFAULT_IMAGE);
}
}
if (mDisplayStatusIcon) {
@ -1489,4 +1501,3 @@ bool RsFriendListModel::isProfileExpanded(const EntryIndex& e) const
return mExpandedProfiles.find(s) != mExpandedProfiles.end();
}

View File

@ -71,15 +71,81 @@
//const int kRecognTagType_Dev_Patcher = 4;
//const int kRecognTagType_Dev_Developer = 5;
#define ICON_CACHE_STORAGE_TIME 240
#define DELAY_BETWEEN_ICON_CACHE_CLEANING 120
#define NUM_AVATAR_SIZES 4 // Number of AvatarSize enum values (SMALL, MEDIUM, LARGE, ORIGINAL)
uint32_t GxsIdDetails::mImagesAllocated = 0;
time_t GxsIdDetails::mLastIconCacheCleaning = time(NULL);
std::map<RsGxsId,std::pair<time_t,QPixmap>[4] > GxsIdDetails::mDefaultIconCache ;
std::map<RsGxsId,std::pair<time_t,QPixmap>[NUM_AVATAR_SIZES] > GxsIdDetails::mDefaultIconCache ;
std::map<std::string,std::pair<time_t,QPixmap>[NUM_AVATAR_SIZES] > GxsIdDetails::mDefaultGroupIconCache ;
QMutex GxsIdDetails::mMutex;
QMutex GxsIdDetails::mIconCacheMutex;
#define ICON_CACHE_STORAGE_TIME 240
#define DELAY_BETWEEN_ICON_CACHE_CLEANING 120
static std::string groupIconCacheKey(const RsGxsGroupId& id, const QString& iconPath)
{
std::string key = id.toStdString();
key.append("|");
key.append(iconPath.toStdString());
return key;
}
static std::string groupIconCacheKeyFromString(const QString& idStr, const QString& iconPath)
{
std::string key = idStr.toStdString();
key.append("|");
key.append(iconPath.toStdString());
return key;
}
template<typename CacheMap>
static void cleanupIconCache(CacheMap& cache, time_t now, int& nb_deleted, uint32_t& size_deleted, uint32_t& total_size, const char* label)
{
for(auto it(cache.begin());it!=cache.end();)
{
bool all_empty = true ;
#ifdef DEBUG_GXSIDDETAILS
std::cerr << " Examining pixmaps sizes for " << label << " " << it->first << "." << std::endl;
#endif
for(int i=0;i<NUM_AVATAR_SIZES;++i)
if(it->second[i].first>0)
{
if(it->second[i].first + ICON_CACHE_STORAGE_TIME < now && it->second[i].second.isDetached())
{
int s = it->second[i].second.width()*it->second[i].second.height()*4;
#ifdef DEBUG_GXSIDDETAILS
std::cerr << " Deleting pixmap " << it->first << " size " << i << " " << s << " bytes." << std::endl;
#endif
it->second[i].second = QPixmap();
it->second[i].first = 0;
++nb_deleted;
size_deleted += s;
}
else
{
all_empty = false;
total_size += it->second[i].second.width()*it->second[i].second.height()*4;
#ifdef DEBUG_GXSIDDETAILS
std::cerr << " Keeping " << it->first << " size " << i << std::endl;
#endif
}
}
if(all_empty)
{
#ifdef DEBUG_GXSIDDETAILS
std::cerr << " Deleting entry " << it->first << " because no pixmaps are stored here. " << std::endl;
#endif
it = cache.erase(it);
}
else
++it;
}
}
void ReputationItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
@ -420,6 +486,13 @@ const QPixmap GxsIdDetails::makeDefaultIcon(const RsGxsId& id, AvatarSize size)
if(id.isNull())
std::cerr << "Weird: null ID" << std::endl;
// Bounds check to prevent array overflow
if((int)size >= NUM_AVATAR_SIZES || (int)size < 0)
{
std::cerr << "Warning: invalid avatar size " << (int)size << ", using MEDIUM" << std::endl;
size = MEDIUM;
}
QMutexLocker lock(&mIconCacheMutex);
auto& it = mDefaultIconCache[id];
@ -447,6 +520,107 @@ const QPixmap GxsIdDetails::makeDefaultIcon(const RsGxsId& id, AvatarSize size)
return image;
}
QPixmap GxsIdDetails::generateColoredIcon(const QString& idStr, const QString& iconPath, int size)
{
// Generate a color from the ID hash
uint hash = qHash(idStr);
// Use hash to determine hue (0-359), with fixed saturation and lightness for pastel look
int hue = hash % 360;
int saturation = 150; // Mid saturation for pastel colors
int lightness = 180; // Light for pastel colors
QColor backgroundColor = QColor::fromHsl(hue, saturation, lightness);
// Create the pixmap
QPixmap pixmap(size, size);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
// Draw colored circle
painter.setPen(Qt::NoPen);
painter.setBrush(backgroundColor);
painter.drawEllipse(0, 0, size, size);
// Load and overlay the category icon
QPixmap categoryIcon(iconPath);
if (!categoryIcon.isNull())
{
// Scale icon to ~70% of total size
int iconSize = static_cast<int>(size * 0.7);
QPixmap scaledIcon = categoryIcon.scaled(iconSize, iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
// Center the icon
int x = (size - scaledIcon.width()) / 2;
int y = (size - scaledIcon.height()) / 2;
painter.drawPixmap(x, y, scaledIcon);
}
painter.end();
return pixmap;
}
const QPixmap GxsIdDetails::makeDefaultGroupIcon(const RsGxsGroupId& id, const QString& iconPath, AvatarSize size)
{
checkCleanImagesCache();
time_t now = time(NULL);
if(id.isNull())
std::cerr << "Weird: null ID" << std::endl;
// Bounds check to prevent array overflow
if((int)size >= NUM_AVATAR_SIZES || (int)size < 0)
{
std::cerr << "Warning: invalid avatar size " << (int)size << ", using MEDIUM" << std::endl;
size = MEDIUM;
}
QMutexLocker lock(&mIconCacheMutex);
auto& it = mDefaultGroupIconCache[groupIconCacheKey(id, iconPath)];
if(it[(int)size].second.width() > 0)
{
it[(int)size].first = now;
return it[(int)size].second;
}
int S = 0;
switch(size)
{
case SMALL: S = 48 ; break;
default:
case MEDIUM: S = 96 ; break;
case ORIGINAL:
case LARGE: S = 192 ; break;
}
QPixmap pixmap = generateColoredIcon(QString::fromStdString(id.toStdString()), iconPath, S);
it[(int)size] = std::make_pair(now, pixmap);
return pixmap;
}
const QPixmap GxsIdDetails::makeDefaultGroupIcon(const QString& idStr, const QString& iconPath, AvatarSize size)
{
// Delegate to the RsGxsGroupId overload to avoid duplicating caching logic
RsGxsGroupId id(idStr.toStdString());
return makeDefaultGroupIcon(id, iconPath, size);
}
const QPixmap GxsIdDetails::makeDefaultGroupIconFromString(const QString& idStr, const QString& iconPath, AvatarSize size)
{
// Delegate to the existing QString overload so that all string-based
// calls share the same cache keys and entries.
return makeDefaultGroupIcon(idStr, iconPath, size);
}
void GxsIdDetails::debug_dumpImagesCache()
{
QMutexLocker lock(&mIconCacheMutex);
@ -457,7 +631,25 @@ void GxsIdDetails::debug_dumpImagesCache()
{
std::cerr << " Identity " << it.first << ":" << std::endl;
for(uint32_t i=0;i<4;++i)
for(uint32_t i=0;i<NUM_AVATAR_SIZES;++i)
{
std::cerr << " Size #" << i << ": " ;
if(it.second[i].first>0)
{
int s = it.second[i].second.width()*it.second[i].second.height()*4;
std::cerr << " Present. Size=" << s << " bytes. Age: " << time(nullptr)-it.second[i].first << " secs. ago. Used: " << !it.second[i].second.isDetached() << std::endl;
}
else
std::cerr << " None." << std::endl;
}
}
for(const auto& it:mDefaultGroupIconCache)
{
std::cerr << " Group " << it.first << ":" << std::endl;
for(uint32_t i=0;i<NUM_AVATAR_SIZES;++i)
{
std::cerr << " Size #" << i << ": " ;
@ -489,52 +681,12 @@ void GxsIdDetails::checkCleanImagesCache()
QMutexLocker lock(&mIconCacheMutex);
for(auto it(mDefaultIconCache.begin());it!=mDefaultIconCache.end();)
{
bool all_empty = true ;
#ifdef DEBUG_GXSIDDETAILS
std::cerr << " Examining pixmaps sizes for " << it->first << "." << std::endl;
#endif
for(int i=0;i<4;++i)
if(it->second[i].first>0)
{
if(it->second[i].first + ICON_CACHE_STORAGE_TIME < now && it->second[i].second.isDetached())
{
int s = it->second[i].second.width()*it->second[i].second.height()*4;
#ifdef DEBUG_GXSIDDETAILS
std::cerr << " Deleting pixmap " << it->first << " size " << i << " " << s << " bytes." << std::endl;
#endif
it->second[i].second = QPixmap();
it->second[i].first = 0;
++nb_deleted;
size_deleted += s;
}
else
{
all_empty = false;
total_size += it->second[i].second.width()*it->second[i].second.height()*4;
#ifdef DEBUG_GXSIDDETAILS
std::cerr << " Keeking " << it->first << " size " << i << std::endl;
#endif
}
}
if(all_empty)
{
#ifdef DEBUG_GXSIDDETAILS
std::cerr << " Deleting entry " << it->first << " because no pixmaps are stored here. " << std::endl;
#endif
it = mDefaultIconCache.erase(it);
}
else
++it;
}
cleanupIconCache(mDefaultIconCache, now, nb_deleted, size_deleted, total_size, "identity");
cleanupIconCache(mDefaultGroupIconCache, now, nb_deleted, size_deleted, total_size, "group");
mLastIconCacheCleaning = now;
std::cerr << "(II) Removed " << nb_deleted << " (" << size_deleted << " bytes) unused icons. Cache contains " << mDefaultIconCache.size() << " icons (" << total_size << " bytes)"<< std::endl;
std::cerr << "(II) Removed " << nb_deleted << " (" << size_deleted << " bytes) unused icons. Cache contains "
<< (mDefaultIconCache.size() + mDefaultGroupIconCache.size()) << " icons (" << total_size << " bytes)"<< std::endl;
}
}

View File

@ -28,6 +28,8 @@
#include <QString>
#include <QStyledItemDelegate>
#include <string>
#include <retroshare/rsidentity.h>
class QLabel;
@ -113,6 +115,27 @@ public:
// These two methods use a cache so as to minimize the memory impact of avatars.
static const QPixmap makeDefaultIcon(const RsGxsId& id, AvatarSize size = MEDIUM);
static const QPixmap makeDefaultGroupIcon(const RsGxsGroupId& id, const QString& iconPath, AvatarSize size = MEDIUM);
static const QPixmap makeDefaultGroupIcon(const QString& idStr, const QString& iconPath, AvatarSize size = MEDIUM);
/*!
* \brief makeDefaultGroupIconFromString
*
* Create a default group icon for identifiers that are not GXS group IDs
* (e.g. RsPeerId, RsPgpId) but are only available as strings.
*
* This overload uses a distinct cache key strategy from the
* makeDefaultGroupIcon(const QString&, ...) overload, to avoid collisions
* with icons generated for real RsGxsGroupId-based group identifiers. In
* particular, the full string identifier (and optionally a type-specific
* prefix in the implementation) is used as the cache key rather than
* assuming that the string encodes a GXS group ID.
*
* Use this function when you need a "group-like" icon for non-GXS ID
* types (such as RsPeerId or RsPgpId) and you only have a textual
* representation. For actual GXS group identifiers, prefer:
* - makeDefaultGroupIcon(const RsGxsGroupId&, const QString&, AvatarSize)
* - makeDefaultGroupIcon(const QString&, const QString&, AvatarSize)
*/
static bool loadPixmapFromData(const unsigned char *data, size_t data_len, QPixmap& pix, AvatarSize size = MEDIUM);
static void checkCleanImagesCache();
static void debug_dumpImagesCache();
@ -140,6 +163,7 @@ private:
qreal shapeangle, qreal angle,
quint16 size, QColor fillColor);
static QPixmap drawIdentIcon(QString hash, quint16 width, bool rotate);
static QPixmap generateColoredIcon(const QString& idStr, const QString& iconPath, int size);
private slots:
void objectDestroyed(QObject *object);
@ -172,6 +196,7 @@ protected:
static uint32_t mImagesAllocated;
static std::map<RsGxsId,std::pair<time_t,QPixmap>[4] > mDefaultIconCache;
static std::map<std::string,std::pair<time_t,QPixmap>[4] > mDefaultGroupIconCache;
static time_t mLastIconCacheCleaning;
int mCheckTimerId;

View File

@ -30,6 +30,7 @@
#include "CreateGxsChannelMsg.h"
#include "GxsChannelUserNotify.h"
#include "gui/gxs/GxsGroupShareKey.h"
#include "gui/gxs/GxsIdDetails.h"
#include "gui/feeds/GxsChannelPostItem.h"
#include "gui/settings/rsharesettings.h"
#include "gui/common/GroupTreeWidget.h"
@ -426,7 +427,7 @@ void GxsChannelDialog::groupInfoToGroupItemInfo(const RsGxsGenericGroupData *gro
groupItemInfo.icon = image;
}
else
groupItemInfo.icon = FilesDefs::getIconFromQtResourcePath(":icons/png/channel.png");
groupItemInfo.icon = GxsIdDetails::makeDefaultGroupIcon(channelGroupData->mMeta.mGroupId, ":icons/channel.png", GxsIdDetails::ORIGINAL);
groupItemInfo.description = QString::fromUtf8(channelGroupData->mDescription.c_str());
}

View File

@ -267,6 +267,10 @@
<file>icons/yahoo.png</file>
<file>icons/yandex.png</file>
<file>icons/png/markdown-mark.png</file>
<file>icons/board.png</file>
<file>icons/channel.png</file>
<file>icons/person.png</file>
<file>icons/wiki.png</file>
<file>icons/png/channel.png</file>
<file>icons/png/circles-black.png</file>
<file>icons/png/circles-gray.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB