diff --git a/retroshare-gui/src/gui/gxsforums/GxsForumThreadWidget.cpp b/retroshare-gui/src/gui/gxsforums/GxsForumThreadWidget.cpp index e7bddbe7a..602bf1b6c 100644 --- a/retroshare-gui/src/gui/gxsforums/GxsForumThreadWidget.cpp +++ b/retroshare-gui/src/gui/gxsforums/GxsForumThreadWidget.cpp @@ -1,2103 +1,2103 @@ -/******************************************************************************* - * 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/QtVersion.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 - - return left.data(RsGxsForumModel::SortRole) < right.data(RsGxsForumModel::SortRole) ; - } - - 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); - mThreadProxyModel->setFilterRegExp(QRegExp(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(index.child(row,0),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(" ",QString::SkipEmptyParts) ; - - 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. - mThreadProxyModel->setFilterRegExp(QRegExp(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)); -} +/******************************************************************************* + * 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/QtVersion.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 + + return left.data(RsGxsForumModel::SortRole) < right.data(RsGxsForumModel::SortRole) ; + } + + 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); + mThreadProxyModel->setFilterRegExp(QRegExp(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(index.child(row,0),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(" ",QString::SkipEmptyParts) ; + + 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. + mThreadProxyModel->setFilterRegExp(QRegExp(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)); +}