/******************************************************************************* * retroshare-gui/src/gui/gxsforums/GxsForumsThreadWidget.cpp * * * * Copyright 2012 Retroshare Team * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU Affero General Public License as * * published by the Free Software Foundation, either version 3 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Affero General Public License for more details. * * * * You should have received a copy of the GNU Affero General Public License * * along with this program. If not, see . * * * *******************************************************************************/ #include #include #include #include #include #include "util/qtthreadsutils.h" #include "util/misc.h" #include "GxsForumThreadWidget.h" #include "ui_GxsForumThreadWidget.h" #include "GxsForumModel.h" #include "GxsForumsDialog.h" #include "gui/RetroShareLink.h" #include "gui/common/RSTreeWidgetItem.h" #include "gui/settings/rsharesettings.h" #include "gui/common/RSElidedItemDelegate.h" #include "gui/settings/rsharesettings.h" #include "gui/gxs/GxsIdTreeWidgetItem.h" #include "gui/Identity/IdDialog.h" #include "gui/gxs/GxsIdDetails.h" #include "util/HandleRichText.h" #include "CreateGxsForumMsg.h" #include "gui/MainWindow.h" #include "gui/msgs/MessageComposer.h" #include "util/DateTime.h" #include "gui/common/UIStateHelper.h" #include "util/RsQtVersion.h" #include "util/imageutil.h" #include #include #include #include // These should be in retroshare/ folder. #include "retroshare/rsgxsflags.h" #include #include //#define DEBUG_FORUMS /* Images for context menu icons */ #define IMAGE_MESSAGE ":/icons/mail/compose.png" #define IMAGE_REPLY ":/icons/mail/reply.png" #define IMAGE_MESSAGEREPLY ":/icons/mail/write-mail.png" #define IMAGE_MESSAGEEDIT ":/icons/png/pencil-edit-button.png" #define IMAGE_MESSAGEREMOVE ":/images/mail_delete.png" #define IMAGE_DOWNLOAD ":/images/start.png" #define IMAGE_DOWNLOADALL ":/images/startall.png" #define IMAGE_COPYLINK ":/images/copyrslink.png" #define IMAGE_BIOHAZARD ":/icons/biohazard_red.png" #define IMAGE_WARNING_YELLOW ":/icons/warning_yellow_128.png" #define IMAGE_WARNING_RED ":/icons/warning_red_128.png" #define IMAGE_WARNING_UNKNOWN ":/icons/bullet_grey_128.png" #define IMAGE_VOID ":/icons/void_128.png" #define IMAGE_PINPOST ":/images/pin32.png" #define IMAGE_POSITIVE_OPINION ":/icons/png/thumbs-up.png" #define IMAGE_NEUTRAL_OPINION ":/icons/png/thumbs-neutral.png" #define IMAGE_NEGATIVE_OPINION ":/icons/png/thumbs-down.png" #define VIEW_LAST_POST 0 #define VIEW_THREADED 1 #define VIEW_FLAT 2 /* Thread constants */ // We need consts for that!! Defined in multiple places. #ifdef DEBUG_FORUMS static std::ostream& operator<<(std::ostream& o,const QModelIndex& q) { return o << "(" << q.row() << "," << q.column() << "," << (void*)q.internalPointer() << ")" ; } #endif class DistributionItemDelegate: public QStyledItemDelegate { public: DistributionItemDelegate() {} virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if(!index.isValid()) { std::cerr << "(EE) attempt to draw an invalid index." << std::endl; return ; } QStyleOptionViewItem opt = option; initStyleOption(&opt, index); // disable default icon opt.icon = QIcon(); // draw default item QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, 0); const QRect r = option.rect; QIcon icon ; // get pixmap unsigned int warning_level = qvariant_cast(index.data(Qt::DecorationRole)); switch(warning_level) { default: case 3: case 0: icon = FilesDefs::getIconFromQtResourcePath(IMAGE_VOID); break; case 1: icon = FilesDefs::getIconFromQtResourcePath(IMAGE_WARNING_YELLOW); break; case 2: icon = FilesDefs::getIconFromQtResourcePath(IMAGE_WARNING_RED); break; } QPixmap pix = icon.pixmap(r.size()); // draw pixmap at center of item const QPoint p = QPoint((r.width() - pix.width())/2, (r.height() - pix.height())/2); painter->drawPixmap(r.topLeft() + p, pix); } virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& /*index*/) const override { static auto img(FilesDefs::getPixmapFromQtResourcePath(IMAGE_WARNING_YELLOW)); return QSize(img.width()*1.2,option.rect.height()); } }; class ReadStatusItemDelegate: public QStyledItemDelegate { public: ReadStatusItemDelegate() {} virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if(!index.isValid()) { std::cerr << "(EE) attempt to draw an invalid index." << std::endl; return ; } QStyleOptionViewItem opt = option; initStyleOption(&opt, index); // disable default icon opt.icon = QIcon(); // draw default item QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, 0); const QRect r = option.rect; QIcon icon ; // get pixmap unsigned int read_status = qvariant_cast(index.data(Qt::DecorationRole)); bool pinned = index.data(RsGxsForumModel::ThreadPinnedRole).toBool(); bool unread = IS_MSG_UNREAD(read_status); bool missing = index.sibling(index.row(),RsGxsForumModel::COLUMN_THREAD_DATA).data(RsGxsForumModel::MissingRole).toBool(); // set icon if (missing) icon = QIcon(); else if(pinned) icon = FilesDefs::getIconFromQtResourcePath(IMAGE_PINPOST); else { if (unread) icon = FilesDefs::getIconFromQtResourcePath(":/images/message-state-unread.png"); else icon = FilesDefs::getIconFromQtResourcePath(":/images/message-state-read.png"); } QPixmap pix = icon.pixmap(r.size()); // draw pixmap at center of item const QPoint p = QPoint((r.width() - pix.width())/2, (r.height() - pix.height())/2); painter->drawPixmap(r.topLeft() + p, pix); } virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& /*index*/) const override { static auto img(FilesDefs::getPixmapFromQtResourcePath(":/images/message-state-unread.png")); return QSize(img.width()*1.2,option.rect.height()); } }; class ForumPostSortFilterProxyModel: public QSortFilterProxyModel { public: explicit ForumPostSortFilterProxyModel(const QHeaderView *header,QObject *parent = NULL): QSortFilterProxyModel(parent),m_header(header) { setDynamicSortFilter(false); // causes crashes when true } bool lessThan(const QModelIndex& left, const QModelIndex& right) const override { bool left_is_not_pinned = ! left.data(RsGxsForumModel::ThreadPinnedRole).toBool(); bool right_is_not_pinned = !right.data(RsGxsForumModel::ThreadPinnedRole).toBool(); if(left_is_not_pinned ^ right_is_not_pinned) return (m_header->sortIndicatorOrder()==Qt::AscendingOrder)?right_is_not_pinned:left_is_not_pinned ; // always put pinned posts on top #if QT_VERSION >= QT_VERSION_CHECK (6, 0, 0) return QVariant::compare(left.data(RsGxsForumModel::SortRole), right.data(RsGxsForumModel::SortRole)) < 0; #else return left.data(RsGxsForumModel::SortRole) < right.data(RsGxsForumModel::SortRole) ; #endif } bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { return sourceModel()->index(source_row,0,source_parent).data(RsGxsForumModel::FilterRole).toString() == RsGxsForumModel::FilterString ; } private: const QHeaderView *m_header ; }; void GxsForumThreadWidget::setTextColorRead (QColor color) { mTextColorRead = color; mThreadModel->setTextColorRead (color);} void GxsForumThreadWidget::setTextColorUnread (QColor color) { mTextColorUnread = color; mThreadModel->setTextColorUnread (color);} void GxsForumThreadWidget::setTextColorUnreadChildren(QColor color) { mTextColorUnreadChildren = color; mThreadModel->setTextColorUnreadChildren(color);} void GxsForumThreadWidget::setTextColorNotSubscribed (QColor color) { mTextColorNotSubscribed = color; mThreadModel->setTextColorNotSubscribed (color);} void GxsForumThreadWidget::setTextColorMissing (QColor color) { mTextColorMissing = color; mThreadModel->setTextColorMissing (color);} // Suppose to be different from unread one void GxsForumThreadWidget::setTextColorPinned (QColor color) { mTextColorPinned = color; mThreadModel->setTextColorPinned (color);} void GxsForumThreadWidget::setBackgroundColorPinned (QColor color) { mBackgroundColorPinned = color; mThreadModel->setBackgroundColorPinned (color);} void GxsForumThreadWidget::setBackgroundColorFiltered(QColor color) { mBackgroundColorFiltered = color; mThreadModel->setBackgroundColorFiltered (color);} GxsForumThreadWidget::GxsForumThreadWidget(const RsGxsGroupId &forumId, QWidget *parent) : GxsMessageFrameWidget(rsGxsForums, parent), ui(new Ui::GxsForumThreadWidget) { ui->setupUi(this); //setUpdateWhenInvisible(true); //mUpdating = false; mUnreadCount = 0; mNewCount = 0; mInMsgAsReadUnread = false; mThreadModel = new RsGxsForumModel(this); mThreadProxyModel = new ForumPostSortFilterProxyModel(ui->threadTreeWidget->header(),this); mThreadProxyModel->setSourceModel(mThreadModel); mThreadProxyModel->setSortRole(RsGxsForumModel::SortRole); ui->threadTreeWidget->setModel(mThreadProxyModel); mThreadProxyModel->setFilterRole(RsGxsForumModel::FilterRole); QSortFilterProxyModel_setFilterRegularExpression(mThreadProxyModel, QString(RsGxsForumModel::FilterString)) ; ui->threadTreeWidget->setSortingEnabled(true); ui->threadTreeWidget->setItemDelegateForColumn(RsGxsForumModel::COLUMN_THREAD_DISTRIBUTION,new DistributionItemDelegate()) ; ui->threadTreeWidget->setItemDelegateForColumn(RsGxsForumModel::COLUMN_THREAD_AUTHOR,new GxsIdTreeItemDelegate()) ; ui->threadTreeWidget->setItemDelegateForColumn(RsGxsForumModel::COLUMN_THREAD_READ,new ReadStatusItemDelegate()) ; ui->threadTreeWidget->header()->setSortIndicatorShown(true); connect(ui->versions_CB, SIGNAL(currentIndexChanged(int)), this, SLOT(changedVersion())); connect(ui->threadTreeWidget, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(threadListCustomPopupMenu(QPoint))); connect(ui->forumName, SIGNAL(clicked(QPoint)), this, SLOT(showForumInfo())); ui->subscribeToolButton->hide() ; connect(ui->subscribeToolButton, SIGNAL(subscribe(bool)), this, SLOT(subscribeGroup(bool))); connect(ui->newmessageButton, SIGNAL(clicked()), this, SLOT(replytoforummessage())); connect(ui->newthreadButton, SIGNAL(clicked()), this, SLOT(createthread())); connect(mThreadModel,SIGNAL(forumLoaded()),this,SLOT(postForumLoading())); ui->newmessageButton->setText(tr("Reply")); ui->newthreadButton->setText(tr("New thread")); connect(ui->threadTreeWidget, SIGNAL(clicked(QModelIndex)), this, SLOT(clickedThread(QModelIndex))); connect(ui->threadTreeWidget->selectionModel(), SIGNAL(currentChanged(const QModelIndex&,const QModelIndex&)), this, SLOT(changedSelection(const QModelIndex&,const QModelIndex&))); //connect(ui->expandButton, SIGNAL(clicked()), this, SLOT(togglethreadview())); ui->expandButton->hide(); connect(ui->previousButton, SIGNAL(clicked()), this, SLOT(previousMessage())); connect(ui->nextButton, SIGNAL(clicked()), this, SLOT(nextMessage())); connect(ui->nextUnreadButton, SIGNAL(clicked()), this, SLOT(nextUnreadMessage())); connect(ui->downloadButton, SIGNAL(clicked()), this, SLOT(downloadAllFiles())); connect(ui->filterLineEdit, SIGNAL(textChanged(QString)), this, SLOT(filterItems(QString))); connect(ui->filterLineEdit, SIGNAL(filterChanged(int)), this, SLOT(filterColumnChanged(int))); connect(ui->threadedView_TB, SIGNAL(toggled(bool)), this, SLOT(toggleThreadedView(bool))); connect(ui->flatView_TB, SIGNAL(toggled(bool)), this, SLOT(toggleFlatView(bool))); connect(ui->latestPostInThreadView_TB, SIGNAL(toggled(bool)), this, SLOT(toggleLstPostInThreadView(bool))); /* Set own item delegate */ RSElidedItemDelegate *itemDelegate = new RSElidedItemDelegate(this); itemDelegate->setSpacing(QSize(0, 2)); itemDelegate->setOnlyPlainText(true); ui->threadTreeWidget->setItemDelegate(itemDelegate); /* add filter actions */ ui->filterLineEdit->addFilter(QIcon(), tr("Title"), RsGxsForumModel::COLUMN_THREAD_TITLE, tr("Search Title")); ui->filterLineEdit->addFilter(QIcon(), tr("Date"), RsGxsForumModel::COLUMN_THREAD_DATE, tr("Search Date")); ui->filterLineEdit->addFilter(QIcon(), tr("Author"), RsGxsForumModel::COLUMN_THREAD_AUTHOR, tr("Search Author")); mLastViewType = -1; float f = QFontMetricsF(font()).height()/14.0f ; /* Set header resize modes and initial section sizes */ QHeaderView * ttheader = ui->threadTreeWidget->header () ; ttheader->resizeSection (RsGxsForumModel::COLUMN_THREAD_DATE, 140*f); ttheader->resizeSection (RsGxsForumModel::COLUMN_THREAD_TITLE, 440*f); ttheader->resizeSection (RsGxsForumModel::COLUMN_THREAD_AUTHOR, 150*f); ui->threadTreeWidget->resizeColumnToContents(RsGxsForumModel::COLUMN_THREAD_DISTRIBUTION); //ui->threadTreeWidget->resizeColumnToContents(RsGxsForumModel::COLUMN_THREAD_READ); QHeaderView_setSectionResizeModeColumn(ttheader, RsGxsForumModel::COLUMN_THREAD_TITLE, QHeaderView::Interactive); QHeaderView_setSectionResizeModeColumn(ttheader, RsGxsForumModel::COLUMN_THREAD_DATE, QHeaderView::Interactive); QHeaderView_setSectionResizeModeColumn(ttheader, RsGxsForumModel::COLUMN_THREAD_AUTHOR, QHeaderView::Interactive); QHeaderView_setSectionResizeModeColumn(ttheader, RsGxsForumModel::COLUMN_THREAD_READ, QHeaderView::Interactive); QHeaderView_setSectionResizeModeColumn(ttheader, RsGxsForumModel::COLUMN_THREAD_DISTRIBUTION, QHeaderView::Fixed); ttheader->setCascadingSectionResizes(true); /* Set header sizes for the fixed columns and resize modes, must be set after processSettings */ ttheader->hideSection (RsGxsForumModel::COLUMN_THREAD_CONTENT); ttheader->hideSection (RsGxsForumModel::COLUMN_THREAD_MSGID); ttheader->hideSection (RsGxsForumModel::COLUMN_THREAD_DATA); ttheader->setContextMenuPolicy(Qt::CustomContextMenu); connect(ttheader, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(headerContextMenuRequested(QPoint))); ui->progressBar->hide(); ui->progressText->hide(); mFillThread = NULL; setGroupId(forumId); //ui->threadTreeWidget->installEventFilter(this) ; // load settings processSettings(true); mDisplayBannedText = false; blankPost(); ui->subscribeToolButton->setToolTip(tr( "

Subscribing to the forum will gather \ available posts from your subscribed friends, and make the \ forum visible to all other friends.

Afterwards you can unsubscribe from the context menu of the forum list at left.

")); #ifdef SUSPENDED_CODE ui->threadTreeWidget->enableColumnCustomize(true); #endif mEventHandlerId = 0; // Needs to be asynced because this function is called by another thread! rsEvents->registerEventsHandler( [this](std::shared_ptr event) { RsQThreadUtils::postToObject([=](){ handleEvent_main_thread(event); }, this ); }, mEventHandlerId, RsEventType::GXS_FORUMS ); mFontSizeHandler.registerFontSize(ui->threadTreeWidget, 1.4f, [this](QAbstractItemView *view, int) { mThreadModel->setFont(view->font()); }); } void GxsForumThreadWidget::handleEvent_main_thread(std::shared_ptr event) { if(event->mType == RsEventType::GXS_FORUMS) { const RsGxsForumEvent *e = dynamic_cast(event.get()); if(!e) return; switch(e->mForumEventCode) { case RsForumEventCode::UPDATED_FORUM: // [[fallthrough]]; case RsForumEventCode::NEW_FORUM: // [[fallthrough]]; case RsForumEventCode::UPDATED_MESSAGE: // [[fallthrough]]; case RsForumEventCode::NEW_MESSAGE: case RsForumEventCode::PINNED_POSTS_CHANGED: case RsForumEventCode::SYNC_PARAMETERS_UPDATED: if(e->mForumGroupId == mForumGroup.mMeta.mGroupId) updateDisplay(true); break; default: break; } } } void GxsForumThreadWidget::showForumInfo() { mThreadId.clear(); ui->threadTreeWidget->selectionModel()->clear(); updateForumDescription(true); } void GxsForumThreadWidget::blank() { ui->subscribeToolButton->hide(); ui->newthreadButton->hide(); ui->forumName->setText(""); ui->progressText->hide(); ui->progressBar->hide(); ui->threadedView_TB->setEnabled(false); ui->flatView_TB->setEnabled(false); ui->latestPostInThreadView_TB->setEnabled(false); ui->filterLineEdit->setEnabled(false); mThreadModel->clear(); blankPost(); } GxsForumThreadWidget::~GxsForumThreadWidget() { rsEvents->unregisterEventsHandler(mEventHandlerId); // save settings processSettings(false); delete ui; } void GxsForumThreadWidget::processSettings(bool load) { QHeaderView *header = ui->threadTreeWidget->header(); Settings->beginGroup(QString("ForumThreadWidget")); if (load) { // load settings // expandFiles bool bValue = Settings->value("expandButton", true).toBool(); ui->expandButton->setChecked(bValue); togglethreadview_internal(); // filterColumn ui->filterLineEdit->setCurrentFilter(Settings->value("filterColumn", RsGxsForumModel::COLUMN_THREAD_TITLE).toInt()); // index of viewBox switch(Settings->value("viewBox", VIEW_THREADED).toInt()) { default: case VIEW_THREADED : ui->threadedView_TB->setChecked(true); break; case VIEW_FLAT : ui->flatView_TB->setChecked(true); break; case VIEW_LAST_POST: ui->latestPostInThreadView_TB->setChecked(true); break; } // state of thread tree header->restoreState(Settings->value("ThreadTree").toByteArray()); // state of splitter ui->threadSplitter->restoreState(Settings->value("threadSplitter").toByteArray()); } else { // save settings // state of thread tree Settings->setValue("ThreadTree", header->saveState()); // state of splitter Settings->setValue("threadSplitter", ui->threadSplitter->saveState()); } Settings->endGroup(); } void GxsForumThreadWidget::changedSelection(const QModelIndex& current,const QModelIndex& last) { if (current!=last && ( ( last.row()>=0 && last.column()>=0) //Double call when retrieve focus. || mThreadId.isNull() //For first click ) && (current.column() != RsGxsForumModel::COLUMN_THREAD_READ) //clickedThread will changedThread after. ) changedThread(current); } void GxsForumThreadWidget::groupIdChanged() { ui->forumName->setText(groupId().isNull () ? "" : tr("Loading...")); mNewCount = 0; mUnreadCount = 0; updateDisplay(true); } QString GxsForumThreadWidget::groupName(bool withUnreadCount) { QString name = groupId().isNull () ? tr("No name") : ui->forumName->text(); if (withUnreadCount && mUnreadCount) { name += QString(" (%1)").arg(mUnreadCount); } return name; } QIcon GxsForumThreadWidget::groupIcon() { if (mNewCount) { return FilesDefs::getIconFromQtResourcePath(":/images/message-state-new.png"); } return QIcon(); } void GxsForumThreadWidget::saveExpandedItems(QList& expanded_items) const { expanded_items.clear(); for(int row = 0; row < mThreadProxyModel->rowCount(); ++row) { std::string path = mThreadProxyModel->index(row,0).data(Qt::DisplayRole).toString().toStdString(); recursSaveExpandedItems(mThreadProxyModel->index(row,0),expanded_items); } } void GxsForumThreadWidget::recursSaveExpandedItems(const QModelIndex& index, QList& expanded_items) const { if(ui->threadTreeWidget->isExpanded(index)) { for(int row=0;rowrowCount(index);++row) recursSaveExpandedItems(mThreadProxyModel->index(row,0,index),expanded_items) ; RsGxsMessageId message_id(index.sibling(index.row(),RsGxsForumModel::COLUMN_THREAD_MSGID).data(Qt::UserRole).toString().toStdString()); expanded_items.push_back(message_id); } } void GxsForumThreadWidget::recursRestoreExpandedItems(const QModelIndex& /*index*/, const QList& expanded_items) { for(auto it(expanded_items.begin());it!=expanded_items.end();++it) ui->threadTreeWidget->setExpanded( mThreadProxyModel->mapFromSource(mThreadModel->getIndexOfMessage(*it)) ,true) ; } void GxsForumThreadWidget::updateDisplay(bool complete) { #ifdef DEBUG_FORUMS std::cerr << "udateDisplay: groupId()=" << groupId()<< std::endl; #endif if(groupId().isNull()) { #ifdef DEBUG_FORUMS std::cerr << " group_id=0. Return!"<< std::endl; #endif ui->nextUnreadButton->setEnabled(false); return; } if(mForumGroup.mMeta.mGroupId.isNull() && !groupId().isNull()) { #ifdef DEBUG_FORUMS std::cerr << " inconsistent group data. Reloading!"<< std::endl; #endif complete = true; } if(complete) // need to update the group data, reload the messages etc. { saveExpandedItems(mSavedExpandedMessages); if(groupId() != mThreadModel->currentGroupId()) mThreadId.clear(); updateGroupData(); mThreadModel->updateForum(groupId()); return; } } QModelIndex GxsForumThreadWidget::GxsForumThreadWidget::getCurrentIndex() const { QModelIndexList selectedIndexes = ui->threadTreeWidget->selectionModel()->selectedIndexes(); if(selectedIndexes.size() != RsGxsForumModel::COLUMN_THREAD_NB_COLUMNS) // check that a single row is selected return QModelIndex(); return *selectedIndexes.begin(); } bool GxsForumThreadWidget::getCurrentPost(ForumModelPostEntry& fmpe) const { QModelIndex indx = getCurrentIndex() ; if(!indx.isValid()) return false ; return mThreadModel->getPostData(mThreadProxyModel->mapToSource(indx),fmpe); } void GxsForumThreadWidget::threadListCustomPopupMenu(QPoint /*point*/) { QMenu contextMnu(this); ForumModelPostEntry current_post ; bool has_current_post = getCurrentPost(current_post); #ifdef DEBUG_FORUMS std::cerr << "Clicked on msg " << current_post.mMsgId << std::endl; #endif QAction *editAct = new QAction(FilesDefs::getIconFromQtResourcePath(IMAGE_MESSAGEEDIT), tr("Edit"), &contextMnu); connect(editAct, SIGNAL(triggered()), this, SLOT(editforummessage())); bool this_is_pinned = mForumGroup.mPinnedPosts.ids.find(mThreadId) != mForumGroup.mPinnedPosts.ids.end(); QAction *pinUpPostAct = new QAction(FilesDefs::getIconFromQtResourcePath(IMAGE_PINPOST), (this_is_pinned?tr("Un-pin this post"):tr("Pin this post up")), &contextMnu); connect(pinUpPostAct , SIGNAL(triggered()), this, SLOT(togglePinUpPost())); QAction *replyAct = new QAction(FilesDefs::getIconFromQtResourcePath(IMAGE_REPLY), tr("Reply"), &contextMnu); connect(replyAct, SIGNAL(triggered()), this, SLOT(replytoforummessage())); QAction *replyauthorAct = new QAction(FilesDefs::getIconFromQtResourcePath(IMAGE_MESSAGEREPLY), tr("Reply to author with private message"), &contextMnu); connect(replyauthorAct, SIGNAL(triggered()), this, SLOT(reply_with_private_message())); QAction *flagaspositiveAct = new QAction(FilesDefs::getIconFromQtResourcePath(IMAGE_POSITIVE_OPINION), tr("Give positive opinion"), &contextMnu); flagaspositiveAct->setToolTip(tr("This will block/hide messages from this person, and notify friend nodes.")) ; flagaspositiveAct->setData(static_cast(RsOpinion::POSITIVE)); connect(flagaspositiveAct, SIGNAL(triggered()), this, SLOT(flagperson())); QAction *flagasneutralAct = new QAction(FilesDefs::getIconFromQtResourcePath(IMAGE_NEUTRAL_OPINION), tr("Give neutral opinion"), &contextMnu); flagasneutralAct->setToolTip(tr("Doing this, you trust your friends to decide to forward this message or not.")) ; flagasneutralAct->setData(static_cast(RsOpinion::NEUTRAL)); connect(flagasneutralAct, SIGNAL(triggered()), this, SLOT(flagperson())); QAction *flagasnegativeAct = new QAction(FilesDefs::getIconFromQtResourcePath(IMAGE_NEGATIVE_OPINION), tr("Give negative opinion"), &contextMnu); flagasnegativeAct->setToolTip(tr("This will block/hide messages from this person, and notify friend nodes.")) ; flagasnegativeAct->setData(static_cast(RsOpinion::NEGATIVE)); connect(flagasnegativeAct, SIGNAL(triggered()), this, SLOT(flagperson())); QAction *newthreadAct = new QAction(FilesDefs::getIconFromQtResourcePath(IMAGE_MESSAGE), tr("Start New Thread"), &contextMnu); newthreadAct->setEnabled (IS_GROUP_SUBSCRIBED(mForumGroup.mMeta.mSubscribeFlags)); connect(newthreadAct , SIGNAL(triggered()), this, SLOT(createthread())); QAction* expandAll = new QAction(tr("Expand all"), &contextMnu); connect(expandAll, SIGNAL(triggered()), ui->threadTreeWidget, SLOT(expandAll())); #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) QAction* expandSubtree = new QAction(tr("Expand subtree"), &contextMnu); connect(expandSubtree, SIGNAL(triggered()), this, SLOT(expandSubtree())); #endif QAction* collapseAll = new QAction(tr( "Collapse all"), &contextMnu); connect(collapseAll, SIGNAL(triggered()), ui->threadTreeWidget, SLOT(collapseAll())); QAction *markMsgAsRead = new QAction(FilesDefs::getIconFromQtResourcePath(":/images/message-mail-read.png"), tr("Mark as read"), &contextMnu); connect(markMsgAsRead, SIGNAL(triggered()), this, SLOT(markMsgAsRead())); QAction *markMsgAsReadChildren = new QAction(FilesDefs::getIconFromQtResourcePath(":/images/message-mail-read.png"), tr("Mark as read") + " (" + tr ("with children") + ")", &contextMnu); connect(markMsgAsReadChildren, SIGNAL(triggered()), this, SLOT(markMsgAsReadChildren())); QAction *markMsgAsUnread = new QAction(FilesDefs::getIconFromQtResourcePath(":/images/message-mail.png"), tr("Mark as unread"), &contextMnu); connect(markMsgAsUnread, SIGNAL(triggered()), this, SLOT(markMsgAsUnread())); QAction *markMsgAsUnreadChildren = new QAction(FilesDefs::getIconFromQtResourcePath(":/images/message-mail.png"), tr("Mark as unread") + " (" + tr ("with children") + ")", &contextMnu); connect(markMsgAsUnreadChildren, SIGNAL(triggered()), this, SLOT(markMsgAsUnreadChildren())); QAction *showinpeopleAct = new QAction(FilesDefs::getIconFromQtResourcePath(":/images/info16.png"), tr("Show author in people tab"), &contextMnu); connect(showinpeopleAct, SIGNAL(triggered()), this, SLOT(showInPeopleTab())); bool has_children = false; if (has_current_post) { has_children = !current_post.mChildren.empty(); } if (IS_GROUP_SUBSCRIBED(mForumGroup.mMeta.mSubscribeFlags)) { markMsgAsReadChildren->setEnabled(current_post.mPostFlags & ForumModelPostEntry::FLAG_POST_HAS_UNREAD_CHILDREN); markMsgAsUnreadChildren->setEnabled(current_post.mPostFlags & ForumModelPostEntry::FLAG_POST_HAS_READ_CHILDREN); #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) expandSubtree->setEnabled(has_children); #endif replyAct->setEnabled (true); replyauthorAct->setEnabled (true); } else { markMsgAsRead->setDisabled(true); markMsgAsReadChildren->setDisabled(true); markMsgAsUnread->setDisabled(true); markMsgAsUnreadChildren->setDisabled(true); replyAct->setDisabled (true); replyauthorAct->setDisabled (true); #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) expandSubtree->setDisabled(true); expandSubtree->setVisible(false); #endif } // disable visibility for childless if (has_current_post) { // still no setEnabled markMsgAsRead->setVisible(IS_MSG_UNREAD(current_post.mMsgStatus)); markMsgAsUnread->setVisible(!IS_MSG_UNREAD(current_post.mMsgStatus)); #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) expandSubtree->setVisible(has_children); #endif markMsgAsReadChildren->setVisible(has_children); markMsgAsUnreadChildren->setVisible(has_children); bool is_pinned = mForumGroup.mPinnedPosts.ids.find( current_post.mMsgId ) != mForumGroup.mPinnedPosts.ids.end(); if(!is_pinned) { RsGxsId author_id; if(rsIdentity->isOwnId(current_post.mAuthorId)) contextMnu.addAction(editAct); else { // Go through the list of own ids and see if one of them is a moderator // TODO: offer to select which moderator ID to use if multiple IDs fit the conditions of the forum std::list own_ids ; rsIdentity->getOwnIds(own_ids) ; for(auto it(own_ids.begin());it!=own_ids.end();++it) if(mForumGroup.canEditPosts(*it)) { contextMnu.addAction(editAct); break ; } } } if(IS_GROUP_ADMIN(mForumGroup.mMeta.mSubscribeFlags) && (current_post.mParent == 0)) contextMnu.addAction(pinUpPostAct); } contextMnu.addAction(replyAct); contextMnu.addAction(newthreadAct); QAction* action = contextMnu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_COPYLINK), tr("Copy RetroShare Link"), this, SLOT(copyMessageLink())); action->setEnabled(!groupId().isNull() && !mThreadId.isNull()); contextMnu.addSeparator(); contextMnu.addAction(markMsgAsRead); contextMnu.addAction(markMsgAsReadChildren); contextMnu.addAction(markMsgAsUnread); contextMnu.addAction(markMsgAsUnreadChildren); contextMnu.addSeparator(); contextMnu.addAction(expandAll); #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) contextMnu.addAction(expandSubtree); #endif contextMnu.addAction(collapseAll); if(has_current_post) { #ifdef DEBUG_FORUMS std::cerr << "Author is: " << current_post.mAuthorId << std::endl; #endif contextMnu.addSeparator(); RsOpinion op; if(!rsIdentity->isOwnId(current_post.mAuthorId) && rsReputations->getOwnOpinion(current_post.mAuthorId,op)) { QMenu *submenu1 = contextMnu.addMenu(tr("Author's reputation")) ; if(op != RsOpinion::POSITIVE) submenu1->addAction(flagaspositiveAct); if(op != RsOpinion::NEUTRAL) submenu1->addAction(flagasneutralAct); if(op != RsOpinion::NEGATIVE) submenu1->addAction(flagasnegativeAct); } contextMnu.addAction(showinpeopleAct); contextMnu.addAction(replyauthorAct); } contextMnu.exec(QCursor::pos()); } void GxsForumThreadWidget::headerContextMenuRequested(const QPoint &pos) { QMenu* header_context_menu = new QMenu(tr("Show column"), this); QAction* title = header_context_menu->addAction(QIcon(), tr("Title")); title->setCheckable(true); title->setChecked(!ui->threadTreeWidget->isColumnHidden(RsGxsForumModel::COLUMN_THREAD_TITLE)); title->setData(RsGxsForumModel::COLUMN_THREAD_TITLE); connect(title, SIGNAL(toggled(bool)), this, SLOT(changeHeaderColumnVisibility(bool))); QAction* read = header_context_menu->addAction(QIcon(), tr("Read")); read->setCheckable(true); read->setChecked(!ui->threadTreeWidget->isColumnHidden(RsGxsForumModel::COLUMN_THREAD_READ)); read->setData(RsGxsForumModel::COLUMN_THREAD_READ); connect(read, SIGNAL(toggled(bool)), this, SLOT(changeHeaderColumnVisibility(bool))); QAction* date = header_context_menu->addAction(QIcon(), tr("Date")); date->setCheckable(true); date->setChecked(!ui->threadTreeWidget->isColumnHidden(RsGxsForumModel::COLUMN_THREAD_DATE)); date->setData(RsGxsForumModel::COLUMN_THREAD_DATE); connect(date, SIGNAL(toggled(bool)), this, SLOT(changeHeaderColumnVisibility(bool))); QAction* distribution = header_context_menu->addAction(QIcon(), tr("Distribution")); distribution->setCheckable(true); distribution->setChecked(!ui->threadTreeWidget->isColumnHidden(RsGxsForumModel::COLUMN_THREAD_DISTRIBUTION)); distribution->setData(RsGxsForumModel::COLUMN_THREAD_DISTRIBUTION); connect(distribution, SIGNAL(toggled(bool)), this, SLOT(changeHeaderColumnVisibility(bool))); // QAction* author = header_context_menu->addAction(QIcon(), tr("Author")); // author->setCheckable(true); // author->setChecked(!ui->threadTreeWidget->isColumnHidden(RsGxsForumModel::COLUMN_THREAD_AUTHOR)); // author->setData(RsGxsForumModel::COLUMN_THREAD_AUTHOR); // connect(author, SIGNAL(toggled(bool)), this, SLOT(changeHeaderColumnVisibility(bool))); QAction* show_text_from_banned = header_context_menu->addAction(QIcon(), tr("Show text from banned persons")); show_text_from_banned->setCheckable(true); show_text_from_banned->setChecked(mDisplayBannedText); connect(show_text_from_banned, SIGNAL(toggled(bool)), this, SLOT(showBannedText(bool))); header_context_menu->exec(mapToGlobal(pos)); delete(header_context_menu); } void GxsForumThreadWidget::changeHeaderColumnVisibility(bool visibility) { QAction* the_action = qobject_cast(sender()); if ( !the_action ) { return; } ui->threadTreeWidget->setColumnHidden(the_action->data().toInt(), !visibility); } void GxsForumThreadWidget::showBannedText(bool display) { mDisplayBannedText = display; if (!mThreadId.isNull()) { updateMessageData(mThreadId); } } #ifdef TODO bool GxsForumThreadWidget::eventFilter(QObject *obj, QEvent *event) { if (obj == ui->threadTreeWidget) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent && keyEvent->key() == Qt::Key_Space) { // Space pressed QTreeWidgetItem *item = ui->threadTreeWidget->currentItem(); clickedThread (item, RsGxsForumModel::COLUMN_THREAD_READ); return true; // eat event } } } // pass the event on to the parent class return RsGxsUpdateBroadcastWidget::eventFilter(obj, event); } #endif void GxsForumThreadWidget::togglethreadview() { // save state of button Settings->setValueToGroup("ForumThreadWidget", "expandButton", ui->expandButton->isChecked()); togglethreadview_internal(); } void GxsForumThreadWidget::togglethreadview_internal() { // if (ui->expandButton->isChecked()) { ui->postText->setVisible(true); ui->expandButton->setIcon(FilesDefs::getIconFromQtResourcePath(QString(":/images/edit_remove24.png"))); ui->expandButton->setToolTip(tr("Hide")); // } else { // ui->postText->setVisible(false); // ui->expandButton->setIcon(FilesDefs::getIconFromQtResourcePath(QString(":/images/edit_add24.png"))); // ui->expandButton->setToolTip(tr("Expand")); // } } void GxsForumThreadWidget::changedVersion() { //if(mUpdating) // return; mThreadId = RsGxsMessageId(ui->versions_CB->itemData(ui->versions_CB->currentIndex()).toString().toStdString()) ; ui->postText->resetImagesStatus(Settings->getForumLoadEmbeddedImages()) ; insertMessage(); } void GxsForumThreadWidget::changedThread(QModelIndex index) { if(!index.isValid()) return; RsGxsMessageId new_id(index.sibling(index.row(),RsGxsForumModel::COLUMN_THREAD_MSGID).data(Qt::UserRole).toString().toStdString()); if(new_id == mThreadId) return; mThreadId = mOrigThreadId = new_id; mLastSelectedPosts[groupId()] = new_id; #ifdef DEBUG_FORUMS std::cerr << "Switched to new thread ID " << mThreadId << std::endl; #endif insertMessage(); if(Settings->getForumMsgSetToReadOnActivate()) { #ifdef DEBUG_FORUMS std::cerr << "Setting message read status to true" << std::endl; #endif markMsgAsReadUnread(true, false, false); } } void GxsForumThreadWidget::clickedThread(QModelIndex index) { #ifdef DEBUG_FORUMS std::cerr << "Clicked on message ID " << mThreadId << ", index=" << index << std::endl; #endif if(!index.isValid()) { #ifdef DEBUG_FORUMS std::cerr << " early return because index is invalid" << std::endl; #endif return; } if (index.column() == RsGxsForumModel::COLUMN_THREAD_READ) { QModelIndex src_index = mThreadProxyModel->mapToSource(index); ForumModelPostEntry fmpe; mThreadModel->getPostData(src_index,fmpe); #ifdef DEBUG_FORUMS std::cerr << "Setting message read status to false" << std::endl; #endif // First Load Message (may change read status) to not recall it after index change. changedThread(index); // Now index is invalid as model was reloaded, Selection isn't updated. markMsgAsReadUnread(IS_MSG_UNREAD(static_cast(fmpe.mMsgStatus)), false, false, mThreadId); } #ifdef DEBUG_FORUMS else std::cerr << " doing nothing" << std::endl; #endif } static QString getDurationString(uint32_t days) { switch(days) { case 0: return QObject::tr("Indefinitely") ; break; case 5: return QObject::tr("5 days") ; break; case 15: return QObject::tr("2 weeks") ; break; case 30: return QObject::tr("1 month") ; break; case 60: return QObject::tr("2 month") ; break; case 180: return QObject::tr("6 month") ; break; case 365: return QObject::tr("1 year") ; break; case 1095: return QObject::tr("3 years") ; break; case 1825: return QObject::tr("5 years") ; break; default: return QString::number(days)+" " + QObject::tr("days") ; } } void GxsForumThreadWidget::setForumDescriptionLoading() { ui->postText->setText(tr("Loading...")); } void GxsForumThreadWidget::clearForumDescription() { ui->postText->clear(); } void GxsForumThreadWidget::blankPost() { ui->newmessageButton->setEnabled(false); ui->previousButton->setEnabled(false); ui->nextButton->setEnabled(false); ui->downloadButton->setEnabled(false); ui->lineLeft->hide(); ui->time_label->clear(); ui->versions_CB->hide(); ui->lineRight->hide(); ui->by_text_label->hide(); ui->by_label->setId(RsGxsId()) ; ui->by_label->hide(); ui->expandButton->hide(); ui->postText->clear() ; ui->postText->setImageBlockWidget(ui->imageBlockWidget) ; ui->postText->resetImagesStatus(Settings->getForumLoadEmbeddedImages()); } void GxsForumThreadWidget::updateForumDescription(bool success) { if(!success) { blank(); QString forum_description = QString("ERROR: Forum could not be loaded. Database might be in heavy use. Please try later."); ui->postText->setText(forum_description); ui->newthreadButton->setEnabled(false); return; } std::cerr << "Updating forum description" << std::endl; if (!mThreadId.isNull()) return; // still call it to not left leftovers from previous post if any blankPost(); RsIdentityDetails details; rsIdentity->getIdDetails(mForumGroup.mMeta.mAuthorId,details); QString author = GxsIdDetails::getName(details); const RsGxsForumGroup& group = mForumGroup; ui->newthreadButton->show(); ui->forumName->setText(QString::fromUtf8(group.mMeta.mGroupName.c_str())); ui->flatView_TB->setEnabled(true); ui->threadedView_TB->setEnabled(true); ui->latestPostInThreadView_TB->setEnabled(true); ui->filterLineEdit->setEnabled(true); QString anti_spam_features1 ; QString forum_description; if(IS_GROUP_PGP_KNOWN_AUTHED(mForumGroup.mMeta.mSignFlags)) anti_spam_features1 = tr("Anonymous/unknown posts forwarded if reputation is positive"); else if(IS_GROUP_PGP_AUTHED(mForumGroup.mMeta.mSignFlags)) anti_spam_features1 = tr("Anonymous posts forwarded if reputation is positive"); forum_description = QString("%1: \t%2
").arg(tr("Forum name"), QString::fromUtf8( group.mMeta.mGroupName.c_str())); forum_description += QString("%1: %2
").arg(tr("Description"), group.mDescription.empty()? tr("[None]
") :(QString::fromUtf8(group.mDescription.c_str())+"
")); forum_description += QString("%1: \t%2
").arg(tr("Subscribers")).arg(group.mMeta.mPop); forum_description += QString("%1: \t%2
").arg(tr("Posts (at neighbor nodes)")).arg(group.mMeta.mVisibleMsgCount); if(group.mMeta.mLastPost==0) forum_description += QString("%1: \t%2
").arg(tr("Last post"),tr("Never")); else forum_description += QString("%1: \t%2
").arg(tr("Last post"),DateTime::formatLongDateTime(group.mMeta.mLastPost)); if(IS_GROUP_SUBSCRIBED(group.mMeta.mSubscribeFlags)) { forum_description += QString("%1: \t%2
").arg(tr("Synchronization"),getDurationString( rsGxsForums->getSyncPeriod(group.mMeta.mGroupId)/86400 )) ; forum_description += QString("%1: \t%2
").arg(tr("Storage"),getDurationString( rsGxsForums->getStoragePeriod(group.mMeta.mGroupId)/86400)); } else { if(group.mMeta.mLastSeen > 0) forum_description += QString("%1: \t%2 days ago
").arg(tr("Last seen at friends:"),QString::number((time(nullptr) - group.mMeta.mLastSeen)/86400)); } QString distrib_string = tr("[unknown]"); switch(static_cast(group.mMeta.mCircleType)) { case RsGxsCircleType::PUBLIC: distrib_string = tr("Public"); break ; case RsGxsCircleType::EXTERNAL: { RsGxsCircleDetails det ; // !! What we need here is some sort of CircleLabel, which loads the circle and updates the label when done. if(rsGxsCircles->getCircleDetails(group.mMeta.mCircleId,det)) distrib_string = tr("Restricted to members of circle \"")+QString::fromUtf8(det.mCircleName.c_str()) +"\""; else distrib_string = tr("Restricted to members of circle ")+QString::fromStdString(group.mMeta.mCircleId.toStdString()) ; } break ; case RsGxsCircleType::NODES_GROUP: { distrib_string = tr("Only friends nodes in group ") ; RsGroupInfo ginfo ; rsPeers->getGroupInfo(RsNodeGroupId(group.mMeta.mInternalCircle),ginfo) ; QString desc; GroupChooser::makeNodeGroupDesc(ginfo, desc); distrib_string += desc ; } break ; case RsGxsCircleType::LOCAL: distrib_string = tr("Your eyes only"); // this is not yet supported. If you see this, it is a bug! break ; default: std::cerr << "(EE) badly initialised group distribution ID = " << group.mMeta.mCircleType << std::endl; } forum_description += QString("%1: \t%2
").arg(tr("Distribution"), distrib_string); forum_description += QString("%1: \t%2
").arg(tr("Owner"), author); if(!anti_spam_features1.isNull()) forum_description += QString("%1: \t%2
").arg(tr("Anti-spam"),anti_spam_features1); ui->subscribeToolButton->setSubscribed(IS_GROUP_SUBSCRIBED(mForumGroup.mMeta.mSubscribeFlags)); ui->newthreadButton->setEnabled(IS_GROUP_SUBSCRIBED(mForumGroup.mMeta.mSubscribeFlags)); if(!group.mAdminList.ids.empty()) { QString admin_list_str ; for(auto it(group.mAdminList.ids.begin());it!=group.mAdminList.ids.end();++it) { RsIdentityDetails det ; rsIdentity->getIdDetails(*it,det); admin_list_str += (admin_list_str.isNull()?"":", ") + QString::fromUtf8(det.mNickname.c_str()) ; } forum_description += QString("%1: %2").arg(tr("Moderators"), admin_list_str); } ui->postText->setText(forum_description); } void GxsForumThreadWidget::insertMessage() { #ifdef DEBUG_FORUMS std::cerr << "Inserting message, threadId=" << mThreadId <versions_CB->hide(); ui->time_label->show(); ui->postText->clear(); return; } if (mThreadId.isNull()) { #ifdef DEBUG_FORUMS std::cerr << " mThreadId=NULL !! That's a bug." << std::endl; #endif ui->versions_CB->hide(); ui->time_label->show(); ui->postText->setText(QString::fromUtf8(mForumGroup.mDescription.c_str())); return; } /* blank text, incase we get nothing */ blankPost(); ui->nextUnreadButton->setEnabled(true); // We use this instead of getCurrentIndex() because right here the currentIndex() is not set yet. QModelIndex index = mThreadProxyModel->mapFromSource(mThreadModel->getIndexOfMessage(mOrigThreadId)); if (index.isValid()) { QModelIndex parentIndex = index.parent(); int curr_index = index.row(); int count = mThreadProxyModel->rowCount(parentIndex); ui->previousButton->setEnabled(curr_index > 0); ui->nextButton->setEnabled(curr_index < count - 1); } else { #ifdef DEBUG_FORUMS std::cerr << " current index invalid! That's a bug." << std::endl; #endif // there is something wrong ui->previousButton->setEnabled(false); ui->nextButton->setEnabled(false); ui->versions_CB->hide(); ui->time_label->show(); return; } ui->newmessageButton->setEnabled(IS_GROUP_SUBSCRIBED(mForumGroup.mMeta.mSubscribeFlags) && mThreadId.isNull() == false); // add/show combobox for versions, if applicable, and enable it. If no older versions of the post available, hide the combobox. std::vector > post_versions = mThreadModel->getPostVersions(mOrigThreadId); #ifdef DEBUG_FORUMS std::cerr << "Looking into existing versions for post " << mOrigThreadId << ", thread history: " << post_versions.size() << std::endl; #endif ui->versions_CB->blockSignals(true) ; while(ui->versions_CB->count() > 0) ui->versions_CB->removeItem(0); if(!post_versions.empty()) { #ifdef DEBUG_FORUMS std::cerr << post_versions.size() << " versions found " << std::endl; #endif ui->versions_CB->setVisible(true) ; ui->time_label->hide(); int current_index = 0 ; for(int i=0;i(post_versions.size());++i) { ui->versions_CB->insertItem(i, ((i==0)?tr("(Latest) "):tr("(Old) "))+" "+DateTime::formatLongDateTime( post_versions[i].first)); ui->versions_CB->setItemData(i,QString::fromStdString(post_versions[i].second.toStdString())); #ifdef DEBUG_FORUMS std::cerr << " added new post version " << post_versions[i].first << " " << post_versions[i].second << std::endl; #endif if(mThreadId == post_versions[i].second) current_index = i ; } ui->versions_CB->setCurrentIndex(current_index) ; } else { ui->versions_CB->hide(); ui->time_label->show(); } ui->versions_CB->blockSignals(false) ; /* request Post */ bool missing = index.sibling(index.row(),RsGxsForumModel::COLUMN_THREAD_DATA).data(RsGxsForumModel::MissingRole).toBool(); if (missing) { // Don't update data for missing message else get multiple entry. setMessageLoadingError(tr("Missing Message:\nThis message is missing. You should receive it later.")); } else { updateMessageData(mThreadId); } // markMsgAsRead(); } void GxsForumThreadWidget::setMessageLoadingError(const QString& error) { ui->time_label->setText(QString("")); ui->by_label->setId(RsGxsId()); ui->lineRight->show(); ui->lineLeft->show(); ui->by_text_label->show(); ui->by_label->show(); ui->threadTreeWidget->setFocus(); ui->postText->setText(error); } void GxsForumThreadWidget::insertMessageData(const RsGxsForumMsg &msg) { /* As some time has elapsed since request - check that this is still the current msg. * otherwise, another request will fill the data */ if ((msg.mMeta.mGroupId != groupId()) || (msg.mMeta.mMsgId != mThreadId)) { std::cerr << "GxsForumThreadWidget::insertPostData() Ignoring Invalid Data...."; std::cerr << std::endl; std::cerr << "\t CurrForumId: " << groupId() << " != msg.GroupId: " << msg.mMeta.mGroupId; std::cerr << std::endl; std::cerr << "\t or CurrThdId: " << mThreadId << " != msg.MsgId: " << msg.mMeta.mMsgId; std::cerr << std::endl; std::cerr << std::endl; return; } RsReputationLevel overall_reputation = rsReputations->overallReputationLevel(msg.mMeta.mAuthorId); bool redacted = (overall_reputation == RsReputationLevel::LOCALLY_NEGATIVE); // TODO enabled even when there are no new message ui->nextUnreadButton->setEnabled(true); ui->lineLeft->show(); ui->time_label->setText(DateTime::formatLongDateTime(msg.mMeta.mPublishTs)); ui->lineRight->show(); ui->by_text_label->show(); ui->by_label->setId(msg.mMeta.mAuthorId); ui->by_label->show(); ui->threadTreeWidget->setFocus(); QString banned_text_info = ""; if(redacted) { ui->downloadButton->setDisabled(true); if (!mDisplayBannedText) { QString extraTxt = tr( "

The author of this message (with ID %1) is banned.").arg(QString::fromStdString(msg.mMeta.mAuthorId.toStdString())) ; extraTxt += tr( "

  • Messages from this author are not forwarded.
  • ") ; extraTxt += tr( "
  • Messages from this author are replaced by this text.
") ; extraTxt += tr( "

You can force the visibility and forwarding of messages by setting a different opinion for that Id in People's tab.

") ; ui->postText->setHtml(extraTxt) ; return; } else { RsIdentityDetails details; rsIdentity->getIdDetails(msg.mMeta.mAuthorId, details); QString name = GxsIdDetails::getName(details); banned_text_info += "

" + tr( "The author of this message (with ID %1) is banned. And named by name ( %2 )").arg(QString::fromStdString(msg.mMeta.mAuthorId.toStdString()), name) + ""; banned_text_info += "

  • " + tr( "Messages from this author are not forwarded.") + "
"; banned_text_info += "

" + tr( "You can force the visibility and forwarding of messages by setting a different opinion for that Id in People's tab.") + "


"; } } uint32_t flags = RSHTML_FORMATTEXT_EMBED_LINKS; if(Settings->getForumLoadEmoticons()) flags |= RSHTML_FORMATTEXT_EMBED_SMILEYS ; flags |= RSHTML_OPTIMIZEHTML_MASK; QColor backgroundColor = ui->postText->palette().base().color(); qreal desiredContrast = Settings->valueFromGroup("Forum", "MinimumContrast", 4.5).toDouble(); int desiredMinimumFontSize = Settings->valueFromGroup("Forum", "MinimumFontSize", 10).toInt(); QString extraTxt = banned_text_info + RsHtml().formatText(ui->postText->document(), QString::fromUtf8(msg.mMsg.c_str()), flags , backgroundColor, desiredContrast, desiredMinimumFontSize ); ui->postText->setHtml(extraTxt); QStringList urls; RsHtml::findAnchors(ui->postText->toHtml(), urls); ui->downloadButton->setEnabled(urls.count() > 0); } void GxsForumThreadWidget::previousMessage() { QModelIndex current_index = getCurrentIndex(); if (!current_index.isValid()) return; QModelIndex parentIndex = current_index.parent(); int index = current_index.row(); //int count = mThreadModel->rowCount(parentIndex) ; if (index > 0) { QModelIndex prevItem = mThreadProxyModel->index(index - 1,0,parentIndex) ; if (prevItem.isValid()) { ui->threadTreeWidget->setCurrentIndex(prevItem); ui->threadTreeWidget->scrollTo(ui->threadTreeWidget->currentIndex());//May change if model reloaded ui->threadTreeWidget->setFocus(); } } ui->previousButton->setEnabled(index-1 > 0); ui->nextButton->setEnabled(true); } void GxsForumThreadWidget::nextMessage() { QModelIndex current_index = getCurrentIndex(); if (!current_index.isValid()) return; QModelIndex parentIndex = current_index.parent(); int index = current_index.row(); int count = mThreadProxyModel->rowCount(parentIndex); if (index < count - 1) { QModelIndex nextItem = mThreadProxyModel->index(index + 1,0,parentIndex) ; if (nextItem.isValid()) { ui->threadTreeWidget->setCurrentIndex(nextItem); ui->threadTreeWidget->scrollTo(ui->threadTreeWidget->currentIndex()); //May change if model reloaded ui->threadTreeWidget->setFocus(); } } ui->previousButton->setEnabled(true); ui->nextButton->setEnabled(index+1 < count - 1); } void GxsForumThreadWidget::downloadAllFiles() { QStringList urls; if (RsHtml::findAnchors(ui->postText->toHtml(), urls) == false) { return; } if (urls.count() == 0) { return; } RetroShareLink::process(urls, RetroShareLink::TYPE_FILE/*, true*/); } void GxsForumThreadWidget::nextUnreadMessage() { QModelIndex index = getCurrentIndex(); if(!index.isValid()) index = mThreadProxyModel->index(0,0); else { if(index.data(RsGxsForumModel::UnreadChildrenRole).toBool()) ui->threadTreeWidget->expand(index); index = ui->threadTreeWidget->indexBelow(index); } while(index.isValid() && !IS_MSG_UNREAD(index.sibling(index.row(),RsGxsForumModel::COLUMN_THREAD_DATA).data(RsGxsForumModel::StatusRole).toUInt())) { if(index.data(RsGxsForumModel::UnreadChildrenRole).toBool()) ui->threadTreeWidget->expand(index); index = ui->threadTreeWidget->indexBelow(index); } ui->threadTreeWidget->setCurrentIndex(index); ui->threadTreeWidget->scrollTo(ui->threadTreeWidget->currentIndex());//May change if model reloaded } void GxsForumThreadWidget::markMsgAsReadUnread (bool read, bool children, bool forum, RsGxsMessageId msgId /*= RsGxsMessageId()*/) { if (groupId().isNull() || !IS_GROUP_SUBSCRIBED(mForumGroup.mMeta.mSubscribeFlags)) { return; } saveExpandedItems(mSavedExpandedMessages); QModelIndex src_index; if(forum) src_index = mThreadModel->root(); else { if (!msgId.isNull()) src_index = mThreadProxyModel->mapToSource(getCurrentIndex()); else src_index = mThreadModel->getIndexOfMessage(mThreadId); } mThreadModel->setMsgReadStatus(src_index,read,children); //Restore Selection whileBlocking(ui->threadTreeWidget)->setCurrentIndex(mThreadProxyModel->mapFromSource(mThreadModel->getIndexOfMessage(mThreadId))); recursRestoreExpandedItems(QModelIndex(),mSavedExpandedMessages); } void GxsForumThreadWidget::markMsgAsRead() { markMsgAsReadUnread(true, false, false); } void GxsForumThreadWidget::markMsgAsReadChildren() { markMsgAsReadUnread(true, true, false); } void GxsForumThreadWidget::markMsgAsUnread() { markMsgAsReadUnread(false, false, false); } void GxsForumThreadWidget::markMsgAsUnreadChildren() { markMsgAsReadUnread(false, true, false); } void GxsForumThreadWidget::setAllMessagesReadDo(bool read) { markMsgAsReadUnread(read, true, true); } #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) void GxsForumThreadWidget::expandSubtree() { QAction* the_action = qobject_cast(sender()); if (!the_action) { return; } const QModelIndex current_index = ui->threadTreeWidget->currentIndex(); if (!current_index.isValid()) { return; } ui->threadTreeWidget->expandRecursively(current_index); } #endif bool GxsForumThreadWidget::navigate(const RsGxsMessageId &msgId) { QModelIndex source_index = mThreadModel->getIndexOfMessage(msgId); if(!source_index.isValid()) { std::cerr << "(EE) Cannot navigate to msg " << msgId << " in forum " << mForumGroup.mMeta.mGroupId << ": index unknown. Setting mNavigatePendingMsgId." << std::endl; mNavigatePendingMsgId = msgId; // not found. That means the forum may not be loaded yet. So we keep that post in mind, for after loading. return true; // we have to return true here, otherwise the caller will intepret the async loading as an error. } QModelIndex indx = mThreadProxyModel->mapFromSource(source_index); ui->threadTreeWidget->selectionModel()->setCurrentIndex(indx,QItemSelectionModel::ClearAndSelect); ui->threadTreeWidget->scrollTo(ui->threadTreeWidget->currentIndex());//May change if model reloaded ui->threadTreeWidget->setFocus(); mNavigatePendingMsgId.clear(); return true; } void GxsForumThreadWidget::copyMessageLink() { if (groupId().isNull() || mThreadId.isNull()) { return; } ForumModelPostEntry fmpe ; getCurrentPost(fmpe); QString thread_title = QString::fromUtf8(fmpe.mTitle.c_str()); RetroShareLink link = RetroShareLink::createGxsMessageLink(RetroShareLink::TYPE_FORUM, groupId(), mThreadId, thread_title); if (link.valid()) { QList urls; urls.push_back(link); RSLinkClipboard::copyLinks(urls); } } void GxsForumThreadWidget::subscribeGroup(bool subscribe) { if (groupId().isNull()) { return; } uint32_t token; rsGxsForums->subscribeToGroup(token, groupId(), subscribe); } void GxsForumThreadWidget::createmessage() { if (groupId().isNull () || !IS_GROUP_SUBSCRIBED(mForumGroup.mMeta.mSubscribeFlags)) { return; } CreateGxsForumMsg *cfm = new CreateGxsForumMsg(groupId(), mThreadId,RsGxsMessageId()); cfm->show(); /* window will destroy itself! */ } void GxsForumThreadWidget::togglePinUpPost() { if (groupId().isNull() || mOrigThreadId.isNull()) return; QModelIndex index = getCurrentIndex(); // normally this method is only called on top level items. We still check it just in case... if(mThreadProxyModel->mapToSource(index).parent().isValid()) { std::cerr << "(EE) togglePinUpPost() called on non top level post. This is inconsistent." << std::endl; return ; } #ifdef DEBUG_FORUMS QString thread_title = index.sibling(index.row(),RsGxsForumModel::COLUMN_THREAD_TITLE).data(Qt::DisplayRole).toString(); std::cerr << "Toggling Pin-up state of post " << mThreadId.toStdString() << ": \"" << thread_title.toStdString() << "\"" << std::endl; #endif if(mForumGroup.mPinnedPosts.ids.find(mThreadId) == mForumGroup.mPinnedPosts.ids.end()) mForumGroup.mPinnedPosts.ids.insert(mThreadId) ; else mForumGroup.mPinnedPosts.ids.erase(mThreadId) ; uint32_t token; rsGxsForums->updateGroup(token,mForumGroup); // We dont call this from here anymore. The update will be called by libretroshare using the rsEvent system when // the data is actually updated. // groupIdChanged(); // reloads all posts. We could also update the model directly, but the cost is so small now ;-) // updateDisplay(true) ; } void GxsForumThreadWidget::createthread() { if (groupId().isNull ()) { QMessageBox::information(this, tr("RetroShare"), tr("No Forum Selected!")); return; } CreateGxsForumMsg *cfm = new CreateGxsForumMsg(groupId(), RsGxsMessageId(),RsGxsMessageId()); cfm->show(); /* window will destroy itself! */ } static QString buildReplyHeader(const RsMsgMetaData &meta) { RetroShareLink link = RetroShareLink::createMessage(meta.mAuthorId, ""); QString from = link.toHtml(); QString header = QString("-----%1-----").arg(QApplication::translate("GxsForumThreadWidget", "Original Message")); header += QString("
%1: %2
").arg(QApplication::translate("GxsForumThreadWidget", "From"), from); header += QString("
%1: %2
").arg(QApplication::translate("GxsForumThreadWidget", "Sent"), DateTime::formatLongDateTime(meta.mPublishTs)); header += QString("%1: %2

").arg(QApplication::translate("GxsForumThreadWidget", "Subject"), QString::fromUtf8(meta.mMsgName.c_str())); header += "
"; header += QApplication::translate("GxsForumThreadWidget", "On %1, %2 wrote:").arg(DateTime::formatDateTime(meta.mPublishTs), from); return header; } void GxsForumThreadWidget::flagperson() { // no need to use the token system for that, since we just need to find out the author's name, which is in the item. if (groupId().isNull() || mThreadId.isNull()) { QMessageBox::information(this, tr("RetroShare"),tr("You cant reply to a non-existant Message")); return; } RsOpinion opinion = static_cast( qobject_cast(sender())->data().toUInt() ); mThreadModel->setAuthorOpinion( mThreadProxyModel->mapToSource(getCurrentIndex()), opinion ); } void GxsForumThreadWidget::replytoforummessage() { async_msg_action( &GxsForumThreadWidget::replyForumMessageData ); } void GxsForumThreadWidget::editforummessage() { async_msg_action( &GxsForumThreadWidget::editForumMessageData ); } void GxsForumThreadWidget::reply_with_private_message() { async_msg_action( &GxsForumThreadWidget::replyMessageData ); } void GxsForumThreadWidget::showInPeopleTab() { async_msg_action( &GxsForumThreadWidget::showAuthorInPeople ); } void GxsForumThreadWidget::async_msg_action(const MsgMethod &action) { if (groupId().isNull() || mThreadId.isNull()) { QMessageBox::information(this, tr("RetroShare"),tr("You cant reply to a non-existant Message")); return; } RsThread::async([this,action]() { // 1 - get message data from p3GxsForums #ifdef DEBUG_FORUMS std::cerr << "Retrieving post data for post " << mThreadId << std::endl; #endif std::set msgs_to_request ; std::vector msgs; msgs_to_request.insert(mThreadId); if(!rsGxsForums->getForumContent(groupId(),msgs_to_request,msgs)) { std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve forum message info for forum " << groupId() << " and thread " << mThreadId << std::endl; return; } if(msgs.size() != 1) { std::cerr << __PRETTY_FUNCTION__ << " more than 1 or no msgs selected in forum " << groupId() << std::endl; return; } // 2 - sort the messages into a proper hierarchy RsGxsForumMsg msg = msgs[0]; // 3 - update the model in the UI thread. RsQThreadUtils::postToObject( [msg,action,this]() { /* Here it goes any code you want to be executed on the Qt Gui * thread, for example to update the data model with new information * after a blocking call to RetroShare API complete */ (this->*action)(msg); }, this ); }); } void GxsForumThreadWidget::replyMessageData(const RsGxsForumMsg &msg) { if ((msg.mMeta.mGroupId != groupId()) || (msg.mMeta.mMsgId != mThreadId)) { std::cerr << "(EE) GxsForumThreadWidget::replyMessageData() ERROR Message Ids have changed!"; std::cerr << std::endl; return; } if (!msg.mMeta.mAuthorId.isNull()) { MessageComposer *msgDialog = MessageComposer::newMsg(); msgDialog->setTitleText(QString::fromUtf8(msg.mMeta.mMsgName.c_str()), MessageComposer::REPLY); msgDialog->setQuotedMsg(QString::fromUtf8(msg.mMsg.c_str()), buildReplyHeader(msg.mMeta)); msgDialog->addRecipient(MessageComposer::TO, RsGxsId(msg.mMeta.mAuthorId)); msgDialog->show(); msgDialog->activateWindow(); /* window will destroy itself! */ } else { QMessageBox::information(this, tr("RetroShare"),tr("You cant reply to an Anonymous Author")); } } void GxsForumThreadWidget::editForumMessageData(const RsGxsForumMsg& msg) { if ((msg.mMeta.mGroupId != groupId()) || (msg.mMeta.mMsgId != mThreadId)) { std::cerr << "(EE) GxsForumThreadWidget::replyMessageData() ERROR Message Ids have changed!"; std::cerr << std::endl; return; } // Go through the list of own ids and see if one of them is a moderator // TODO: offer to select which moderator ID to use if multiple IDs fit the conditions of the forum RsGxsId moderator_id ; std::list own_ids ; rsIdentity->getOwnIds(own_ids) ; for(auto it(own_ids.begin());it!=own_ids.end();++it) if(mForumGroup.mAdminList.ids.find(*it) != mForumGroup.mAdminList.ids.end()) { moderator_id = *it; break; } // Check that author is in own ids, if not use the moderator id that was collected among own ids. bool is_own = false ; for(auto it(own_ids.begin());it!=own_ids.end() && !is_own;++it) if(*it == msg.mMeta.mAuthorId) is_own = true ; if (!msg.mMeta.mAuthorId.isNull()) { CreateGxsForumMsg *cfm = new CreateGxsForumMsg(groupId(), msg.mMeta.mParentId, msg.mMeta.mMsgId, is_own?(msg.mMeta.mAuthorId):moderator_id,!is_own); cfm->insertPastedText(QString::fromUtf8(msg.mMsg.c_str())) ; cfm->show(); /* window will destroy itself! */ } else { QMessageBox::information(this, tr("RetroShare"),tr("You cant reply to an Anonymous Author")); } } void GxsForumThreadWidget::replyForumMessageData(const RsGxsForumMsg &msg) { if ((msg.mMeta.mGroupId != groupId()) || (msg.mMeta.mMsgId != mThreadId)) { std::cerr << "(EE) GxsForumThreadWidget::replyMessageData() ERROR Message Ids have changed!"; std::cerr << std::endl; return; } if (!msg.mMeta.mAuthorId.isNull()) { CreateGxsForumMsg *cfm = new CreateGxsForumMsg(groupId(), mThreadId,RsGxsMessageId()); RsHtml::makeQuotedText(ui->postText); cfm->insertPastedText(RsHtml::makeQuotedText(ui->postText)) ; cfm->show(); /* window will destroy itself! */ } else { QMessageBox::information(this, tr("RetroShare"),tr("You cant reply to an Anonymous Author")); } } void GxsForumThreadWidget::toggleThreadedView(bool b) { if(b) changedViewBox(VIEW_THREADED); } void GxsForumThreadWidget::toggleFlatView(bool b) { if(b) changedViewBox(VIEW_FLAT); } void GxsForumThreadWidget::toggleLstPostInThreadView(bool b) { if(b) changedViewBox(VIEW_LAST_POST); } void GxsForumThreadWidget::changedViewBox(int view_mode) { ui->threadTreeWidget->selectionModel()->clear(); ui->threadTreeWidget->selectionModel()->reset(); mThreadId.clear(); // save index Settings->setValueToGroup("ForumThreadWidget", "viewBox", view_mode); if(view_mode == VIEW_FLAT) mThreadModel->setTreeMode(RsGxsForumModel::TREE_MODE_FLAT); else mThreadModel->setTreeMode(RsGxsForumModel::TREE_MODE_TREE); if(view_mode == VIEW_LAST_POST) mThreadModel->setSortMode(RsGxsForumModel::SORT_MODE_CHILDREN_PUBLISH_TS); else mThreadModel->setSortMode(RsGxsForumModel::SORT_MODE_PUBLISH_TS); if( (mLastSelectedPosts.count(groupId()) > 0) && !mLastSelectedPosts[groupId()].isNull() && mThreadModel->getIndexOfMessage(mLastSelectedPosts[groupId()]).isValid()) { QModelIndex source_index = mThreadModel->getIndexOfMessage(mLastSelectedPosts[groupId()]); QModelIndex index = mThreadProxyModel->mapFromSource(source_index); ui->threadTreeWidget->selectionModel()->setCurrentIndex(index,QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); ui->threadTreeWidget->scrollTo(ui->threadTreeWidget->currentIndex());//May change if model reloaded } } void GxsForumThreadWidget::filterColumnChanged(int column) { filterItems(ui->filterLineEdit->text()); // save index Settings->setValueToGroup("ForumThreadWidget", "filterColumn", column); } void GxsForumThreadWidget::filterItems(const QString& text) { QStringList lst = text.split(" ",QtSkipEmptyParts) ; int filterColumn = ui->filterLineEdit->currentFilter(); uint32_t count; mThreadModel->setFilter(filterColumn,lst,count) ; // We do this in order to trigger a new filtering action in the proxy model. QSortFilterProxyModel_setFilterRegularExpression(mThreadProxyModel, QString(RsGxsForumModel::FilterString)) ; if(!lst.empty()) ui->threadTreeWidget->expandAll(); else { // currentIndex() not on the clicked message, so not this way // if (!mThreadId.isNull()) { // an_index = mThreadProxyModel->mapToSource(ui->threadTreeWidget->currentIndex()); // } ui->threadTreeWidget->collapseAll(); if (!mThreadId.isNull()) { // ...but this one QModelIndex an_index = mThreadModel->getIndexOfMessage(mThreadId); if (an_index.isValid()) { QModelIndex the_index = mThreadProxyModel->mapFromSource(an_index); ui->threadTreeWidget->setCurrentIndex(the_index); ui->threadTreeWidget->scrollTo(the_index); // don't change focus // ui->threadTreeWidget->setFocus(); } } } if(count > 0) ui->filterLineEdit->setToolTip(tr("No result.")) ; else ui->filterLineEdit->setToolTip(tr("Found %1 results.").arg(count)) ; } /*********************** **** **** **** ***********************/ /** Request / Response of Data ********************************/ /*********************** **** **** **** ***********************/ void GxsForumThreadWidget::postForumLoading() { if(groupId().isNull()) { ui->nextUnreadButton->setEnabled(false); return; } #ifdef DEBUG_FORUMS std::cerr << "Post forum loading..." << std::endl; #endif if (!mNavigatePendingMsgId.isNull()) navigate(mNavigatePendingMsgId); else if( (mLastSelectedPosts.count(groupId()) > 0) && !mLastSelectedPosts[groupId()].isNull() && mThreadModel->getIndexOfMessage(mLastSelectedPosts[groupId()]).isValid()) { #ifdef DEBUG_FORUMS std::cerr << "Last selected msg navigation: " << mLastSelectedPosts[groupId()].toStdString() << ". Using it as new thread Id" << std::endl; #endif QModelIndex source_index = mThreadModel->getIndexOfMessage(mLastSelectedPosts[groupId()]); QModelIndex index = mThreadProxyModel->mapFromSource(source_index); ui->threadTreeWidget->selectionModel()->setCurrentIndex(index,QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); ui->threadTreeWidget->scrollTo(ui->threadTreeWidget->currentIndex());//May change if model reloaded } else { QModelIndex source_index = mThreadModel->getIndexOfMessage(mThreadId); if(!mThreadId.isNull() && source_index.isValid()) { QModelIndex index = mThreadProxyModel->mapFromSource(source_index); ui->threadTreeWidget->selectionModel()->setCurrentIndex(index,QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); ui->threadTreeWidget->scrollTo(ui->threadTreeWidget->currentIndex());//May change if model reloaded #ifdef DEBUG_FORUMS std::cerr << " re-selecting index of message " << mThreadId << " to " << source_index.row() << "," << source_index.column() << " " << (void*)source_index.internalPointer() << std::endl; #endif } else { #ifdef DEBUG_FORUMS std::cerr << " previously message " << mThreadId << " not visible anymore -> de-selecting" << std::endl; #endif ui->threadTreeWidget->selectionModel()->clear(); ui->threadTreeWidget->selectionModel()->reset(); mThreadId.clear(); //blank(); } // we also need to restore expanded threads } ui->newthreadButton->show(); ui->forumName->setText(QString::fromUtf8(mForumGroup.mMeta.mGroupName.c_str())); ui->threadTreeWidget->sortByColumn(RsGxsForumModel::COLUMN_THREAD_DATE, Qt::DescendingOrder); ui->threadTreeWidget->update(); ui->threadedView_TB->setEnabled(true); ui->flatView_TB->setEnabled(true); ui->flatView_TB->setEnabled(true); ui->threadedView_TB->setEnabled(true); ui->latestPostInThreadView_TB->setEnabled(true); ui->filterLineEdit->setEnabled(true); recursRestoreExpandedItems(mThreadProxyModel->mapFromSource(mThreadModel->root()),mSavedExpandedMessages); //mUpdating = false; ui->nextUnreadButton->setEnabled(true); } void GxsForumThreadWidget::updateGroupData() { if(groupId().isNull()) return; // ui->threadTreeWidget->selectionModel()->clear(); // ui->threadTreeWidget->selectionModel()->reset(); // mThreadProxyModel->clear(); setForumDescriptionLoading(); RsThread::async([this]() { // 1 - get message data from p3GxsForums std::list forumIds; std::vector groups; forumIds.push_back(groupId()); bool success = false; if(!rsGxsForums->getForumsInfo(forumIds,groups)) std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve forum group info for forum " << groupId() << std::endl; else if(groups.size() != 1) std::cerr << __PRETTY_FUNCTION__ << " obtained more than one group info for forum " << groupId() << std::endl; else success = true; if(success) { // 2 - sort the messages into a proper hierarchy RsGxsForumGroup group(groups[0]); // we use a copy to share the object in order to avoid group deletion while we're in the thread. // 3 - update the model in the UI thread. RsQThreadUtils::postToObject( [group,this]() { /* Here it goes any code you want to be executed on the Qt Gui * thread, for example to update the data model with new information * after a blocking call to RetroShare API complete */ mForumGroup = group; mThreadId.clear(); ui->threadTreeWidget->setColumnHidden(RsGxsForumModel::COLUMN_THREAD_DISTRIBUTION, !IS_GROUP_PGP_KNOWN_AUTHED(mForumGroup.mMeta.mSignFlags) && !(IS_GROUP_PGP_AUTHED(mForumGroup.mMeta.mSignFlags))); ui->subscribeToolButton->setHidden(IS_GROUP_SUBSCRIBED(mForumGroup.mMeta.mSubscribeFlags)) ; updateForumDescription(true); emit groupChanged(this); // signals the parent widget to e.g. update the group tab name }, this ); } else RsQThreadUtils::postToObject( [this]() { updateForumDescription(false); },this); }); } void GxsForumThreadWidget::updateMessageData(const RsGxsMessageId& msgId) { RsThread::async([msgId,this]() { // 1 - get message data from p3GxsForums #ifdef DEBUG_FORUMS std::cerr << "Retrieving post data for post " << msgId << std::endl; #endif std::set msgs_to_request ; std::vector msgs; msgs_to_request.insert(msgId); QString error_string; if(!rsGxsForums->getForumContent(groupId(),msgs_to_request,msgs)) { std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve message info for forum " << groupId() << " and MsgId " << msgId << std::endl; error_string = tr("Failed to retrieve this message. Is the database currently overloaded?"); } if(msgs.empty()) { std::cerr << __PRETTY_FUNCTION__ << " no posts for msgId " << msgId << ". Database corruption?" << std::endl; error_string = tr("No data for this message. Is the database corrupted?"); } if(msgs.size() > 1) { std::cerr << __PRETTY_FUNCTION__ << " obtained more than one msg info for msgId " << msgId << ". This could be a bug. Only showing the first msg in the list." << std::endl; std::cerr << "Messages are:" << std::endl; for(auto it(msgs.begin());it!=msgs.end();++it) std::cerr << (*it).mMeta << std::endl; error_string = tr("More than one entry for this message. Is the database corrupted?"); } if(error_string.isNull()) { // 2 - sort the messages into a proper hierarchy RsGxsForumMsg msg(msgs[0]); // 3 - update the model in the UI thread. RsQThreadUtils::postToObject( [msg,this]() { /* Here it goes any code you want to be executed on the Qt Gui * thread, for example to update the data model with new information * after a blocking call to RetroShare API complete */ insertMessageData(msg); ui->threadTreeWidget->setColumnHidden(RsGxsForumModel::COLUMN_THREAD_DISTRIBUTION, !IS_GROUP_PGP_KNOWN_AUTHED(mForumGroup.mMeta.mSignFlags) && !(IS_GROUP_PGP_AUTHED(mForumGroup.mMeta.mSignFlags))); ui->subscribeToolButton->setHidden(IS_GROUP_SUBSCRIBED(mForumGroup.mMeta.mSubscribeFlags)) ; }, this ); } else RsQThreadUtils::postToObject( [error_string,this](){ setMessageLoadingError(error_string); } ); }); } void GxsForumThreadWidget::showAuthorInPeople(const RsGxsForumMsg& msg) { if(msg.mMeta.mAuthorId.isNull()) { std::cerr << "(EE) GxsForumThreadWidget::loadMsgData_showAuthorInPeople() ERROR Missing Message Data..."; std::cerr << std::endl; } /* window will destroy itself! */ IdDialog *idDialog = dynamic_cast(MainWindow::getPage(MainWindow::People)); if (!idDialog) return ; MainWindow::showWindow(MainWindow::People); idDialog->navigate(RsGxsId(msg.mMeta.mAuthorId)); }