From 8046c235b1f593d8e1404065f7d6319f02e198c5 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:22:29 +0200 Subject: [PATCH 1/3] Added rs wiki links Enabled open links in Textbrowser retroshare links now functional inside a webpage Added markup default text cheats Aded image attachaments --- retroshare-gui/src/gui/RetroShareLink.cpp | 98 +++++++- retroshare-gui/src/gui/RetroShareLink.h | 3 +- .../src/gui/WikiPoos/WikiDialog.cpp | 234 +++++++++++++++++- retroshare-gui/src/gui/WikiPoos/WikiDialog.h | 19 ++ retroshare-gui/src/gui/WikiPoos/WikiDialog.ui | 10 +- .../src/gui/WikiPoos/WikiEditDialog.cpp | 184 ++++++++++++++ .../src/gui/WikiPoos/WikiEditDialog.h | 13 + .../src/gui/WikiPoos/WikiEditDialog.ui | 157 +++++++++++- 8 files changed, 697 insertions(+), 21 deletions(-) diff --git a/retroshare-gui/src/gui/RetroShareLink.cpp b/retroshare-gui/src/gui/RetroShareLink.cpp index 35fc3d836..538f179ef 100644 --- a/retroshare-gui/src/gui/RetroShareLink.cpp +++ b/retroshare-gui/src/gui/RetroShareLink.cpp @@ -39,6 +39,9 @@ #endif #include "msgs/MessageComposer.h" #include "Posted/PostedDialog.h" +#ifdef RS_USE_WIKI +#include "WikiPoos/WikiDialog.h" +#endif #include "util/misc.h" #include @@ -79,9 +82,10 @@ #define HOST_FILE_TREE "collection" #define HOST_CHAT_ROOM "chat_room" #define HOST_CIRCLE "circle" -#define HOST_REGEXP "file|person|forum|channel|wire|search|message|certificate|extra|private_chat|public_msg|posted|identity|collection|chat_room|circle" +#define HOST_REGEXP "file|person|forum|channel|wire|search|message|certificate|extra|private_chat|public_msg|posted|identity|collection|chat_room|circle|wiki" #define HOST_WIRE "wire" +#define HOST_WIKI "wiki" #define FILE_NAME "name" #define FILE_SIZE "size" #define FILE_HASH "hash" @@ -102,6 +106,10 @@ #define WIRE_ID "id" #define WIRE_MSGID "msgid" +#define WIKI_NAME "name" +#define WIKI_ID "id" +#define WIKI_MSGID "msgid" + #define SEARCH_KEYWORDS "keywords" #define MESSAGE_ID "id" @@ -315,6 +323,19 @@ void RetroShareLink::fromUrl(const QUrl& url) return; } + if (url.host() == HOST_WIKI) { + _type = TYPE_WIKI; + _name = decodedQueryItemValue(urlQuery, WIKI_NAME); + _hash = urlQuery.queryItemValue(WIKI_ID); + _msgId = urlQuery.queryItemValue(WIKI_MSGID); + +#ifdef DEBUG_RSLINK + std::cerr << "Got a wiki link!!" << std::endl; +#endif + check(); + return; + } + if (url.host() == HOST_SEARCH) { _type = TYPE_SEARCH; _name = decodedQueryItemValue(urlQuery, SEARCH_KEYWORDS); @@ -857,6 +878,17 @@ void RetroShareLink::check() _valid = false; break; + case TYPE_WIKI: + if(_size != 0) + _valid = false; + + if(_name.isEmpty()) + _valid = false; + + if(_hash.isEmpty()) + _valid = false; + break; + case TYPE_SEARCH: if(_size != 0) _valid = false; @@ -963,6 +995,8 @@ QString RetroShareLink::title() const return QString("Channel id: %1").arg(hash()); case TYPE_WIRE: return QString("Wire id: %1").arg(hash()); + case TYPE_WIKI: + return QString("Wiki id: %1").arg(hash()); case TYPE_SEARCH: return QString("Search files"); @@ -1172,6 +1206,17 @@ QString RetroShareLink::toString() const break; + case TYPE_WIKI: + url.setScheme(RSLINK_SCHEME); + url.setHost(HOST_WIKI); + urlQuery.addQueryItem(WIKI_NAME, encodeItem(_name)); + urlQuery.addQueryItem(WIKI_ID, _hash); + if (!_msgId.isEmpty()) { + urlQuery.addQueryItem(WIKI_MSGID, _msgId); + } + + break; + } #if QT_VERSION >= QT_VERSION_CHECK(5,0,0) @@ -1418,6 +1463,7 @@ static void processList(const QStringList &list, const QString &textSingular, co case TYPE_FILE_TREE: case TYPE_CHAT_ROOM: case TYPE_CIRCLES: + case TYPE_WIKI: // no need to ask break; @@ -1487,12 +1533,18 @@ static void processList(const QStringList &list, const QString &textSingular, co QStringList chatroomFound; QStringList chatroomUnknown; + // wiki + QStringList wikiFound; + QStringList wikiMsgFound; + QStringList wikiUnknown; + QStringList wikiMsgUnknown; + // for summary at end QList processedList; QList errorList; - processedList << &fileAdded << &personAdded << &forumFound << &channelFound << &wireFound << &searchStarted << &messageStarted << &postedFound << &chatroomFound; - errorList << &fileExist << &personExist << &personFailed << &personNotFound << &forumUnknown << &forumMsgUnknown << &channelUnknown << &channelMsgUnknown << &wireUnknown << &wireMsgUnknown << &messageReceipientNotAccepted << &messageReceipientUnknown << &postedUnknown << &postedMsgUnknown << &chatroomUnknown; + processedList << &fileAdded << &personAdded << &forumFound << &channelFound << &wireFound << &searchStarted << &messageStarted << &postedFound << &chatroomFound << &wikiFound; + errorList << &fileExist << &personExist << &personFailed << &personNotFound << &forumUnknown << &forumMsgUnknown << &channelUnknown << &channelMsgUnknown << &wireUnknown << &wireMsgUnknown << &messageReceipientNotAccepted << &messageReceipientUnknown << &postedUnknown << &postedMsgUnknown << &chatroomUnknown << &wikiUnknown << &wikiMsgUnknown; // not needed: forumFound, channelFound, wireFound, messageStarted // we want to merge all single file links into one collection @@ -1933,6 +1985,36 @@ static void processList(const QStringList &list, const QString &textSingular, co } break; +#ifdef RS_USE_WIKI + case TYPE_WIKI: + { +#ifdef DEBUG_RSLINK + std::cerr << " RetroShareLink::process WikiRequest : name : " << link.name().toStdString() << ". id : " << link.hash().toStdString() << ". msgId : " << link.msgId().toStdString() << std::endl; +#endif + + MainWindow::showWindow(MainWindow::Wiki); + WikiDialog *wikiDialog = dynamic_cast(MainWindow::getPage(MainWindow::Wiki)); + if (!wikiDialog) { + return false; + } + + if (wikiDialog->navigate(RsGxsGroupId(link.id().toStdString()), RsGxsMessageId(link.msgId().toStdString()))) { + if (link.msgId().isEmpty()) { + wikiFound.append(link.name()); + } else { + wikiMsgFound.append(link.name()); + } + } else { + if (link.msgId().isEmpty()) { + wikiUnknown.append(link.name()); + } else { + wikiMsgUnknown.append(link.name()); + } + } + } + break; +#endif + default: std::cerr << " RetroShareLink::process unknown type: " << link.type() << std::endl; ++countUnknown; @@ -2049,6 +2131,16 @@ static void processList(const QStringList &list, const QString &textSingular, co } } + // wiki + if (flag & RSLINK_PROCESS_NOTIFY_ERROR) { + if (!wikiUnknown.isEmpty()) { + processList(wikiUnknown, QObject::tr("Wiki not found"), QObject::tr("Wikis not found"), result); + } + if (!wikiMsgUnknown.isEmpty()) { + processList(wikiMsgUnknown, QObject::tr("Wiki page not found"), QObject::tr("Wiki pages not found"), result); + } + } + // chat room if (flag & RSLINK_PROCESS_NOTIFY_ERROR) { if (!chatroomUnknown.isEmpty()) { diff --git a/retroshare-gui/src/gui/RetroShareLink.h b/retroshare-gui/src/gui/RetroShareLink.h index 2b25e2a2d..d14fe33a4 100644 --- a/retroshare-gui/src/gui/RetroShareLink.h +++ b/retroshare-gui/src/gui/RetroShareLink.h @@ -77,7 +77,8 @@ class RetroShareLink TYPE_FILE_TREE = 0x0d, TYPE_CHAT_ROOM = 0x0e, TYPE_WIRE = 0x0f, - TYPE_CIRCLES = 0x10 + TYPE_CIRCLES = 0x10, + TYPE_WIKI = 0x11 }; public: diff --git a/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp b/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp index 1c92d2840..0c9d16b5f 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp +++ b/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp @@ -31,6 +31,7 @@ #include "WikiDialog.h" #include "WikiUserNotify.h" +#include "gui/RetroShareLink.h" #include "gui/WikiPoos/WikiAddDialog.h" #include "gui/WikiPoos/WikiEditDialog.h" #include "gui/settings/rsharesettings.h" @@ -53,6 +54,8 @@ #include #include #include +#include +#include #include @@ -107,6 +110,9 @@ WikiDialog::WikiDialog(QWidget *parent) : /* Invoke the Qt Designer generated object setup routine */ ui.setupUi(this); + mNavigatePendingPageId.clear(); + mShouldUpdateGroupStatistics = false; + mLastGroupStatisticsUpdateTs = 0; mAddPageDialog = NULL; mAddGroupDialog = NULL; mEditDialog = NULL; @@ -409,6 +415,11 @@ void WikiDialog::pagesTreeCustomPopupMenu(QPoint point) contextMenu.addSeparator(); + QAction *copyLinkAction = contextMenu.addAction(QIcon(IMAGE_COPYLINK), tr("Copy RetroShare Link"), this, SLOT(copyWikiPageLink())); + copyLinkAction->setEnabled(hasSelection); + + contextMenu.addSeparator(); + QAction *showPageIdAction = contextMenu.addAction(tr("Show Page Id")); showPageIdAction->setCheckable(true); showPageIdAction->setChecked(!ui.treeWidget_Pages->isColumnHidden(WIKI_GROUP_COL_PAGEID)); @@ -631,6 +642,10 @@ void WikiDialog::loadGroupMeta() if (groupMeta.size() > 0) { insertGroupsData(groupMeta); + for (const auto &meta : groupMeta) + { + updateGroupStatistics(meta.mGroupId); + } if (!mGroupId.isNull()) { const int subscribeFlags = ui.groupTreeWidget->subscribeFlags( @@ -709,6 +724,16 @@ void WikiDialog::loadPages(const RsGxsGroupId &groupId) ui.treeWidget_Pages->addTopLevelItem(pageItem); } + + if (!mNavigatePendingPageId.isNull()) + { + QTreeWidgetItem *item = findPageItem(mNavigatePendingPageId); + if (item) + { + ui.treeWidget_Pages->setCurrentItem(item); + mNavigatePendingPageId.clear(); + } + } }, this); }); } @@ -881,10 +906,18 @@ void WikiDialog::groupListCustomPopupMenu(QPoint /*point*/) //restoreKeysAct->setEnabled(!mForumId.empty() && !IS_GROUP_ADMIN(subscribeFlags)); //contextMnu.addAction( restoreKeysAct); - //action = contextMnu.addAction(QIcon(IMAGE_COPYLINK), tr("Copy RetroShare Link"), this, SLOT(copyForumLink())); - //action->setEnabled(!mForumId.empty()); + contextMnu.addSeparator(); - //contextMnu.addSeparator(); + action = contextMnu.addAction(QIcon(IMAGE_COPYLINK), tr("Copy RetroShare Link"), this, SLOT(copyWikiGroupLink())); + action->setEnabled(!mGroupId.isNull()); + + contextMnu.addSeparator(); + + QAction *markReadAction = contextMnu.addAction(QIcon(":/images/message-mail-read.png"), tr("Mark all as read"), this, SLOT(markGroupAsRead())); + markReadAction->setEnabled(!mGroupId.isNull() && IS_GROUP_SUBSCRIBED(subscribeFlags)); + + QAction *markUnreadAction = contextMnu.addAction(QIcon(":/images/message-mail.png"), tr("Mark all as unread"), this, SLOT(markGroupAsUnread())); + markUnreadAction->setEnabled(!mGroupId.isNull() && IS_GROUP_SUBSCRIBED(subscribeFlags)); contextMnu.exec(QCursor::pos()); } @@ -990,6 +1023,7 @@ void WikiDialog::handleEvent_main_thread(std::shared_ptr event) int subscribeFlags = ui.groupTreeWidget->subscribeFlags(QString::fromStdString(mGroupId.toStdString())); updateModerationState(mGroupId, subscribeFlags); } + updateGroupStatistics(e->mWikiGroupId); break; case RsWikiEventCode::UPDATED_SNAPSHOT: case RsWikiEventCode::NEW_SNAPSHOT: @@ -997,6 +1031,7 @@ void WikiDialog::handleEvent_main_thread(std::shared_ptr event) if (e->mWikiGroupId == mGroupId) { wikiGroupChanged(QString::fromStdString(mGroupId.toStdString())); } + updateGroupStatistics(e->mWikiGroupId); break; case RsWikiEventCode::SUBSCRIBE_STATUS_CHANGED: // Refresh group list to update subscription status @@ -1006,7 +1041,11 @@ void WikiDialog::handleEvent_main_thread(std::shared_ptr event) int subscribeFlags = ui.groupTreeWidget->subscribeFlags(QString::fromStdString(mGroupId.toStdString())); updateModerationState(mGroupId, subscribeFlags); } + updateGroupStatistics(e->mWikiGroupId); break; + case RsWikiEventCode::READ_STATUS_CHANGED: + updateGroupStatistics(e->mWikiGroupId); + break; case RsWikiEventCode::NEW_COMMENT: // Refresh comments if the event is for the currently displayed page if (e->mWikiGroupId == mCurrentGroupId && !mCurrentPageId.isNull()) { @@ -1060,3 +1099,192 @@ void WikiDialog::loadComments(const RsGxsGroupId &groupId, const RsGxsMessageId mCommentTreeWidget->requestComments(groupId, msgVersions, msgId); } } + +bool WikiDialog::navigate(const RsGxsGroupId &groupId, const RsGxsMessageId &pageId) +{ + if (groupId.isNull()) + { + return false; + } + + mGroupId = groupId; + ui.groupTreeWidget->activateId(QString::fromStdString(groupId.toStdString()), true); + + if (!pageId.isNull()) + { + mNavigatePendingPageId = pageId; + QTreeWidgetItem *item = findPageItem(pageId); + if (item) + { + ui.treeWidget_Pages->setCurrentItem(item); + mNavigatePendingPageId.clear(); + } + } + else + { + mNavigatePendingPageId.clear(); + } + return true; +} + +void WikiDialog::copyWikiGroupLink() +{ + if (mGroupId.isNull()) + { + return; + } + QString groupName; + ui.groupTreeWidget->getGroupName(QString::fromStdString(mGroupId.toStdString()), groupName); + + RetroShareLink link = RetroShareLink::createGxsGroupLink(RetroShareLink::TYPE_WIKI, mGroupId, groupName); + QList links; + links.append(link); + RSLinkClipboard::copyLinks(links); +} + +void WikiDialog::copyWikiPageLink() +{ + RsGxsGroupId groupId; + RsGxsMessageId pageId; + RsGxsMessageId origPageId; + if (!getSelectedPage(groupId, pageId, origPageId)) + { + return; + } + + QTreeWidgetItem *item = ui.treeWidget_Pages->currentItem(); + if (!item) + { + return; + } + QString pageName = item->text(WIKI_GROUP_COL_PAGENAME); + + RetroShareLink link = RetroShareLink::createGxsMessageLink(RetroShareLink::TYPE_WIKI, groupId, pageId, pageName); + QList links; + links.append(link); + RSLinkClipboard::copyLinks(links); +} + +void WikiDialog::paintEvent(QPaintEvent *pe) +{ + rstime_t now = time(nullptr); + + if (mShouldUpdateGroupStatistics && now > 2 + mLastGroupStatisticsUpdateTs) + { + for (auto& groupId : mGroupStatisticsToUpdate) + { + updateGroupStatisticsReal(groupId); + } + + mShouldUpdateGroupStatistics = false; + mLastGroupStatisticsUpdateTs = time(nullptr); + mGroupStatisticsToUpdate.clear(); + } + + MainPage::paintEvent(pe); +} + +void WikiDialog::updateGroupStatistics(const RsGxsGroupId &groupId) +{ + mGroupStatisticsToUpdate.insert(groupId); + mShouldUpdateGroupStatistics = true; + update(); // Trigger paint event +} + +void WikiDialog::updateGroupStatisticsReal(const RsGxsGroupId &groupId) +{ + RsThread::async([this, groupId]() + { + GxsGroupStatistic stats; + bool ok = false; + uint32_t token; + if (rsWiki->requestGroupStatistic(token, groupId)) + { + auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(10); + auto st = rsWiki->requestStatus(token); + while (!(st == RsTokenService::FAILED || st >= RsTokenService::COMPLETE) + && std::chrono::steady_clock::now() < timeout) + { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + st = rsWiki->requestStatus(token); + } + + if (st == RsTokenService::COMPLETE) + { + ok = rsWiki->getGroupStatistic(token, stats); + } + else + { + rsWiki->cancelRequest(token); + } + } + + if (!ok) + { + std::cerr << "WikiDialog::updateGroupStatisticsReal failed to collect group statistics for group " << groupId << std::endl; + return; + } + + RsQThreadUtils::postToObject([this, stats, groupId]() + { + QTreeWidgetItem *item = ui.groupTreeWidget->getItemFromId(QString::fromStdString(stats.mGrpId.toStdString())); + if (item) + { + ui.groupTreeWidget->setCounts(item, stats.mNumThreadMsgsUnread, stats.mNumMsgs); + } + + mCachedGroupStats[groupId] = stats; + getUserNotify()->updateIcon(); + }, this); + }); +} + +void WikiDialog::markGroupAsRead() +{ + markAllGroupMessagesRead(true); +} + +void WikiDialog::markGroupAsUnread() +{ + markAllGroupMessagesRead(false); +} + +void WikiDialog::markAllGroupMessagesRead(bool read) +{ + if (mGroupId.isNull()) + { + return; + } + + RsGxsGroupId groupId = mGroupId; + QPointer self(this); + RsThread::async([self, groupId, read]() + { + if (!rsWiki) + { + return; + } + + std::vector snapshots; + if (rsWiki->getSnapshots(groupId, snapshots)) + { + for (const auto &page : snapshots) + { + rsWiki->setMessageReadStatus(RsGxsGrpMsgIdPair(groupId, page.mMeta.mMsgId), read); + } + } + + RsQThreadUtils::postToObject([self, groupId]() + { + if (self) + { + if (self->mGroupId == groupId) + { + self->loadPages(groupId); + } + self->updateDisplay(); + self->updateGroupStatistics(groupId); + } + }, self); + }); +} diff --git a/retroshare-gui/src/gui/WikiPoos/WikiDialog.h b/retroshare-gui/src/gui/WikiPoos/WikiDialog.h index b0f2c20ed..5447f6626 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiDialog.h +++ b/retroshare-gui/src/gui/WikiPoos/WikiDialog.h @@ -52,8 +52,11 @@ public: virtual UserNotify *createUserNotify(QObject *parent) override; + bool navigate(const RsGxsGroupId &groupId, const RsGxsMessageId &pageId); + protected: virtual void showEvent(QShowEvent *event) override; + virtual void paintEvent(QPaintEvent *pe) override; private slots: void updateDisplay(); @@ -90,6 +93,12 @@ private slots: // Comments (placeholder for future UI integration) void loadComments(const RsGxsGroupId &groupId, const RsGxsMessageId &msgId); + void copyWikiGroupLink(); + void copyWikiPageLink(); + + void markGroupAsRead(); + void markGroupAsUnread(); + private: void clearWikiPage(); @@ -99,6 +108,10 @@ private: void setSelectedPageReadStatus(bool read); QTreeWidgetItem *findPageItem(const RsGxsMessageId &pageId) const; + void markAllGroupMessagesRead(bool read); + void updateGroupStatistics(const RsGxsGroupId &groupId); + void updateGroupStatisticsReal(const RsGxsGroupId &groupId); + bool getSelectedPage(RsGxsGroupId &groupId, RsGxsMessageId &pageId, RsGxsMessageId &origPageId); std::string getSelectedPage(); const RsGxsGroupId &getSelectedGroup(); @@ -138,6 +151,12 @@ private: RsGxsGroupId mGroupId; // From GroupTreeWidget bool mCanModerate = false; bool mInitialLoadDone = false; + RsGxsMessageId mNavigatePendingPageId; + + std::set mGroupStatisticsToUpdate; + bool mShouldUpdateGroupStatistics = false; + rstime_t mLastGroupStatisticsUpdateTs = 0; + std::map mCachedGroupStats; /* UI - from Designer */ Ui::WikiDialog ui; diff --git a/retroshare-gui/src/gui/WikiPoos/WikiDialog.ui b/retroshare-gui/src/gui/WikiPoos/WikiDialog.ui index b2ce7ece4..34b2ead44 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiDialog.ui +++ b/retroshare-gui/src/gui/WikiPoos/WikiDialog.ui @@ -46,7 +46,7 @@ 0 0 - 240 + 302 510 @@ -315,7 +315,7 @@ 0 0 - 511 + 449 480 @@ -366,7 +366,11 @@ 0 - + + + true + + diff --git a/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.cpp b/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.cpp index 856a8857d..d8c8101f9 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.cpp +++ b/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.cpp @@ -21,11 +21,19 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include #include "gui/gxs/GxsIdTreeWidgetItem.h" #include "gui/WikiPoos/WikiEditDialog.h" #include "util/DateTime.h" #include "util/qtthreadsutils.h" +#include "util/HandleRichText.h" #include #include "util/rstime.h" @@ -96,6 +104,16 @@ WikiEditDialog::WikiEditDialog(QWidget *parent) ui.pushButton_Preview->setIcon(FilesDefs::getIconFromQtResourcePath(QString(":/icons/png/search.png"))); ui.pushButton_History->setIcon(FilesDefs::getIconFromQtResourcePath(QString(":/icons/png/history-clock-white.png"))); + connect(ui.btnHeading, SIGNAL(clicked()), this, SLOT(onMarkdownHeading())); + connect(ui.btnBold, SIGNAL(clicked()), this, SLOT(onMarkdownBold())); + connect(ui.btnItalic, SIGNAL(clicked()), this, SLOT(onMarkdownItalic())); + connect(ui.btnQuote, SIGNAL(clicked()), this, SLOT(onMarkdownQuote())); + connect(ui.btnCode, SIGNAL(clicked()), this, SLOT(onMarkdownCode())); + connect(ui.btnLink, SIGNAL(clicked()), this, SLOT(onMarkdownLink())); + connect(ui.btnImage, SIGNAL(clicked()), this, SLOT(onMarkdownImage())); + connect(ui.btnUnorderedList, SIGNAL(clicked()), this, SLOT(onMarkdownUnorderedList())); + connect(ui.btnOrderedList, SIGNAL(clicked()), this, SLOT(onMarkdownOrderedList())); + ui.checkBox_OldHistory->setChecked(false); mOldHistoryEnabled = false; ui.groupBox_History->hide(); @@ -1184,3 +1202,169 @@ QTreeWidgetItem *WikiEditDialog::findHistoryItem(const RsGxsMessageId &msgId) co return nullptr; } + +void WikiEditDialog::insertMarkdownWrap(const QString &prefix, const QString &suffix) +{ + QTextCursor cursor = ui.textEdit->textCursor(); + if (cursor.hasSelection()) { + QString selectedText = cursor.selectedText(); + cursor.insertText(prefix + selectedText + suffix); + } else { + cursor.insertText(prefix + suffix); + // Move cursor inside the wrapper + cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, suffix.length()); + ui.textEdit->setTextCursor(cursor); + } + ui.textEdit->setFocus(); +} + +void WikiEditDialog::insertMarkdownBlockPrefix(const QString &prefix) +{ + QTextCursor cursor = ui.textEdit->textCursor(); + cursor.beginEditBlock(); + + int startPos = cursor.selectionStart(); + int endPos = cursor.selectionEnd(); + + QTextBlock startBlock = ui.textEdit->document()->findBlock(startPos); + QTextBlock endBlock = ui.textEdit->document()->findBlock(endPos); + + QTextBlock block = startBlock; + while (block.isValid()) { + QTextCursor blockCursor(block); + blockCursor.movePosition(QTextCursor::StartOfBlock); + blockCursor.insertText(prefix); + + if (block == endBlock) { + break; + } + block = block.next(); + } + + cursor.endEditBlock(); + ui.textEdit->setFocus(); +} + +void WikiEditDialog::onMarkdownHeading() +{ + insertMarkdownBlockPrefix("### "); +} + +void WikiEditDialog::onMarkdownBold() +{ + insertMarkdownWrap("**", "**"); +} + +void WikiEditDialog::onMarkdownItalic() +{ + insertMarkdownWrap("*", "*"); +} + +void WikiEditDialog::onMarkdownQuote() +{ + insertMarkdownBlockPrefix("> "); +} + +void WikiEditDialog::onMarkdownCode() +{ + insertMarkdownWrap("`", "`"); +} + +void WikiEditDialog::onMarkdownLink() +{ + QTextCursor cursor = ui.textEdit->textCursor(); + if (cursor.hasSelection()) { + QString selectedText = cursor.selectedText(); + cursor.insertText("[" + selectedText + "](http://)"); + // Position cursor inside parentheses (right after http://) + cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 1); + ui.textEdit->setTextCursor(cursor); + } else { + cursor.insertText("[text](http://)"); + // Position cursor inside parentheses (right after http://) + cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 8); + ui.textEdit->setTextCursor(cursor); + } + ui.textEdit->setFocus(); +} + +void WikiEditDialog::onMarkdownImage() +{ + QString fileName = QFileDialog::getOpenFileName(this, + tr("Select Image to Embed"), "", + tr("Images (*.png *.jpg *.jpeg *.gif *.bmp);;All Files (*)")); + if (fileName.isEmpty()) { + return; + } + + // Try to optimize, scale, and compress the image using RsHtml::makeEmbeddedImage + QString encodedImage; + static const uint32_t MAX_ALLOWED_GXS_MESSAGE_SIZE = 199000; + if (RsHtml::makeEmbeddedImage(fileName, encodedImage, 640 * 480, MAX_ALLOWED_GXS_MESSAGE_SIZE - 200)) { + int srcStart = encodedImage.indexOf("src=\""); + if (srcStart != -1) { + srcStart += 5; + int srcEnd = encodedImage.indexOf("\"", srcStart); + if (srcEnd != -1) { + QString src = encodedImage.mid(srcStart, srcEnd - srcStart); + QTextCursor cursor = ui.textEdit->textCursor(); + QString altText = cursor.selectedText(); + if (altText.isEmpty()) { + altText = "image"; + } + cursor.insertText(QString("![%1](%2)").arg(altText, src)); + ui.textEdit->setFocus(); + return; + } + } + } + + // Fallback to raw unoptimized embedding if makeEmbeddedImage fails + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, tr("Error"), tr("Could not open image file.")); + return; + } + + QByteArray data = file.readAll(); + if (data.size() > 150 * 1024) { + QMessageBox::StandardButton reply = QMessageBox::warning(this, + tr("Image Too Large"), + tr("The selected image is %1 KB. Wiki pages have a total message limit of 200 KB, " + "and base64 encoding will increase this size further.\n\n" + "Are you sure you want to embed this image anyway?").arg(data.size() / 1024), + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::No) { + return; + } + } + + QFileInfo fileInfo(fileName); + QString ext = fileInfo.suffix().toLower(); + if (ext == "jpg") ext = "jpeg"; + QString mimeType = "image/" + ext; + + QString base64Data = QString::fromLatin1(data.toBase64()); + + QTextCursor cursor = ui.textEdit->textCursor(); + QString altText = cursor.selectedText(); + if (altText.isEmpty()) { + altText = "image"; + } + + cursor.insertText(QString("![%1](data:%2;base64,%3)") + .arg(altText) + .arg(mimeType) + .arg(base64Data)); + ui.textEdit->setFocus(); +} + +void WikiEditDialog::onMarkdownUnorderedList() +{ + insertMarkdownBlockPrefix("- "); +} + +void WikiEditDialog::onMarkdownOrderedList() +{ + insertMarkdownBlockPrefix("1. "); +} diff --git a/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.h b/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.h index 90caa2de7..18269db48 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.h +++ b/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.h @@ -63,6 +63,16 @@ void generateMerge(); void historyItemChanged(QTreeWidgetItem *item, int column); void updateMergeButtonState(); +void onMarkdownHeading(); +void onMarkdownBold(); +void onMarkdownItalic(); +void onMarkdownQuote(); +void onMarkdownCode(); +void onMarkdownLink(); +void onMarkdownImage(); +void onMarkdownUnorderedList(); +void onMarkdownOrderedList(); + private: void updateHistoryStatus(); @@ -94,6 +104,9 @@ QString getAuthorName(const RsGxsMessageId &msgId) const; QTreeWidgetItem *findHistoryItem(const RsGxsMessageId &msgId) const; void resetEditorState(); +void insertMarkdownWrap(const QString &prefix, const QString &suffix); +void insertMarkdownBlockPrefix(const QString &prefix); + bool mNewPage; diff --git a/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.ui b/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.ui index e8e4675c1..fafcc0682 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.ui +++ b/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.ui @@ -78,16 +78,16 @@ - - - - true - - - Merge for Republish - - - + + + + true + + + Merge for Republish + + + @@ -282,6 +282,138 @@ + + + + + 10 + 75 + true + + + + <html><head/><body><p>Heading (###)</p></body></html> + + + H + + + + + + + <html><head/><body><p>Bold (**bold**)</p></body></html> + + + ... + + + + :/icons/textedit/bold.png:/icons/textedit/bold.png + + + + + + + <html><head/><body><p>Italic (*italic*)</p></body></html> + + + ... + + + + :/icons/textedit/italic.png:/icons/textedit/italic.png + + + + + + + + 10 + 75 + true + + + + <html><head/><body><p>Code (`code`)</p></body></html> + + + C + + + + + + + <html><head/><body><p>Blockquote (&gt; quote)</p></body></html> + + + ... + + + + :/icons/textedit/code.png:/icons/textedit/code.png + + + + + + + <html><head/><body><p>Link ([text](url))</p></body></html> + + + ... + + + + :/icons/textedit/link.png:/icons/textedit/link.png + + + + + + + <html><head/><body><p>Image (![alt](url))</p></body></html> + + + ... + + + + :/icons/textedit/photo-of-a-landscape.png:/icons/textedit/photo-of-a-landscape.png + + + + + + + <html><head/><body><p>Unordered List (- item)</p></body></html> + + + ... + + + + :/icons/textedit/bullet-list.png:/icons/textedit/bullet-list.png + + + + + + + <html><head/><body><p>Ordered List (1. item)</p></body></html> + + + ... + + + + :/icons/textedit/numberd-list.png:/icons/textedit/numberd-list.png + + + @@ -371,6 +503,9 @@
gui/gxs/GxsIdChooser.h
- + + + + From 8c11be4a8cb8dec1bd9747df208d9acc8d68ccfc Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:32:53 +0200 Subject: [PATCH 2/3] Added own fontsizehandler --- retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp | 5 +++++ retroshare-gui/src/gui/WikiPoos/WikiDialog.h | 2 ++ 2 files changed, 7 insertions(+) diff --git a/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp b/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp index 0c9d16b5f..2943a8b1b 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp +++ b/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp @@ -38,6 +38,7 @@ #include "gui/gxs/WikiGroupDialog.h" #include "gui/gxs/GxsCommentTreeWidget.h" #include "gui/gxs/GxsIdDetails.h" +#include "gui/common/RSTreeWidget.h" #include "util/DateTime.h" #include "util/qtthreadsutils.h" @@ -176,6 +177,8 @@ WikiDialog::WikiDialog(QWidget *parent) : }, this ); }, mEventHandlerId, wikiEventType); + + mFontSizeHandler.registerFontSize(ui.groupTreeWidget->treeWidget(), 1.2f); } WikiDialog::~WikiDialog() @@ -1015,6 +1018,8 @@ void WikiDialog::handleEvent_main_thread(std::shared_ptr event) #endif switch(e->mWikiEventCode) { + case RsWikiEventCode::UNKNOWN: + break; case RsWikiEventCode::UPDATED_COLLECTION: case RsWikiEventCode::NEW_COLLECTION: updateDisplay(); // Refresh global list diff --git a/retroshare-gui/src/gui/WikiPoos/WikiDialog.h b/retroshare-gui/src/gui/WikiPoos/WikiDialog.h index 5447f6626..0e4b98ab7 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiDialog.h +++ b/retroshare-gui/src/gui/WikiPoos/WikiDialog.h @@ -158,6 +158,8 @@ private: rstime_t mLastGroupStatisticsUpdateTs = 0; std::map mCachedGroupStats; + FontSizeHandler mFontSizeHandler; + /* UI - from Designer */ Ui::WikiDialog ui; From b553fe4b4e637c21e30cf6adfd445b6881239362 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:12:54 +0200 Subject: [PATCH 3/3] update editor icons --- .../src/gui/WikiPoos/WikiEditDialog.ui | 10 +++++++++- retroshare-gui/src/gui/icons.qrc | 2 ++ .../src/gui/icons/textedit/blockquote.png | Bin 0 -> 5594 bytes .../src/gui/icons/textedit/heading.png | Bin 0 -> 4185 bytes 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 retroshare-gui/src/gui/icons/textedit/blockquote.png create mode 100644 retroshare-gui/src/gui/icons/textedit/heading.png diff --git a/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.ui b/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.ui index fafcc0682..dc8670f4c 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.ui +++ b/retroshare-gui/src/gui/WikiPoos/WikiEditDialog.ui @@ -297,6 +297,10 @@ H + + + :/icons/textedit/heading.png:/icons/textedit/heading.png + @@ -342,6 +346,10 @@ C + + + :/icons/textedit/code.png:/icons/textedit/code.png + @@ -354,7 +362,7 @@ - :/icons/textedit/code.png:/icons/textedit/code.png + :/icons/textedit/blockquote.png:/icons/textedit/blockquote.png diff --git a/retroshare-gui/src/gui/icons.qrc b/retroshare-gui/src/gui/icons.qrc index ed38a2c12..02e60643d 100644 --- a/retroshare-gui/src/gui/icons.qrc +++ b/retroshare-gui/src/gui/icons.qrc @@ -353,6 +353,8 @@ icons/textedit/smile.png icons/textedit/font-increase.png icons/textedit/code.png + icons/textedit/blockquote.png + icons/textedit/heading.png icons/fullscreen.png icons/fullscreen-exit.png icons/notification.png diff --git a/retroshare-gui/src/gui/icons/textedit/blockquote.png b/retroshare-gui/src/gui/icons/textedit/blockquote.png new file mode 100644 index 0000000000000000000000000000000000000000..eb735c446118ccfb1d9e84e49185eebf40b3eb26 GIT binary patch literal 5594 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 bMaLY_rJmTmncOUOV1X=5txbxID53uc!`N+& literal 0 HcmV?d00001 diff --git a/retroshare-gui/src/gui/icons/textedit/heading.png b/retroshare-gui/src/gui/icons/textedit/heading.png new file mode 100644 index 0000000000000000000000000000000000000000..a1e83bb3702fb8214505f8bd83a7a3549e6cb594 GIT binary patch literal 4185 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 IZ017$KM%eN)c^nh literal 0 HcmV?d00001