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..2943a8b1b 100644 --- a/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp +++ b/retroshare-gui/src/gui/WikiPoos/WikiDialog.cpp @@ -31,12 +31,14 @@ #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" #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" @@ -53,6 +55,8 @@ #include #include #include +#include +#include #include @@ -107,6 +111,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; @@ -170,6 +177,8 @@ WikiDialog::WikiDialog(QWidget *parent) : }, this ); }, mEventHandlerId, wikiEventType); + + mFontSizeHandler.registerFontSize(ui.groupTreeWidget->treeWidget(), 1.2f); } WikiDialog::~WikiDialog() @@ -409,6 +418,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 +645,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 +727,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 +909,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()); } @@ -982,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 @@ -990,6 +1028,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 +1036,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 +1046,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 +1104,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..0e4b98ab7 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,14 @@ 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; + + FontSizeHandler mFontSizeHandler; /* 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..dc8670f4c 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,146 @@ + + + + + 10 + 75 + true + + + + <html><head/><body><p>Heading (###)</p></body></html> + + + H + + + + :/icons/textedit/heading.png:/icons/textedit/heading.png + + + + + + + <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 + + + + :/icons/textedit/code.png:/icons/textedit/code.png + + + + + + + <html><head/><body><p>Blockquote (&gt; quote)</p></body></html> + + + ... + + + + :/icons/textedit/blockquote.png:/icons/textedit/blockquote.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 +511,9 @@
gui/gxs/GxsIdChooser.h
- + + + + 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 000000000..eb735c446 Binary files /dev/null and b/retroshare-gui/src/gui/icons/textedit/blockquote.png differ 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 000000000..a1e83bb37 Binary files /dev/null and b/retroshare-gui/src/gui/icons/textedit/heading.png differ