diff --git a/retroshare-gui/src/gui/Posted/PostedDialog.cpp b/retroshare-gui/src/gui/Posted/PostedDialog.cpp index 733878810..3bd30264c 100644 --- a/retroshare-gui/src/gui/Posted/PostedDialog.cpp +++ b/retroshare-gui/src/gui/Posted/PostedDialog.cpp @@ -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()); } diff --git a/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp b/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp index 084c9a808..02ce7d978 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp +++ b/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp @@ -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 @@ -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); } diff --git a/retroshare-gui/src/gui/common/AvatarDefs.cpp b/retroshare-gui/src/gui/common/AvatarDefs.cpp index 608e6e285..ac6017fa5 100644 --- a/retroshare-gui/src/gui/common/AvatarDefs.cpp +++ b/retroshare-gui/src/gui/common/AvatarDefs.cpp @@ -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); diff --git a/retroshare-gui/src/gui/common/FriendListModel.cpp b/retroshare-gui/src/gui/common/FriendListModel.cpp index 94fa21ebd..ab238cf03 100644 --- a/retroshare-gui/src/gui/common/FriendListModel.cpp +++ b/retroshare-gui/src/gui/common/FriendListModel.cpp @@ -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(); } - diff --git a/retroshare-gui/src/gui/gxs/GxsIdDetails.cpp b/retroshare-gui/src/gui/gxs/GxsIdDetails.cpp index 5c1c11b49..af14551c2 100644 --- a/retroshare-gui/src/gui/gxs/GxsIdDetails.cpp +++ b/retroshare-gui/src/gui/gxs/GxsIdDetails.cpp @@ -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[4] > GxsIdDetails::mDefaultIconCache ; +std::map[NUM_AVATAR_SIZES] > GxsIdDetails::mDefaultIconCache ; +std::map[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 +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;isecond[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(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;i0) + { + 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;ifirst << "." << 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; } } diff --git a/retroshare-gui/src/gui/gxs/GxsIdDetails.h b/retroshare-gui/src/gui/gxs/GxsIdDetails.h index 122b5e089..79f3bcc9d 100644 --- a/retroshare-gui/src/gui/gxs/GxsIdDetails.h +++ b/retroshare-gui/src/gui/gxs/GxsIdDetails.h @@ -28,6 +28,8 @@ #include #include +#include + #include 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[4] > mDefaultIconCache; + static std::map[4] > mDefaultGroupIconCache; static time_t mLastIconCacheCleaning; int mCheckTimerId; diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelDialog.cpp b/retroshare-gui/src/gui/gxschannels/GxsChannelDialog.cpp index 31dcc25a1..035d2778e 100644 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelDialog.cpp +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelDialog.cpp @@ -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()); } diff --git a/retroshare-gui/src/gui/icons.qrc b/retroshare-gui/src/gui/icons.qrc index 4d7c2e46e..58819ecdc 100644 --- a/retroshare-gui/src/gui/icons.qrc +++ b/retroshare-gui/src/gui/icons.qrc @@ -267,6 +267,10 @@ icons/yahoo.png icons/yandex.png icons/png/markdown-mark.png + icons/board.png + icons/channel.png + icons/person.png + icons/wiki.png icons/png/channel.png icons/png/circles-black.png icons/png/circles-gray.png diff --git a/retroshare-gui/src/gui/icons/board.png b/retroshare-gui/src/gui/icons/board.png new file mode 100644 index 000000000..034d65667 Binary files /dev/null and b/retroshare-gui/src/gui/icons/board.png differ diff --git a/retroshare-gui/src/gui/icons/channel.png b/retroshare-gui/src/gui/icons/channel.png new file mode 100644 index 000000000..b26cd02ed Binary files /dev/null and b/retroshare-gui/src/gui/icons/channel.png differ diff --git a/retroshare-gui/src/gui/icons/person.png b/retroshare-gui/src/gui/icons/person.png new file mode 100644 index 000000000..2404a9520 Binary files /dev/null and b/retroshare-gui/src/gui/icons/person.png differ diff --git a/retroshare-gui/src/gui/icons/wiki.png b/retroshare-gui/src/gui/icons/wiki.png new file mode 100644 index 000000000..af853a7ce Binary files /dev/null and b/retroshare-gui/src/gui/icons/wiki.png differ