diff --git a/libretroshare/src/gxs/rsgxsnotify.h b/libretroshare/src/gxs/rsgxsnotify.h index a6fd4c6fb..71db32ebc 100644 --- a/libretroshare/src/gxs/rsgxsnotify.h +++ b/libretroshare/src/gxs/rsgxsnotify.h @@ -48,7 +48,7 @@ public: TYPE_UPDATED = 0x07, TYPE_MESSAGE_DELETED = 0x08, TYPE_GROUP_DELETED = 0x09, - }; + }; virtual NotifyType getType() = 0; diff --git a/libretroshare/src/retroshare/rsposted.h b/libretroshare/src/retroshare/rsposted.h index 4cd6e8cf4..3c7724f6c 100644 --- a/libretroshare/src/retroshare/rsposted.h +++ b/libretroshare/src/retroshare/rsposted.h @@ -115,6 +115,7 @@ enum class RsPostedEventCode: uint8_t UPDATED_MESSAGE = 0x05, READ_STATUS_CHANGED = 0x06, STATISTICS_CHANGED = 0x07, + MESSAGE_VOTES_UPDATED = 0x08, }; @@ -127,6 +128,7 @@ struct RsGxsPostedEvent: RsEvent RsPostedEventCode mPostedEventCode; RsGxsGroupId mPostedGroupId; RsGxsMessageId mPostedMsgId; + RsGxsMessageId mPostedThreadId; ///* @see RsEvent @see RsSerializable void serial_process( RsGenericSerializer::SerializeJob j,RsGenericSerializer::SerializeContext& ctx) override @@ -172,7 +174,11 @@ public: virtual bool getBoardsServiceStatistics(GxsServiceStatistic& stat) =0; - enum RS_DEPRECATED RankType {TopRankType, HotRankType, NewRankType }; + virtual bool voteForPost(bool up,const RsGxsGroupId& postGrpId,const RsGxsMessageId& postMsgId,const RsGxsId& voterId) =0; + + virtual bool setPostReadStatus(const RsGxsGrpMsgIdPair& msgId, bool read) = 0; + + enum RS_DEPRECATED RankType {TopRankType, HotRankType, NewRankType }; RS_DEPRECATED_FOR(getBoardsInfo) virtual bool getGroupData( const uint32_t& token, @@ -201,8 +207,9 @@ public: //virtual bool createNewComment(uint32_t &token, RsGxsComment &comment) = 0; //virtual bool createNewVote(uint32_t &token, RsGxsVote &vote) = 0; + RS_DEPRECATED_FOR(setPostReadStatus) + virtual void setMessageReadStatus(uint32_t& token, const RsGxsGrpMsgIdPair& msgId, bool read) = 0; ////////////////////////////////////////////////////////////////////////////// -virtual void setMessageReadStatus(uint32_t& token, const RsGxsGrpMsgIdPair& msgId, bool read) = 0; virtual bool createGroup(uint32_t &token, RsPostedGroup &group) = 0; virtual bool createPost(uint32_t &token, RsPostedPost &post) = 0; diff --git a/libretroshare/src/services/p3postbase.cc b/libretroshare/src/services/p3postbase.cc index c4c96f3b5..5c25ca52c 100644 --- a/libretroshare/src/services/p3postbase.cc +++ b/libretroshare/src/services/p3postbase.cc @@ -38,6 +38,7 @@ /**** * #define POSTBASE_DEBUG 1 ****/ +#define POSTBASE_DEBUG 1 #define POSTBASE_BACKGROUND_PROCESSING 0x0002 #define PROCESSING_START_PERIOD 30 @@ -88,101 +89,123 @@ void p3PostBase::notifyChanges(std::vector &changes) #endif for(auto it = changes.begin(); it != changes.end(); ++it) - { - RsGxsMsgChange *msgChange = dynamic_cast(*it); + { + RsGxsMsgChange *msgChange = dynamic_cast(*it); - if (msgChange) - { + if(msgChange) + { + // To start with we are just going to trigger updates on these groups. + // FUTURE OPTIMISATION. + // It could be taken a step further and directly request these msgs for an update. + addGroupForProcessing(msgChange->mGroupId); + + if (rsEvents) + { + switch(msgChange->getType()) + { + case RsGxsNotify::TYPE_RECEIVED_NEW: + case RsGxsNotify::TYPE_PUBLISHED: + { + auto ev = std::make_shared(); + ev->mPostedMsgId = msgChange->mMsgId; + ev->mPostedThreadId = msgChange->mNewMsgItem->meta.mThreadId; + ev->mPostedGroupId = msgChange->mGroupId; + ev->mPostedEventCode = RsPostedEventCode::NEW_MESSAGE; + rsEvents->postEvent(ev); #ifdef POSTBASE_DEBUG - std::cerr << "p3PostBase::notifyChanges() Found Message Change Notification"; - std::cerr << std::endl; + std::cerr << "p3PostBase::notifyChanges() Found Message Change Notification: NEW/PUBLISHED ID=" << msgChange->mMsgId << " in group " << msgChange->mGroupId << ", thread ID = " << msgChange->mNewMsgItem->meta.mThreadId << std::endl; #endif + } + break; + + case RsGxsNotify::TYPE_PROCESSED: + { + auto ev = std::make_shared(); + ev->mPostedMsgId = msgChange->mMsgId; + ev->mPostedGroupId = msgChange->mGroupId; + ev->mPostedEventCode = RsPostedEventCode::MESSAGE_VOTES_UPDATED; + rsEvents->postEvent(ev); #ifdef POSTBASE_DEBUG - std::cerr << "p3PostBase::notifyChanges() Msgs for Group: " << mit->first; - std::cerr << std::endl; + std::cerr << "p3PostBase::notifyChanges() Found Message Change Notification: PROCESSED ID=" << msgChange->mMsgId << " in group " << msgChange->mGroupId << std::endl; #endif - // To start with we are just going to trigger updates on these groups. - // FUTURE OPTIMISATION. - // It could be taken a step further and directly request these msgs for an update. - addGroupForProcessing(msgChange->mGroupId); - - if (rsEvents && (msgChange->getType() == RsGxsNotify::TYPE_RECEIVED_NEW || msgChange->getType() == RsGxsNotify::TYPE_PUBLISHED)) - { - auto ev = std::make_shared(); - ev->mPostedMsgId = msgChange->mMsgId; - ev->mPostedGroupId = msgChange->mGroupId; - ev->mPostedEventCode = RsPostedEventCode::NEW_MESSAGE; - rsEvents->postEvent(ev); - } - } - - RsGxsGroupChange *grpChange = dynamic_cast(*it); - - /* pass on Group Changes to GUI */ - if (grpChange && rsEvents) - { + } + break; + default: #ifdef POSTBASE_DEBUG - std::cerr << "p3PostBase::notifyChanges() Found Group Change Notification"; - std::cerr << std::endl; + std::cerr << "p3PostBase::notifyChanges() Found Message Change Notification: type " << msgChange->getType() << " (ignored) " << msgChange->mMsgId << std::endl; +#endif + break; + } + } + } + + RsGxsGroupChange *grpChange = dynamic_cast(*it); + + /* pass on Group Changes to GUI */ + if (grpChange && rsEvents) + { +#ifdef POSTBASE_DEBUG + std::cerr << "p3PostBase::notifyChanges() Found Group Change Notification"; + std::cerr << std::endl; #endif const RsGxsGroupId& group_id(grpChange->mGroupId); switch(grpChange->getType()) - { - case RsGxsNotify::TYPE_PROCESSED: // happens when the group is subscribed - { - auto ev = std::make_shared(); - ev->mPostedGroupId = group_id; - ev->mPostedEventCode = RsPostedEventCode::SUBSCRIBE_STATUS_CHANGED; - rsEvents->postEvent(ev); - } - break; + { + case RsGxsNotify::TYPE_PROCESSED: // happens when the group is subscribed + { + auto ev = std::make_shared(); + ev->mPostedGroupId = group_id; + ev->mPostedEventCode = RsPostedEventCode::SUBSCRIBE_STATUS_CHANGED; + rsEvents->postEvent(ev); + } + break; - case RsGxsNotify::TYPE_STATISTICS_CHANGED: - { - auto ev = std::make_shared(); - ev->mPostedGroupId = group_id; - ev->mPostedEventCode = RsPostedEventCode::STATISTICS_CHANGED; - rsEvents->postEvent(ev); - } - break; + case RsGxsNotify::TYPE_STATISTICS_CHANGED: + { + auto ev = std::make_shared(); + ev->mPostedGroupId = group_id; + ev->mPostedEventCode = RsPostedEventCode::STATISTICS_CHANGED; + rsEvents->postEvent(ev); + } + break; - case RsGxsNotify::TYPE_PUBLISHED: - case RsGxsNotify::TYPE_RECEIVED_NEW: - { - /* group received */ + case RsGxsNotify::TYPE_PUBLISHED: + case RsGxsNotify::TYPE_RECEIVED_NEW: + { + /* group received */ - if(mKnownPosted.find(group_id) == mKnownPosted.end()) - { - mKnownPosted.insert(std::make_pair(group_id, time(nullptr))); - IndicateConfigChanged(); + if(mKnownPosted.find(group_id) == mKnownPosted.end()) + { + mKnownPosted.insert(std::make_pair(group_id, time(nullptr))); + IndicateConfigChanged(); - auto ev = std::make_shared(); - ev->mPostedGroupId = group_id; - ev->mPostedEventCode = RsPostedEventCode::NEW_POSTED_GROUP; - rsEvents->postEvent(ev); + auto ev = std::make_shared(); + ev->mPostedGroupId = group_id; + ev->mPostedEventCode = RsPostedEventCode::NEW_POSTED_GROUP; + rsEvents->postEvent(ev); #ifdef POSTBASE_DEBUG - std::cerr << "p3PostBase::notifyChanges() Incoming Group: " << group_id; - std::cerr << std::endl; + std::cerr << "p3PostBase::notifyChanges() Incoming Group: " << group_id; + std::cerr << std::endl; #endif - } - else - RsInfo() << __PRETTY_FUNCTION__ - << " Not notifying already known forum " - << group_id << std::endl; - } - break; + } + else + RsInfo() << __PRETTY_FUNCTION__ + << " Not notifying already known forum " + << group_id << std::endl; + } + break; - default: - RsErr() << " Got a GXS event of type " << grpChange->getType() << " Currently not handled." << std::endl; - break; - } - } + default: + RsErr() << " Got a GXS event of type " << grpChange->getType() << " Currently not handled." << std::endl; + break; + } + } delete *it; - } + } } void p3PostBase::service_tick() diff --git a/libretroshare/src/services/p3posted.cc b/libretroshare/src/services/p3posted.cc index 88ff25808..251bf10cd 100644 --- a/libretroshare/src/services/p3posted.cc +++ b/libretroshare/src/services/p3posted.cc @@ -442,6 +442,57 @@ bool p3Posted::createBoard(RsPostedGroup& board) return true; } +bool p3Posted::voteForPost(bool up,const RsGxsGroupId& postGrpId,const RsGxsMessageId& postMsgId,const RsGxsId& authorId) +{ + // Do some basic tests + + if(!rsIdentity->isOwnId(authorId)) // This is ruled out before waitToken complains. Not sure it's needed. + { + std::cerr << __PRETTY_FUNCTION__ << ": vote submitted with an ID that is not yours! This cannot work." << std::endl; + return false; + } + + RsGxsVote vote; + + vote.mMeta.mGroupId = postGrpId; + vote.mMeta.mThreadId = postMsgId; + vote.mMeta.mParentId = postMsgId; + vote.mMeta.mAuthorId = authorId; + + if (up) + vote.mVoteType = GXS_VOTE_UP; + else + vote.mVoteType = GXS_VOTE_DOWN; + + uint32_t token; + + if(!createNewVote(token, vote)) + { + std::cerr << __PRETTY_FUNCTION__ << " Error! Failed submitting vote to (group,msg) " << postGrpId << "," << postMsgId << " from author " << authorId << std::endl; + return false; + } + + if(waitToken(token) != RsTokenService::COMPLETE) + { + std::cerr << __PRETTY_FUNCTION__ << " Error! GXS operation failed." << std::endl; + return false; + } + return true; +} + +bool p3Posted::setPostReadStatus(const RsGxsGrpMsgIdPair& msgId, bool read) +{ + uint32_t token; + + setMessageReadStatus(token,msgId,read); + + if(waitToken(token) != RsTokenService::COMPLETE) + { + std::cerr << __PRETTY_FUNCTION__ << " Error! GXS operation failed." << std::endl; + return false; + } + return true; +} bool p3Posted::editBoard(RsPostedGroup& board) { uint32_t token; diff --git a/libretroshare/src/services/p3posted.h b/libretroshare/src/services/p3posted.h index bc213ac0d..8f71a97a1 100644 --- a/libretroshare/src/services/p3posted.h +++ b/libretroshare/src/services/p3posted.h @@ -84,7 +84,11 @@ virtual void receiveHelperChanges(std::vector& changes) bool createBoard(RsPostedGroup& board) override; - virtual bool getGroupData(const uint32_t &token, std::vector &groups) override; + bool voteForPost(bool up,const RsGxsGroupId& postGrpId,const RsGxsMessageId& postMsgId,const RsGxsId& voterId) override; + + bool setPostReadStatus(const RsGxsGrpMsgIdPair& msgId, bool read) override; + + virtual bool getGroupData(const uint32_t &token, std::vector &groups) override; virtual bool getPostData(const uint32_t &token, std::vector &posts, std::vector &cmts, std::vector &vots) override; virtual bool getPostData(const uint32_t &token, std::vector &posts, std::vector &cmts) override; virtual bool getPostData(const uint32_t &token, std::vector &posts) override; @@ -126,17 +130,17 @@ virtual void setMessageReadStatus(uint32_t& token, const RsGxsGrpMsgIdPair& msgI return mCommentService->createGxsComment(token, msg) && waitToken(token) == RsTokenService::COMPLETE ; } - virtual bool createNewVote(uint32_t &token, RsGxsVote &msg) + virtual bool createNewVote(uint32_t &token, RsGxsVote &msg) override { return mCommentService->createGxsVote(token, msg); } virtual bool acknowledgeComment( - uint32_t token, std::pair& msgId ) + uint32_t token, std::pair& msgId ) override { return acknowledgeMsg(token, msgId); } virtual bool acknowledgeVote( - uint32_t token, std::pair& msgId ) + uint32_t token, std::pair& msgId ) override { if (mCommentService->acknowledgeVote(token, msgId)) return true; return acknowledgeMsg(token, msgId); diff --git a/retroshare-gui/src/gui/Identity/IdDialog.ui b/retroshare-gui/src/gui/Identity/IdDialog.ui index 127f7a85a..d785dbc4c 100644 --- a/retroshare-gui/src/gui/Identity/IdDialog.ui +++ b/retroshare-gui/src/gui/Identity/IdDialog.ui @@ -268,7 +268,7 @@ - 1 + 0 @@ -289,8 +289,8 @@ 0 0 - 1372 - 719 + 634 + 523 @@ -1031,7 +1031,7 @@ border-image: url(:/images/closepressed.png) - + :/images/edit_16.png:/images/edit_16.png diff --git a/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.cpp b/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.cpp new file mode 100644 index 000000000..40e50685e --- /dev/null +++ b/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.cpp @@ -0,0 +1,511 @@ +/******************************************************************************* + * retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.cpp * + * * + * Copyright (C) 2019 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 "rshare.h" +#include "BoardPostDisplayWidget.h" +#include "PhotoView.h" +#include "gui/gxs/GxsIdDetails.h" +#include "util/misc.h" +#include "gui/common/FilesDefs.h" +#include "util/qtthreadsutils.h" +#include "util/HandleRichText.h" +#include "gui/Identity/IdDialog.h" +#include "gui/MainWindow.h" + +#include "ui_BoardPostDisplayWidget_compact.h" +#include "ui_BoardPostDisplayWidget_card.h" + +#include +#include + +#define LINK_IMAGE ":/images/thumb-link.png" + +// #ifdef DEBUG_BOARDPOSTDISPLAYWIDGET 1 + +/** Constructor */ + +const char *BoardPostDisplayWidget_compact::DEFAULT_BOARD_IMAGE = ":/icons/png/newsfeed2.png"; + +//=================================================================================================================================== +//== Base class BoardPostDisplayWidgetBase == +//=================================================================================================================================== + +BoardPostDisplayWidgetBase::BoardPostDisplayWidgetBase(const RsPostedPost& post,uint8_t display_flags,QWidget *parent) + : QWidget(parent), mPost(post),mDisplayFlags(display_flags) +{ +} + +void BoardPostDisplayWidgetBase::setCommentsSize(int comNb) +{ + QString sComButText ; + + if (comNb == 1) + sComButText = tr("1 comment"); + else if(comNb > 1) + sComButText = tr("%1 comments").arg(comNb); + else + sComButText = tr("No comments yet. Click to add one."); + + commentButton()->setToolTip(sComButText); + + if(comNb > 0) + commentButton()->setIcon(FilesDefs::getIconFromQtResourcePath(":/images/comments_blue.png")); + else + commentButton()->setIcon(FilesDefs::getIconFromQtResourcePath(":/images/comments.png")); + +// QString sComButText = tr("Comment"); +// if (comNb == 1) +// sComButText = sComButText.append("(1)"); +// else if(comNb > 1) +// sComButText = tr("Comments ").append("(%1)").arg(comNb); +// + commentButton()->setText(tr("Comments")); +} + +void BoardPostDisplayWidgetBase::makeDownVote() +{ + RsGxsGrpMsgIdPair msgId; + msgId.first = mPost.mMeta.mGroupId; + msgId.second = mPost.mMeta.mMsgId; + + voteUpButton()->setEnabled(false); + voteDownButton()->setEnabled(false); + + emit vote(msgId, false); +} + +void BoardPostDisplayWidgetBase::makeUpVote() +{ + RsGxsGrpMsgIdPair msgId; + msgId.first = mPost.mMeta.mGroupId; + msgId.second = mPost.mMeta.mMsgId; + + voteUpButton()->setEnabled(false); + voteDownButton()->setEnabled(false); + + emit vote(msgId, true); +} + +void BoardPostDisplayWidgetBase::setReadStatus(bool isNew, bool isUnread) +{ + if (isUnread) + readButton()->setIcon(FilesDefs::getIconFromQtResourcePath(":/images/message-state-unread.png")); + else + readButton()->setIcon(FilesDefs::getIconFromQtResourcePath(":/images/message-state-read.png")); + + newLabel()->setVisible(isNew); + + mainFrame()->setProperty("new", isNew); + mainFrame()->style()->unpolish(mainFrame()); + mainFrame()->style()->polish(mainFrame()); +} + +void BoardPostDisplayWidget_compact::doExpand(bool e) +{ +#ifdef DEBUG_BOARDPOSTDISPLAYWIDGET + std::cerr << "Expanding" << std::endl; +#endif + if(e) + ui->frame_notes->show(); + else + ui->frame_notes->hide(); + + emit expand(mPost.mMeta.mMsgId,e); +} + +void BoardPostDisplayWidgetBase::loadComments(bool e) +{ + emit commentsRequested(mPost.mMeta.mMsgId,e); +} + +void BoardPostDisplayWidgetBase::readToggled() +{ + bool s = IS_MSG_UNREAD(mPost.mMeta.mMsgStatus); + + emit changeReadStatusRequested(mPost.mMeta.mMsgId,s); +} + +void BoardPostDisplayWidgetBase::setup() +{ + // show/hide things based on the view type + + if(!(mDisplayFlags & SHOW_COMMENTS)) + commentButton()->setChecked(false); + else + commentButton()->setChecked(true); + + /* clear ui */ + titleLabel()->setText(tr("Loading")); + dateLabel()->clear(); + fromLabel()->clear(); + siteLabel()->clear(); + + QObject::connect(commentButton(), SIGNAL(toggled(bool)), this, SLOT(loadComments(bool))); + QObject::connect(voteUpButton(), SIGNAL(clicked()), this, SLOT(makeUpVote())); + QObject::connect(voteDownButton(), SIGNAL(clicked()), this, SLOT(makeDownVote())); + QObject::connect(readButton(), SIGNAL(clicked()), this, SLOT(readToggled())); + + QAction *CopyLinkAction = new QAction(QIcon(""),tr("Copy RetroShare Link"), this); + connect(CopyLinkAction, SIGNAL(triggered()), this, SLOT(handleCopyLinkClicked())); + + int S = QFontMetricsF(font()).height() ; + + readButton()->setChecked(false); + + QMenu *menu = new QMenu(); + menu->addAction(CopyLinkAction); + menu->addSeparator(); + shareButton()->setMenu(menu); + + connect(shareButton(),SIGNAL(pressed()),this,SLOT(handleShareButtonClicked())); + + RsReputationLevel overall_reputation = rsReputations->overallReputationLevel(mPost.mMeta.mAuthorId); + bool redacted = (overall_reputation == RsReputationLevel::LOCALLY_NEGATIVE); + + if(redacted) + { + commentButton()->setDisabled(true); + voteUpButton()->setDisabled(true); + voteDownButton()->setDisabled(true); + fromLabel()->setId(mPost.mMeta.mAuthorId); + titleLabel()->setText(tr( "

The author of this message (with ID %1) is banned.").arg(QString::fromStdString(mPost.mMeta.mAuthorId.toStdString()))) ; + QDateTime qtime; + qtime.setTime_t(mPost.mMeta.mPublishTs); + QString timestamp = qtime.toString("hh:mm dd-MMM-yyyy"); + dateLabel()->setText(timestamp); + pictureLabel()->setDisabled(true); + } + else + { + QPixmap sqpixmap2 = FilesDefs::getPixmapFromQtResourcePath(":/images/thumb-default.png"); + + QDateTime qtime; + qtime.setTime_t(mPost.mMeta.mPublishTs); + QString timestamp = qtime.toString("hh:mm dd-MMM-yyyy"); + QString timestamp2 = misc::timeRelativeToNow(mPost.mMeta.mPublishTs) + " " + tr("ago"); + dateLabel()->setText(timestamp2); + dateLabel()->setToolTip(timestamp); + + fromLabel()->setId(mPost.mMeta.mAuthorId); + + // Use QUrl to check/parse our URL + // The only combination that seems to work: load as EncodedUrl, extract toEncoded(). + QByteArray urlarray(mPost.mLink.c_str()); + QUrl url = QUrl::fromEncoded(urlarray.trimmed()); + QString urlstr = "Invalid Link"; + QString sitestr = "Invalid Link"; + + bool urlOkay = url.isValid(); + if (urlOkay) + { + QString scheme = url.scheme(); + if ((scheme != "https") + && (scheme != "http") + && (scheme != "ftp") + && (scheme != "retroshare")) + { + urlOkay = false; + sitestr = "Invalid Link Scheme"; + } + } + + if (urlOkay) + { + urlstr = QString(" "); + urlstr += QString::fromUtf8(mPost.mMeta.mMsgName.c_str()); + urlstr += QString(" "); + + QString siteurl = url.toEncoded(); + sitestr = QString(" %2 ").arg(siteurl).arg(siteurl); + + titleLabel()->setText(urlstr); + } + else + titleLabel()->setText( QString::fromUtf8(mPost.mMeta.mMsgName.c_str()) ); + + if (urlarray.isEmpty()) + siteLabel()->hide(); + + siteLabel()->setText(sitestr); + } + + //QString score = "Hot" + QString::number(post.mHotScore); + //score += " Top" + QString::number(post.mTopScore); + //score += " New" + QString::number(post.mNewScore); + + QString score = QString::number(mPost.mTopScore); + + scoreLabel()->setText(score); + + // FIX THIS UP LATER. + notes()->setText(RsHtml().formatText(NULL, QString::fromUtf8(mPost.mNotes.c_str()), RSHTML_FORMATTEXT_EMBED_SMILEYS | RSHTML_FORMATTEXT_EMBED_LINKS)); + pictureLabel()->setText(RsHtml().formatText(NULL, QString::fromUtf8(mPost.mNotes.c_str()), RSHTML_FORMATTEXT_EMBED_SMILEYS | RSHTML_FORMATTEXT_EMBED_LINKS)); + + // feed. + //frame_comment->show(); + commentButton()->show(); + + setCommentsSize(mPost.mComments); + + setReadStatus(IS_MSG_NEW(mPost.mMeta.mMsgStatus), IS_MSG_UNREAD(mPost.mMeta.mMsgStatus) || IS_MSG_NEW(mPost.mMeta.mMsgStatus)); + + // disable voting buttons - if they have already voted. + if (mPost.mMeta.mMsgStatus & GXS_SERV::GXS_MSG_STATUS_VOTE_MASK) + { + voteUpButton()->setEnabled(false); + voteDownButton()->setEnabled(false); + } + + connect(pictureLabel(), SIGNAL(clicked()), this, SLOT(viewPicture())); + +#ifdef TODO + emit sizeChanged(this); +#endif +} +void BoardPostDisplayWidgetBase::handleShareButtonClicked() +{ + emit shareButtonClicked(); +} + +void BoardPostDisplayWidgetBase::handleCopyLinkClicked() +{ + emit copylinkClicked(); +} +//=================================================================================================================================== +//== class BoardPostDisplayWidget == +//=================================================================================================================================== + +BoardPostDisplayWidget_compact::BoardPostDisplayWidget_compact(const RsPostedPost& post, uint8_t display_flags,QWidget *parent=nullptr) + : BoardPostDisplayWidgetBase(post,display_flags,parent), ui(new Ui::BoardPostDisplayWidget_compact()) +{ + ui->setupUi(this); + setup(); + + ui->verticalLayout->addStretch(); + ui->verticalLayout->setAlignment(Qt::AlignTop); + ui->topLayout->setAlignment(Qt::AlignTop); + ui->arrowsLayout->addStretch(); + ui->arrowsLayout->setAlignment(Qt::AlignTop); + ui->verticalLayout_2->addStretch(); + + adjustSize(); +} + +BoardPostDisplayWidget_compact::~BoardPostDisplayWidget_compact() +{ + delete ui; +} + +void BoardPostDisplayWidget_compact::setup() +{ + BoardPostDisplayWidgetBase::setup(); + + // show/hide things based on the view type + + setAttribute(Qt::WA_DeleteOnClose, true); + ui->pictureLabel->setEnableZoom(false); + + RsReputationLevel overall_reputation = rsReputations->overallReputationLevel(mPost.mMeta.mAuthorId); + bool redacted = (overall_reputation == RsReputationLevel::LOCALLY_NEGATIVE); + + int desired_height = QFontMetricsF(font()).height() * 5; + ui->pictureLabel->setFixedSize(16/9.0*desired_height,desired_height); + + if(redacted) + { + ui->pictureLabel->setPicture( FilesDefs::getPixmapFromQtResourcePath(":/images/thumb-blocked.png") ); + } + else + { + if(mPost.mImage.mData != NULL) + { + QPixmap pixmap; + GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); + // Wiping data - as its been passed to thumbnail. + +#ifdef DEBUG_BOARDPOSTDISPLAYWIDGET + std::cerr << "Got pixmap of size " << pixmap.width() << " x " << pixmap.height() << std::endl; + std::cerr << "Saving to pix.png" << std::endl; + pixmap.save("pix.png","PNG"); +#endif + + ui->pictureLabel->setPicture(pixmap); + } + else + ui->pictureLabel->setPicture( FilesDefs::getPixmapFromQtResourcePath(":/images/thumb-default.png") ); + } + + ui->notes->setText(RsHtml().formatText(NULL, QString::fromUtf8(mPost.mNotes.c_str()), RSHTML_FORMATTEXT_EMBED_SMILEYS | RSHTML_FORMATTEXT_EMBED_LINKS)); + + QObject::connect(ui->expandButton, SIGNAL(toggled(bool)), this, SLOT(doExpand(bool))); + + QTextDocument doc; + doc.setHtml(notes()->text()); + + if(mDisplayFlags & SHOW_NOTES) + { + ui->frame_notes->show(); + ui->expandButton->setChecked(true); + } + else + { + ui->frame_notes->hide(); + ui->expandButton->setChecked(false); + } + + if(doc.toPlainText().trimmed().isEmpty()) + { + ui->frame_notes->hide(); + ui->expandButton->hide(); + } + updateGeometry(); + +#ifdef TODO + emit sizeChanged(this); +#endif +} + +void BoardPostDisplayWidget_compact::viewPicture() +{ + if(mPost.mImage.mData == NULL) + return; + + QString timestamp = misc::timeRelativeToNow(mPost.mMeta.mPublishTs); + QPixmap pixmap; + GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); + RsGxsId authorID = mPost.mMeta.mAuthorId; + + PhotoView *PView = new PhotoView(); + + PView->setPixmap(pixmap); + PView->setTitle(QString::fromUtf8(mPost.mMeta.mMsgName.c_str())); + PView->setName(authorID); + PView->setTime(timestamp); + PView->setGroupId(mPost.mMeta.mGroupId); + PView->setMessageId(mPost.mMeta.mMsgId); + + PView->show(); + + emit thumbnailOpenned(); +} + +QToolButton *BoardPostDisplayWidget_compact::voteUpButton() { return ui->voteUpButton; } +QToolButton *BoardPostDisplayWidget_compact::commentButton() { return ui->commentButton; } +QToolButton *BoardPostDisplayWidget_compact::voteDownButton() { return ui->voteDownButton; } +QLabel *BoardPostDisplayWidget_compact::newLabel() { return ui->newLabel; } +QToolButton *BoardPostDisplayWidget_compact::readButton() { return ui->readButton; } +QLabel *BoardPostDisplayWidget_compact::siteLabel() { return ui->siteLabel; } +GxsIdLabel *BoardPostDisplayWidget_compact::fromLabel() { return ui->fromLabel; } +QLabel *BoardPostDisplayWidget_compact::dateLabel() { return ui->dateLabel; } +QLabel *BoardPostDisplayWidget_compact::titleLabel() { return ui->titleLabel; } +QLabel *BoardPostDisplayWidget_compact::scoreLabel() { return ui->scoreLabel; } +QLabel *BoardPostDisplayWidget_compact::notes() { return ui->notes; } +QPushButton *BoardPostDisplayWidget_compact::shareButton() { return ui->shareButton; } +QLabel *BoardPostDisplayWidget_compact::pictureLabel() { return ui->pictureLabel; } +QFrame *BoardPostDisplayWidget_compact::mainFrame() { return ui->mainFrame; } + +//=================================================================================================================================== +//== class BoardPostDisplayWidget_card == +//=================================================================================================================================== + +BoardPostDisplayWidget_card::BoardPostDisplayWidget_card(const RsPostedPost& post, uint8_t display_flags, QWidget *parent) + : BoardPostDisplayWidgetBase(post,display_flags,parent), ui(new Ui::BoardPostDisplayWidget_card()) +{ + ui->setupUi(this); + setup(); + + ui->verticalLayout->addStretch(); + ui->verticalLayout->setAlignment(Qt::AlignTop); + ui->topLayout->setAlignment(Qt::AlignTop); + ui->arrowsLayout->addStretch(); + ui->arrowsLayout->setAlignment(Qt::AlignTop); + ui->verticalLayout_2->addStretch(); + + adjustSize(); +} + +BoardPostDisplayWidget_card::~BoardPostDisplayWidget_card() +{ + delete ui; +} + +void BoardPostDisplayWidget_card::setup() +{ + BoardPostDisplayWidgetBase::setup(); + + RsReputationLevel overall_reputation = rsReputations->overallReputationLevel(mPost.mMeta.mAuthorId); + bool redacted = (overall_reputation == RsReputationLevel::LOCALLY_NEGATIVE); + + if(redacted) + { + ui->pictureLabel->setPixmap( FilesDefs::getPixmapFromQtResourcePath(":/images/thumb-blocked.png") ); + } + else + { + if(mPost.mImage.mData != NULL) + { + QPixmap pixmap; + GxsIdDetails::loadPixmapFromData(mPost.mImage.mData, mPost.mImage.mSize, pixmap,GxsIdDetails::ORIGINAL); + // Wiping data - as its been passed to thumbnail. + + QPixmap scaledpixmap; + if(pixmap.width() > 800){ + QPixmap scaledpixmap = pixmap.scaledToWidth(800, Qt::SmoothTransformation); + ui->pictureLabel->setPixmap(scaledpixmap); + }else{ + ui->pictureLabel->setPixmap(pixmap); + } + + ui->pictureLabel->show(); + } + else + ui->pictureLabel->hide(); + } + + QTextDocument doc; + doc.setHtml(notes()->text()); + + if(doc.toPlainText().trimmed().isEmpty()) + notes()->hide(); +} + +QToolButton *BoardPostDisplayWidget_card::voteUpButton() { return ui->voteUpButton; } +QToolButton *BoardPostDisplayWidget_card::commentButton() { return ui->commentButton; } +QToolButton *BoardPostDisplayWidget_card::voteDownButton() { return ui->voteDownButton; } +QLabel *BoardPostDisplayWidget_card::newLabel() { return ui->newLabel; } +QToolButton *BoardPostDisplayWidget_card::readButton() { return ui->readButton; } +QLabel *BoardPostDisplayWidget_card::siteLabel() { return ui->siteLabel; } +GxsIdLabel *BoardPostDisplayWidget_card::fromLabel() { return ui->fromLabel; } +QLabel *BoardPostDisplayWidget_card::dateLabel() { return ui->dateLabel; } +QLabel *BoardPostDisplayWidget_card::titleLabel() { return ui->titleLabel; } +QLabel *BoardPostDisplayWidget_card::scoreLabel() { return ui->scoreLabel; } +QLabel *BoardPostDisplayWidget_card::notes() { return ui->notes; } +QPushButton *BoardPostDisplayWidget_card::shareButton() { return ui->shareButton; } +QLabel *BoardPostDisplayWidget_card::pictureLabel() { return ui->pictureLabel; } +QFrame *BoardPostDisplayWidget_card::mainFrame() { return ui->mainFrame; } + diff --git a/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.h b/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.h new file mode 100644 index 000000000..30720d692 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.h @@ -0,0 +1,183 @@ +/******************************************************************************* + * retroshare-gui/src/gui/Posted/BoardPostDisplayWidget.h * + * * + * Copyright (C) 2019 by 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 . * + * * + *******************************************************************************/ + +#pragma once + +#include +#include + +#include + +namespace Ui { +class BoardPostDisplayWidget_card; +class BoardPostDisplayWidget_compact; +} + +class QPushButton; +class QFrame; +class QLabel; +class QToolButton; +class QTextEdit; +class ClickableLabel; +class GxsIdLabel; + +struct RsPostedPost; + +class BoardPostDisplayWidgetBase: public QWidget +{ + Q_OBJECT + +public: + enum DisplayFlags: uint8_t { + SHOW_NONE = 0x00, + SHOW_COMMENTS = 0x01, + SHOW_NOTES = 0x02, + }; + + enum DisplayMode: uint8_t { + DISPLAY_MODE_UNKNOWN = 0x00, + DISPLAY_MODE_COMPACT = 0x01, + DISPLAY_MODE_CARD = 0x02, + }; + + BoardPostDisplayWidgetBase(const RsPostedPost& post,uint8_t display_flags,QWidget *parent); + virtual ~BoardPostDisplayWidgetBase() {} + + static const char *DEFAULT_BOARD_IMAGE; + +protected slots: + /* GxsGroupFeedItem */ + + virtual void setup(); // to be overloaded by the different views + + virtual QToolButton *voteUpButton() =0; + virtual QToolButton *commentButton() =0; + virtual QToolButton *voteDownButton() =0; + virtual QLabel *newLabel() =0; + virtual QLabel *titleLabel()=0; + virtual QLabel *siteLabel()=0; + virtual GxsIdLabel *fromLabel()=0; + virtual QLabel *dateLabel()=0; + virtual QLabel *scoreLabel() =0; + virtual QLabel *notes() =0; + virtual QLabel *pictureLabel()=0; + virtual QToolButton *readButton() =0; + virtual QPushButton *shareButton() =0; + virtual QFrame *mainFrame() =0; + + void loadComments(bool e); + void readToggled(); + void setReadStatus(bool isNew, bool isUnread) ; + void makeUpVote() ; + void makeDownVote() ; + void setCommentsSize(int comNb) ; + void handleShareButtonClicked() ; + void handleCopyLinkClicked() ; + + +signals: + void changeReadStatusRequested(const RsGxsMessageId&,bool); + void vote(const RsGxsGrpMsgIdPair& msgId, bool up_or_down); + void expand(RsGxsMessageId,bool); + void commentsRequested(const RsGxsMessageId&,bool); + void thumbnailOpenned(); + void shareButtonClicked(); + void copylinkClicked(); + +protected: + RsPostedPost mPost; + uint8_t mDisplayFlags; +}; + +class BoardPostDisplayWidget_compact : public BoardPostDisplayWidgetBase +{ + Q_OBJECT + +public: + BoardPostDisplayWidget_compact(const RsPostedPost& post, uint8_t display_flags, QWidget *parent); + virtual ~BoardPostDisplayWidget_compact(); + + static const char *DEFAULT_BOARD_IMAGE; + + QToolButton *voteUpButton() override; + QToolButton *commentButton() override; + QToolButton *voteDownButton() override; + QLabel *newLabel() override; + QLabel *siteLabel() override; + GxsIdLabel *fromLabel() override; + QLabel *dateLabel() override; + QLabel *titleLabel() override; + QLabel *scoreLabel() override; + QLabel *notes() override; + QLabel *pictureLabel() override; + QToolButton *readButton() override; + QPushButton *shareButton() override; + QFrame *mainFrame() override; + +public slots: + void viewPicture() ; + +protected slots: + /* GxsGroupFeedItem */ + void doExpand(bool) ; + +protected: + void setup() override; // to be overloaded by the different views + +private: + /** Qt Designer generated object */ + Ui::BoardPostDisplayWidget_compact *ui; +}; + +class BoardPostDisplayWidget_card : public BoardPostDisplayWidgetBase +{ + Q_OBJECT + +public: + BoardPostDisplayWidget_card(const RsPostedPost& post,uint8_t display_flags,QWidget *parent=nullptr); + virtual ~BoardPostDisplayWidget_card(); + + static const char *DEFAULT_BOARD_IMAGE; + + QToolButton *voteUpButton() override; + QToolButton *commentButton() override; + QToolButton *voteDownButton() override; + QLabel *newLabel() override; + QLabel *siteLabel() override; + GxsIdLabel *fromLabel() override; + QLabel *dateLabel() override; + QLabel *titleLabel() override; + QLabel *scoreLabel() override; + QLabel *notes() override; + QToolButton *readButton() override; + QPushButton *shareButton() override; + QLabel *pictureLabel() override; + QFrame *mainFrame() override; + +protected slots: + /* GxsGroupFeedItem */ + +protected: + void setup() override; // to be overloaded by the different views + +private: + /** Qt Designer generated object */ + Ui::BoardPostDisplayWidget_card *ui; +}; diff --git a/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget_card.ui b/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget_card.ui new file mode 100644 index 000000000..11c5dd748 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget_card.ui @@ -0,0 +1,463 @@ + + + BoardPostDisplayWidget_card + + + + 0 + 0 + 558 + 202 + + + + + 0 + 0 + + + + + + + + + + + 0 + + + QLayout::SetMaximumSize + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 2 + + + QLayout::SetMaximumSize + + + 1 + + + 2 + + + 1 + + + 2 + + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Vote up + + + + + + + :/images/up-arrow.png:/images/up-arrow.png + + + true + + + + + + + + 9 + + + + 0 + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Vote down + + + + + + \/ + + + + :/images/down-arrow.png:/images/down-arrow.png + + + true + + + + + + + + + + + + 5 + + + 0 + + + 6 + + + + + + 0 + 0 + + + + + 50 + false + + + + Posted by + + + + + + + + 0 + 0 + + + + Signed by + + + true + + + + + + + + 0 + 0 + + + + You eyes only + + + true + + + + + + + + 24 + 16777215 + + + + Qt::NoFocus + + + Toggle Message Read Status + + + + :/images/message-state-unread.png:/images/message-state-unread.png + + + false + + + false + + + true + + + + + + + New + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 70 + 20 + + + + + + + + + + + 0 + 0 + + + + + Arial + 10 + 75 + true + + + + This is a very very very very loooooooooooooooonnnnnnnnnnnnnnnnng title don't you think? Yes it is and should wrap around I hope + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + site + + + true + + + + + + + + + PictureLabel + + + true + + + + + + + Qt::Horizontal + + + + 268 + 17 + + + + + + + + + + TextLabel + + + true + + + true + + + + + + + + + Comments + + + + :/images/comments.png:/images/comments.png + + + true + + + Qt::ToolButtonTextBesideIcon + + + true + + + + + + + Share + + + + :/images/share.png:/images/share.png + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + GxsIdLabel + QLabel +

gui/gxs/GxsIdLabel.h
+ + + StyledLabel + QLabel +
gui/common/StyledLabel.h
+
+ + + + + + + diff --git a/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget_compact.ui b/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget_compact.ui new file mode 100644 index 000000000..21fa7d87c --- /dev/null +++ b/retroshare-gui/src/gui/Posted/BoardPostDisplayWidget_compact.ui @@ -0,0 +1,511 @@ + + + BoardPostDisplayWidget_compact + + + + 0 + 0 + 540 + 172 + + + + + 0 + 0 + + + + + + + + + + + 0 + + + QLayout::SetMaximumSize + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 2 + + + QLayout::SetMaximumSize + + + 1 + + + 2 + + + 1 + + + 2 + + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + + 0 + 0 + + + + Vote up + + + + + + + :/images/up-arrow.png:/images/up-arrow.png + + + true + + + + + + + + 9 + + + + 0 + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Vote down + + + + + + \/ + + + + :/images/down-arrow.png:/images/down-arrow.png + + + true + + + + + + + + + + 0 + 0 + + + + Click to view picture + + + PictureLabel_compact + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + 3 + + + + + + 0 + 0 + + + + + Arial + 10 + 75 + true + + + + This is a very very very very loooooooooooooooonnnnnnnnnnnnnnnnng title don't you think? Yes it is and should wrap around I hope + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 6 + + + 2 + + + 2 + + + 6 + + + + + + 0 + 0 + + + + + 50 + false + + + + Posted by + + + + + + + + 0 + 0 + + + + Signed by + + + true + + + + + + + + 0 + 0 + + + + You eyes only + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 70 + 20 + + + + + + + + + + + 0 + 0 + + + + site + + + true + + + + + + + + + Comments + + + + :/images/comments.png:/images/comments.png + + + true + + + Qt::ToolButtonTextBesideIcon + + + true + + + + + + + Expand + + + + + + + :/images/expand.png:/images/expand.png + + + true + + + true + + + + + + + Share + + + + :/images/share.png:/images/share.png + + + true + + + + + + + + 24 + 16777215 + + + + Qt::NoFocus + + + Toggle Message Read Status + + + + :/images/message-state-unread.png:/images/message-state-unread.png + + + false + + + false + + + true + + + + + + + New + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 2 + + + QLayout::SetMaximumSize + + + 1 + + + 2 + + + 1 + + + 2 + + + + + + 0 + 0 + + + + TextLabel + + + true + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + GxsIdLabel + QLabel +
gui/gxs/GxsIdLabel.h
+
+ + StyledLabel + QLabel +
gui/common/StyledLabel.h
+
+ + ZoomableLabel + QLabel +
gui/gxschannels/GxsChannelPostThumbnail.h
+
+
+ + + + + +
diff --git a/retroshare-gui/src/gui/Posted/PostedCardView.h b/retroshare-gui/src/gui/Posted/PostedCardView.h index 79c9c3fdb..0f4f5f2ab 100644 --- a/retroshare-gui/src/gui/Posted/PostedCardView.h +++ b/retroshare-gui/src/gui/Posted/PostedCardView.h @@ -31,7 +31,7 @@ class PostedCardView; } class FeedHolder; -class RsPostedPost; +struct RsPostedPost; class PostedCardView : public BasePostedItem { @@ -47,7 +47,7 @@ protected: void setup() override; void fill() override; - void doExpand(bool open) override {} + void doExpand(bool) override {} void setComment(const RsGxsComment&) override; void setReadStatus(bool isNew, bool isUnread) override; void toggle() override {} diff --git a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp index a9edea275..cec0f67c6 100644 --- a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp +++ b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp @@ -45,7 +45,7 @@ #define VIEW_IMAGE 2 #define VIEW_LINK 3 -PostedCreatePostDialog::PostedCreatePostDialog(RsPosted *posted, const RsGxsGroupId& grpId, QWidget *parent): +PostedCreatePostDialog::PostedCreatePostDialog(RsPosted *posted, const RsGxsGroupId& grpId, const RsGxsId& default_author, QWidget *parent): QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint), mPosted(posted), mGrpId(grpId), ui(new Ui::PostedCreatePostDialog) @@ -70,8 +70,8 @@ PostedCreatePostDialog::PostedCreatePostDialog(RsPosted *posted, const RsGxsGrou ui->sizeWarningLabel->setText(QString("Post size is limited to %1 KB, pictures will be downscaled.").arg(MAXMESSAGESIZE / 1024)); /* fill in the available OwnIds for signing */ - ui->idChooser->loadIds(IDCHOOSER_ID_REQUIRED, RsGxsId()); - + ui->idChooser->loadIds(IDCHOOSER_ID_REQUIRED, default_author); + QSignalMapper *signalMapper = new QSignalMapper(this); connect(ui->postButton, SIGNAL(clicked()), signalMapper, SLOT(map())); connect(ui->imageButton, SIGNAL(clicked()), signalMapper, SLOT(map())); @@ -86,6 +86,15 @@ PostedCreatePostDialog::PostedCreatePostDialog(RsPosted *posted, const RsGxsGrou /* load settings */ processSettings(true); + + // Override the default ID, if supplied, since it is changed by processSettings + + if(!default_author.isNull()) + { + ui->idChooser->setChosenId(default_author); + + // should we save the ID in the settings here? I'm not sure we want this. + } } PostedCreatePostDialog::~PostedCreatePostDialog() diff --git a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h index 64ab35566..ba3defd06 100644 --- a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h +++ b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h @@ -39,7 +39,7 @@ public: * @param tokenQ parent callee token * @param posted */ - explicit PostedCreatePostDialog(RsPosted* posted, const RsGxsGroupId& grpId, QWidget *parent = 0); + explicit PostedCreatePostDialog(RsPosted* posted, const RsGxsGroupId& grpId, const RsGxsId& default_author=RsGxsId(),QWidget *parent = 0); ~PostedCreatePostDialog(); private: diff --git a/retroshare-gui/src/gui/Posted/PostedDialog.cpp b/retroshare-gui/src/gui/Posted/PostedDialog.cpp index fa5826151..327ecdd12 100644 --- a/retroshare-gui/src/gui/Posted/PostedDialog.cpp +++ b/retroshare-gui/src/gui/Posted/PostedDialog.cpp @@ -21,7 +21,7 @@ #include "PostedDialog.h" #include "PostedItem.h" #include "PostedGroupDialog.h" -#include "PostedListWidget.h" +#include "PostedListWidgetWithModel.h" #include "PostedUserNotify.h" #include "gui/gxs/GxsGroupShareKey.h" #include "gui/settings/rsharesettings.h" @@ -195,7 +195,7 @@ int PostedDialog::shareKeyType() GxsMessageFrameWidget *PostedDialog::createMessageFrameWidget(const RsGxsGroupId &groupId) { - return new PostedListWidget(groupId); + return new PostedListWidgetWithModel(groupId); } RsGxsCommentService *PostedDialog::getCommentService() diff --git a/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.cpp b/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.cpp new file mode 100644 index 000000000..4a5164834 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.cpp @@ -0,0 +1,1170 @@ +/******************************************************************************* + * retroshare-gui/src/gui/gxschannels/PostedListWidgetWithModel.cpp * + * * + * Copyright 2013 by Robert Fernie * + * * + * 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 "retroshare/rsgxscircles.h" + +#include "ui_PostedListWidgetWithModel.h" +#include "gui/feeds/GxsChannelPostItem.h" +#include "gui/gxs/GxsIdDetails.h" +#include "gui/gxs/GxsCommentDialog.h" +#include "util/misc.h" +#include "gui/Posted/PostedCreatePostDialog.h" +#include "gui/common/UIStateHelper.h" +#include "gui/common/RSTabWidget.h" +#include "gui/settings/rsharesettings.h" +#include "gui/feeds/SubFileItem.h" +#include "gui/notifyqt.h" +#include "gui/Identity/IdDialog.h" +#include "gui/RetroShareLink.h" +#include "util/HandleRichText.h" +#include "util/DateTime.h" +#include "util/qtthreadsutils.h" +#include "gui/common/FilesDefs.h" + +#include "gui/MainWindow.h" + +#include "PostedListWidgetWithModel.h" +#include "PostedPostsModel.h" +#include "BoardPostDisplayWidget.h" + +#include + +#define ROLE_PUBLISH FEED_TREEWIDGET_SORTROLE + +/**** + * #define DEBUG_POSTED + ***/ + +/* View mode */ +#define VIEW_MODE_FEEDS 1 +#define VIEW_MODE_FILES 2 + +// Determine the Shape and size of cells as a factor of the font height. An aspect ratio of 3/4 is what's needed +// for the image, so it's important that the height is a bit larger so as to leave some room for the text. +// +// +#define IMAGE_COPYLINK ":/images/copyrslink.png" +#define IMAGE_AUTHOR ":/images/user/personal64.png" + +Q_DECLARE_METATYPE(RsPostedPost); + +// Delegate used to paint into the table of thumbnails + +//=================================================================================================================================== +//== PostedPostDelegate == +//=================================================================================================================================== + +std::ostream& operator<<(std::ostream& o,const QSize& s) { return o << s.width() << " x " << s.height() ; } + +void PostedPostDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + if((option.state & QStyle::State_Selected)) // Avoids double display. The selected widget is never exactly the size of the rendered one, + return; // so when selected, we only draw the selected one. + + // prepare + painter->save(); + painter->setClipRect(option.rect); + + RsPostedPost post = index.data(Qt::UserRole).value() ; + + painter->save(); + + painter->fillRect( option.rect, option.palette.background()); + painter->restore(); + + QPixmap pixmap(option.rect.size()); + pixmap.fill(option.palette.alternateBase().color()); // use base() instead to have all widgets the same background + + if(mDisplayMode == BoardPostDisplayWidget_compact::DISPLAY_MODE_COMPACT) + { + BoardPostDisplayWidget_compact w(post,displayFlags(post.mMeta.mMsgId),nullptr); + + w.setFixedSize(option.rect.size()); + + w.updateGeometry(); + w.adjustSize(); + w.render(&pixmap,QPoint(0,0),QRegion(),QWidget::DrawChildren );// draw the widgets, not the background + } + else + { + BoardPostDisplayWidget_card w(post,displayFlags(post.mMeta.mMsgId),nullptr); + + w.setFixedSize(option.rect.size()); + w.updateGeometry(); + w.adjustSize(); + w.render(&pixmap,QPoint(0,0),QRegion(),QWidget::DrawChildren );// draw the widgets, not the background + } + +#ifdef TODO + if(IS_MSG_UNREAD(post.mMeta.mMsgStatus) || IS_MSG_NEW(post.mMeta.mMsgStatus)) + { + QPainter p(&pixmap); + QFontMetricsF fm(option.font); + p.drawPixmap(mZoom*QPoint(6.2*fm.height(),6.9*fm.height()),FilesDefs::getPixmapFromQtResourcePath(STAR_OVERLAY_IMAGE).scaled(mZoom*7*fm.height(),mZoom*7*fm.height(),Qt::KeepAspectRatio,Qt::SmoothTransformation)); + } +#endif + +// // debug +// if(index.row()==0 && index.column()==0) +// { +// QFile file("yourFile.png"); +// file.open(QIODevice::WriteOnly); +// pixmap.save(&file, "PNG"); +// std::cerr << "Saved pxmap to png" << std::endl; +// } + //std::cerr << "option.rect = " << option.rect.width() << "x" << option.rect.height() << ". fm.height()=" << QFontMetricsF(option.font).height() << std::endl; + + painter->save(); + painter->drawPixmap(option.rect.topLeft(), pixmap /*,.scaled(option.rect.width(),option.rect.width()*w.height()/(float)w.width(),Qt::KeepAspectRatio,Qt::SmoothTransformation)*/); + painter->restore(); +} + +QSize PostedPostDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + // This is the only place where we actually set the size of cells + + RsPostedPost post = index.data(Qt::UserRole).value() ; + + if(mDisplayMode == BoardPostDisplayWidget_compact::DISPLAY_MODE_COMPACT) + { + BoardPostDisplayWidget_compact w(post,displayFlags(post.mMeta.mMsgId),nullptr); + w.adjustSize(); + return w.size(); + } + else + { + BoardPostDisplayWidget_card w(post,displayFlags(post.mMeta.mMsgId),nullptr); + w.adjustSize(); + return w.size(); + } +} +void PostedPostDelegate::expandItem(RsGxsMessageId msgId,bool expanded) +{ + std::cerr << __PRETTY_FUNCTION__ << ": received expandItem signal. b=" << expanded << std::endl; + if(expanded) + mExpandedItems.insert(msgId); + else + mExpandedItems.erase(msgId); + + mPostListWidget->forceRedraw(); +} + +uint8_t PostedPostDelegate::displayFlags(const RsGxsMessageId &id) const +{ + uint8_t flags=0; + + if(mExpandedItems.find(id) != mExpandedItems.end()) + flags |= BoardPostDisplayWidget_compact::SHOW_NOTES; + + if(mShowCommentItems.find(id) != mShowCommentItems.end()) + flags |= BoardPostDisplayWidget_compact::SHOW_COMMENTS; + + return flags; +} + +QWidget *PostedPostDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex& index) const +{ + RsPostedPost post = index.data(Qt::UserRole).value() ; + + if(index.column() == RsPostedPostsModel::COLUMN_POSTS) + { + QWidget *w ; + + if(mDisplayMode==BoardPostDisplayWidget_compact::DISPLAY_MODE_COMPACT) + w = new BoardPostDisplayWidget_compact(post,displayFlags(post.mMeta.mMsgId),parent); + else + w = new BoardPostDisplayWidget_card(post,displayFlags(post.mMeta.mMsgId),parent); + + QObject::connect(w,SIGNAL(vote(RsGxsGrpMsgIdPair,bool)),mPostListWidget,SLOT(voteMsg(RsGxsGrpMsgIdPair,bool))); + QObject::connect(w,SIGNAL(expand(RsGxsMessageId,bool)),this,SLOT(expandItem(RsGxsMessageId,bool))); + QObject::connect(w,SIGNAL(commentsRequested(const RsGxsMessageId&,bool)),mPostListWidget,SLOT(openComments(const RsGxsMessageId&))); + QObject::connect(w,SIGNAL(changeReadStatusRequested(const RsGxsMessageId&,bool)),mPostListWidget,SLOT(changeReadStatus(const RsGxsMessageId&,bool))); + + // All other interactions with the widget should cause the msg to be set as read. + QObject::connect(w,SIGNAL(thumbnailOpenned()),mPostListWidget,SLOT(markCurrentPostAsRead())); + QObject::connect(w,SIGNAL(vote(RsGxsGrpMsgIdPair,bool)),mPostListWidget,SLOT(markCurrentPostAsRead())); + QObject::connect(w,SIGNAL(expand(RsGxsMessageId,bool)),this,SLOT(markCurrentPostAsRead())); + QObject::connect(w,SIGNAL(commentsRequested(const RsGxsMessageId&,bool)),mPostListWidget,SLOT(markCurrentPostAsRead())); + QObject::connect(w,SIGNAL(shareButtonClicked()),mPostListWidget,SLOT(markCurrentPostAsRead())); + QObject::connect(w,SIGNAL(copylinkClicked()),mPostListWidget,SLOT(copyMessageLink())); + + w->setFixedSize(option.rect.size()); + w->adjustSize(); + w->updateGeometry(); + w->adjustSize(); + + return w; + } + else + return NULL; +} +void PostedPostDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/* index */) const +{ + editor->setGeometry(option.rect); +} + +//=================================================================================================================================== +//== PostedListWidgetWithModel == +//=================================================================================================================================== + +/** Constructor */ +PostedListWidgetWithModel::PostedListWidgetWithModel(const RsGxsGroupId& postedId, QWidget *parent) : + GxsMessageFrameWidget(rsPosted, parent), + ui(new Ui::PostedListWidgetWithModel) +{ + /* Invoke the Qt Designer generated object setup routine */ + ui->setupUi(this); + + ui->postsTree->setModel(mPostedPostsModel = new RsPostedPostsModel()); + ui->postsTree->setItemDelegate(mPostedPostsDelegate = new PostedPostDelegate(this)); + ui->postsTree->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // prevents bug on w10, since row size depends on widget width + ui->postsTree->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);// more beautiful if we scroll at pixel level + ui->postsTree->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + ui->postsTree->setPlaceholderText(tr("No files in this post, or no post selected")); + ui->postsTree->setSortingEnabled(true); + ui->postsTree->sortByColumn(0, Qt::AscendingOrder); + ui->postsTree->setAutoSelect(true); + + ui->idChooser->setFlags(IDCHOOSER_ID_REQUIRED); + + connect(ui->tabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(tabCloseRequested(int))); + ui->tabWidget->hideCloseButton(0); + ui->tabWidget->hideCloseButton(1); + + connect(ui->sortStrategy_CB,SIGNAL(currentIndexChanged(int)),this,SLOT(updateSorting(int))); + connect(ui->nextButton,SIGNAL(clicked()),this,SLOT(next10Posts())); + connect(ui->prevButton,SIGNAL(clicked()),this,SLOT(prev10Posts())); + + connect(ui->postsTree,SIGNAL(customContextMenuRequested(const QPoint&)),this,SLOT(postContextMenu(const QPoint&))); + connect(ui->viewModeButton,SIGNAL(clicked()),this,SLOT(switchDisplayMode())); + + connect(mPostedPostsModel,SIGNAL(boardPostsLoaded()),this,SLOT(postPostLoad())); + + QFontMetricsF fm(font()); + + /* Setup UI helper */ + + /* Connect signals */ + connect(ui->submitPostButton, SIGNAL(clicked()), this, SLOT(createMsg())); + connect(ui->subscribeToolButton, SIGNAL(subscribe(bool)), this, SLOT(subscribeGroup(bool))); + connect(ui->filter_LE, SIGNAL(textChanged(QString)), this, SLOT(filterItems(QString))); + connect(NotifyQt::getInstance(), SIGNAL(settingsChanged()),this, SLOT(settingsChanged())); + + /* add filter actions */ + ui->postsTree->setPlaceholderText(tr("Thumbnails")); + //ui->postsTree->setMinimumWidth(COLUMN_SIZE_FONT_FACTOR_W*QFontMetricsF(font()).height()+1); + + connect(ui->postsTree,SIGNAL(sizeChanged(QSize)),this,SLOT(handlePostsTreeSizeChange(QSize))); + + /* load settings */ + processSettings(true); + + /* Initialize subscribe button */ + QIcon icon; + icon.addPixmap(QPixmap(":/images/redled.png"), QIcon::Normal, QIcon::On); + icon.addPixmap(QPixmap(":/images/start.png"), QIcon::Normal, QIcon::Off); + + // ui->commentsDialog->setTokenService(rsPosted->getTokenService(),rsPosted); + + /* Initialize GUI */ + // setAutoDownload(false); + + settingsChanged(); + setGroupId(postedId); + + mPostedPostsDelegate->setDisplayMode(BoardPostDisplayWidget_compact::DISPLAY_MODE_CARD); + + switchDisplayMode(); // makes everything consistent and chooses classic view as default + updateSorting(ui->sortStrategy_CB->currentIndex()); + + 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_POSTED ); +} + +void PostedListWidgetWithModel::switchDisplayMode() +{ + if(mPostedPostsDelegate->getDisplayMode() == BoardPostDisplayWidget_compact::DISPLAY_MODE_CARD) + { + ui->viewModeButton->setIcon(FilesDefs::getIconFromQtResourcePath(":images/classic.png")); + ui->viewModeButton->setToolTip(tr("Click to switch to card view")); + + mPostedPostsDelegate->setDisplayMode(BoardPostDisplayWidget_compact::DISPLAY_MODE_COMPACT); + } + else + { + ui->viewModeButton->setIcon(FilesDefs::getIconFromQtResourcePath(":images/card.png")); + ui->viewModeButton->setToolTip(tr("Click to switch to compact view")); + + mPostedPostsDelegate->setDisplayMode(BoardPostDisplayWidget_compact::DISPLAY_MODE_CARD); + } + mPostedPostsModel->triggerRedraw(); +} + +void PostedListWidgetWithModel::updateSorting(int s) +{ + switch(s) + { + default: + case 0: mPostedPostsModel->setSortingStrategy(RsPostedPostsModel::SORT_NEW_SCORE); break; + case 1: mPostedPostsModel->setSortingStrategy(RsPostedPostsModel::SORT_TOP_SCORE); break; + case 2: mPostedPostsModel->setSortingStrategy(RsPostedPostsModel::SORT_HOT_SCORE); break; + } +} + +void PostedListWidgetWithModel::handlePostsTreeSizeChange(QSize size) +{ + std::cerr << "resizing!"<< std::endl; + mPostedPostsDelegate->setCellWidth(size.width()); + mPostedPostsModel->triggerRedraw(); +} + +void PostedListWidgetWithModel::filterItems(QString text) +{ + QStringList lst = text.split(" ",QString::SkipEmptyParts) ; + + uint32_t count; + mPostedPostsModel->setFilter(lst,count) ; + + ui->showLabel->setText(QString::number(mPostedPostsModel->displayedStartPostIndex()+1)+" - "+QString::number(std::min(mPostedPostsModel->filteredPostsCount(),mPostedPostsModel->displayedStartPostIndex()+10+1))); +} + +void PostedListWidgetWithModel::next10Posts() +{ + if(mPostedPostsModel->displayedStartPostIndex() + 10 < mPostedPostsModel->filteredPostsCount()) + { + mPostedPostsModel->setPostsInterval(10+mPostedPostsModel->displayedStartPostIndex(),10); + ui->showLabel->setText(QString::number(mPostedPostsModel->displayedStartPostIndex()+1)+" - "+QString::number(std::min(mPostedPostsModel->filteredPostsCount(),mPostedPostsModel->displayedStartPostIndex()+10+1))); + } +} + +void PostedListWidgetWithModel::prev10Posts() +{ + if((int)mPostedPostsModel->displayedStartPostIndex() - 10 >= 0) + { + mPostedPostsModel->setPostsInterval(mPostedPostsModel->displayedStartPostIndex()-10,10); + ui->showLabel->setText(QString::number(mPostedPostsModel->displayedStartPostIndex()+1)+" - "+QString::number(std::min(mPostedPostsModel->filteredPostsCount(),mPostedPostsModel->displayedStartPostIndex()+10+1))); + } +} + +void PostedListWidgetWithModel::showAuthorInPeople() +{ + QModelIndex index = ui->postsTree->selectionModel()->currentIndex(); + + if(!index.isValid()) + throw std::runtime_error("No post under mouse!"); + + RsPostedPost post = index.data(Qt::UserRole).value() ; + + if(post.mMeta.mMsgId.isNull()) + throw std::runtime_error("Post has empty MsgId!"); + + if(post.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(post.mMeta.mAuthorId)); +} +void PostedListWidgetWithModel::postContextMenu(const QPoint&) +{ + QMenu menu(this); + + menu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_COPYLINK), tr("Copy RetroShare Link"), this, SLOT(copyMessageLink())); + menu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_AUTHOR), tr("Show author in People tab"), this, SLOT(showAuthorInPeople())); + +#ifdef TODO + // This feature is not implemented yet in libretroshare. + + if(IS_GROUP_PUBLISHER(mGroup.mMeta.mSubscribeFlags)) + menu.addAction(FilesDefs::getIconFromQtResourcePath(":/images/edit_16.png"), tr("Edit"), this, SLOT(editPost())); +#endif + + menu.exec(QCursor::pos()); +} + +void PostedListWidgetWithModel::copyMessageLink() +{ + try + { + if (groupId().isNull()) + throw std::runtime_error("No channel currently selected!"); + + QModelIndex index = ui->postsTree->selectionModel()->currentIndex(); + + if(!index.isValid()) + throw std::runtime_error("No post under mouse!"); + + RsPostedPost post = index.data(Qt::UserRole).value() ; + + if(post.mMeta.mMsgId.isNull()) + throw std::runtime_error("Post has empty MsgId!"); + + RetroShareLink link = RetroShareLink::createGxsMessageLink(RetroShareLink::TYPE_POSTED, groupId(), post.mMeta.mMsgId, QString::fromUtf8(post.mMeta.mMsgName.c_str())); + + if (!link.valid()) + throw std::runtime_error("Link is not valid"); + + QList urls; + urls.push_back(link); + RSLinkClipboard::copyLinks(urls); + QMessageBox::information(NULL,tr("information"),tr("The Retrohare link was copied to your clipboard.")) ; + } + catch(std::exception& e) + { + QMessageBox::critical(NULL,tr("Link creation error"),tr("Link could not be created: ")+e.what()); + } +} + +#ifdef TODO +void PostedListWidgetWithModel::editPost() +{ + QModelIndex index = ui->postsTree->selectionModel()->currentIndex(); + RsPostedPost post = index.data(Qt::UserRole).value() ; + + CreatePostedMsg *msgDialog = new CreatePostedMsg(post.mMeta.mGroupId,post.mMeta.mMsgId); + msgDialog->show(); +} +#endif + +void PostedListWidgetWithModel::handleEvent_main_thread(std::shared_ptr event) +{ + const RsGxsPostedEvent *e = dynamic_cast(event.get()); + + if(!e) + return; + + switch(e->mPostedEventCode) + { + case RsPostedEventCode::NEW_MESSAGE: // [[fallthrough]]; + { + // special treatment here because the message might be a comment, so we need to refresh the comment tab if openned + + for(int i=2;itabWidget->count();++i) + { + auto *t = dynamic_cast(ui->tabWidget->widget(i)); + + if(t->groupId() == e->mPostedGroupId) + t->refresh(); + } + } + case RsPostedEventCode::NEW_POSTED_GROUP: // [[fallthrough]]; + case RsPostedEventCode::UPDATED_POSTED_GROUP: // [[fallthrough]]; + case RsPostedEventCode::UPDATED_MESSAGE: + { + if(e->mPostedGroupId == groupId()) + updateDisplay(true); + } + + default: + break; + } +} + +#ifdef TO_REMOVE +void PostedListWidgetWithModel::showPostDetails() +{ + QModelIndex index = ui->postsTree->selectionModel()->currentIndex(); + RsPostedPost post = index.data(Qt::UserRole).value() ; + + QTextDocument doc; + doc.setHtml(post.mMsg.c_str()); + + if(post.mMeta.mPublishTs == 0) + { + ui->postDetails_TE->clear(); + ui->postLogo_LB->hide(); + ui->postName_LB->hide(); + ui->postTime_LB->hide(); + mChannelPostFilesModel->clear(); + + return; + } + + ui->postLogo_LB->show(); + ui->postName_LB->show(); + ui->postTime_LB->show(); + + if(index.row()==0 && index.column()==0) + std::cerr << "here" << std::endl; + + std::cerr << "showPostDetails: setting mSelectedPost to current post Id " << post.mMeta.mMsgId << ". Previous value: " << mSelectedPost << std::endl; + mSelectedPost = post.mMeta.mMsgId; + + std::list files; + for(auto& file:post.mFiles) + files.push_back(ChannelPostFileInfo(file,post.mMeta.mPublishTs)); + + mChannelPostFilesModel->setFiles(files); + + auto all_msgs_versions(post.mOlderVersions); + all_msgs_versions.insert(post.mMeta.mMsgId); + + ui->commentsDialog->commentLoad(post.mMeta.mGroupId, all_msgs_versions, post.mMeta.mMsgId); + + std::cerr << "Showing details about selected index : "<< index.row() << "," << index.column() << std::endl; + + ui->postDetails_TE->setText(RsHtml().formatText(NULL, QString::fromUtf8(post.mMsg.c_str()), RSHTML_FORMATTEXT_EMBED_SMILEYS | RSHTML_FORMATTEXT_EMBED_LINKS)); + + QPixmap postImage; + + if (post.mThumbnail.mData != NULL) + GxsIdDetails::loadPixmapFromData(post.mThumbnail.mData, post.mThumbnail.mSize, postImage,GxsIdDetails::ORIGINAL); + else + postImage = FilesDefs::getPixmapFromQtResourcePath(ChannelPostThumbnailView::CHAN_DEFAULT_IMAGE); + + int W = QFontMetricsF(font()).height() * 8; + + // Using fixed width so that the post will not displace the text when we browse. + + ui->postLogo_LB->setPixmap(postImage); + ui->postLogo_LB->setFixedSize(W,postImage.height()/(float)postImage.width()*W); + + ui->postName_LB->setText(QString::fromUtf8(post.mMeta.mMsgName.c_str())); + ui->postName_LB->setFixedWidth(W); + ui->postTime_LB->setText(QDateTime::fromMSecsSinceEpoch(post.mMeta.mPublishTs*1000).toString("MM/dd/yyyy, hh:mm")); + ui->postTime_LB->setFixedWidth(W); + + //ui->channelPostFiles_TV->resizeColumnToContents(RsGxsChannelPostFilesModel::COLUMN_FILES_FILE); + //ui->channelPostFiles_TV->resizeColumnToContents(RsGxsChannelPostFilesModel::COLUMN_FILES_SIZE); + ui->channelPostFiles_TV->setAutoSelect(true); + + // Now also set the post as read + + if(IS_MSG_UNREAD(post.mMeta.mMsgStatus) || IS_MSG_NEW(post.mMeta.mMsgStatus)) + { + RsGxsGrpMsgIdPair postId; + postId.second = post.mMeta.mMsgId; + postId.first = post.mMeta.mGroupId; + + RsThread::async([postId]() { rsGxsChannels->markRead(postId, true) ; } ); + } +} +#endif + +void PostedListWidgetWithModel::updateGroupData() +{ + if(groupId().isNull()) + return; + + RsThread::async([this]() + { + std::vector groups; + + if(!rsPosted->getBoardsInfo(std::list{ groupId() }, groups)) + { + std::cerr << __PRETTY_FUNCTION__ << " failed to get boards group value for group: " << groupId() << std::endl; + return; + } + + if(groups.size() != 1) + { + RsErr() << __PRETTY_FUNCTION__ << " cannot retrieve posted group data for group ID " << groupId() << ": ERROR." << std::endl; + return; + } + + RsQThreadUtils::postToObject( [this,groups]() + { + bool group_changed = (groups[0].mMeta.mGroupId!=mGroup.mMeta.mGroupId); + + mGroup = groups[0]; + mPostedPostsModel->updateBoard(groupId()); + + insertBoardDetails(mGroup); + + if(group_changed) + while(ui->tabWidget->widget(2) != nullptr) + tabCloseRequested(2); + + emit groupDataLoaded(); + emit groupChanged(this); // signals the parent widget to e.g. update the group tab name + } ); + }); +} + +void PostedListWidgetWithModel::postPostLoad() +{ + std::cerr << "Post channel load..." << std::endl; + + if(!mSelectedPost.isNull()) + { + QModelIndex index = mPostedPostsModel->getIndexOfMessage(mSelectedPost); + + std::cerr << "Setting current index to " << index.row() << ","<< index.column() << " for current post " + << mSelectedPost.toStdString() << std::endl; + + ui->postsTree->selectionModel()->setCurrentIndex(index,QItemSelectionModel::ClearAndSelect); + ui->postsTree->scrollTo(index);//May change if model reloaded + ui->postsTree->setFocus(); + } + else + std::cerr << "No pre-selected channel post." << std::endl; + + whileBlocking(ui->showLabel)->setText(QString::number(mPostedPostsModel->displayedStartPostIndex()+1)+" - "+QString::number(std::min(mPostedPostsModel->filteredPostsCount(),mPostedPostsModel->displayedStartPostIndex()+10+1))); + whileBlocking(ui->filter_LE)->setText(QString()); +} + +void PostedListWidgetWithModel::forceRedraw() +{ + if(mPostedPostsModel) + mPostedPostsModel->deepUpdate(); +} + +void PostedListWidgetWithModel::updateDisplay(bool complete) +{ +#ifdef DEBUG_CHANNEL + std::cerr << "udateDisplay: groupId()=" << groupId()<< std::endl; +#endif + if(groupId().isNull()) + { +#ifdef DEBUG_CHANNEL + std::cerr << " group_id=0. Return!"<< std::endl; +#endif + return; + } + + if(mGroup.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. + { + updateGroupData(); + + return; + } +} +PostedListWidgetWithModel::~PostedListWidgetWithModel() +{ + rsEvents->unregisterEventsHandler(mEventHandlerId); + // save settings + processSettings(false); + + delete ui; +} + +void PostedListWidgetWithModel::processSettings(bool load) +{ + Settings->beginGroup(QString("ChannelPostsWidget")); + + if (load) + { + } + else + { + } + + Settings->endGroup(); +} + +void PostedListWidgetWithModel::settingsChanged() +{ +} + +QString PostedListWidgetWithModel::groupName(bool) +{ + return QString::fromUtf8(mGroup.mMeta.mGroupName.c_str()); +} + +void PostedListWidgetWithModel::groupNameChanged(const QString &name) +{ +} + +QIcon PostedListWidgetWithModel::groupIcon() +{ + /* CHANNEL IMAGE */ + QPixmap postedImage; + if (mGroup.mGroupImage.mData != NULL) + GxsIdDetails::loadPixmapFromData(mGroup.mGroupImage.mData, mGroup.mGroupImage.mSize, postedImage,GxsIdDetails::ORIGINAL); + else + postedImage = FilesDefs::getPixmapFromQtResourcePath(BoardPostDisplayWidget_compact::DEFAULT_BOARD_IMAGE); + + return QIcon(postedImage); +} + +void PostedListWidgetWithModel::setAllMessagesReadDo(bool read, uint32_t &token) +{ + if (groupId().isNull() || !IS_GROUP_SUBSCRIBED(mGroup.mMeta.mSubscribeFlags)) + return; + + QModelIndex src_index; + + mPostedPostsModel->setAllMsgReadStatus(read); + +} + +void PostedListWidgetWithModel::openComments(const RsGxsMessageId& msgId) +{ + QModelIndex index = mPostedPostsModel->getIndexOfMessage(msgId); + + if(!index.isValid()) + return; + + RsGxsId current_author; + ui->idChooser->getChosenId(current_author); + + RsPostedPost post = index.data(Qt::UserRole).value() ; + auto *commentDialog = new GxsCommentDialog(this,current_author,rsPosted->getTokenService(),rsPosted); + + std::set msg_versions({post.mMeta.mMsgId}); + commentDialog->commentLoad(post.mMeta.mGroupId, msg_versions, post.mMeta.mMsgId); + + QString title = QString::fromUtf8(post.mMeta.mMsgName.c_str()); + if(title.length() > 30) + title = title.left(27) + "..."; + + ui->tabWidget->addTab(commentDialog,title); +} + +void PostedListWidgetWithModel::markCurrentPostAsRead() +{ + QModelIndex index = ui->postsTree->selectionModel()->currentIndex(); + + if(!index.isValid()) + throw std::runtime_error("No post under mouse!"); + + mPostedPostsModel->setMsgReadStatus(index,true); +} + +void PostedListWidgetWithModel::changeReadStatus(const RsGxsMessageId& msgId,bool b) +{ + QModelIndex index=mPostedPostsModel->getIndexOfMessage(msgId); + mPostedPostsModel->setMsgReadStatus(index, b); +} +void PostedListWidgetWithModel::tabCloseRequested(int index) +{ + std::cerr << "GxsCommentContainer::tabCloseRequested(" << index << ")"; + std::cerr << std::endl; + + if (index != 0) + { + QWidget *comments = ui->tabWidget->widget(index); + ui->tabWidget->removeTab(index); + delete comments; + } + else + { + std::cerr << "GxsCommentContainer::tabCloseRequested() Not closing First Tab"; + std::cerr << std::endl; + } +} + +void PostedListWidgetWithModel::createMsg() +{ + if (groupId().isNull()) { + return; + } + + if (!IS_GROUP_SUBSCRIBED(mGroup.mMeta.mSubscribeFlags)) { + return; + } + RsGxsId author_id; + ui->idChooser->getChosenId(author_id); +std::cerr << "Chosing default ID " << author_id<< std::endl; + PostedCreatePostDialog *msgDialog = new PostedCreatePostDialog(rsPosted,groupId(),author_id); + msgDialog->show(); + + /* window will destroy itself! */ +} + +void PostedListWidgetWithModel::insertBoardDetails(const RsPostedGroup& group) +{ + // save selection if needed + + /* IMAGE */ + QPixmap chanImage; + if (group.mGroupImage.mData != NULL) { + GxsIdDetails::loadPixmapFromData(group.mGroupImage.mData, group.mGroupImage.mSize, chanImage,GxsIdDetails::ORIGINAL); + } else { + chanImage = QPixmap(BoardPostDisplayWidget_compact::DEFAULT_BOARD_IMAGE); + } + if(group.mMeta.mGroupName.empty()) + ui->namelabel->setText(tr("[No name]")); + else + ui->namelabel->setText(QString::fromUtf8(group.mMeta.mGroupName.c_str())); + + ui->logoLabel->setPixmap(chanImage); + ui->logoLabel->setFixedSize(QSize(ui->logoLabel->height()*chanImage.width()/(float)chanImage.height(),ui->logoLabel->height())); // make the logo have the same aspect ratio than the original image + + //ui->submitPostButton->setEnabled(bool(group.mMeta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_PUBLISH)); + + ui->subscribeToolButton->setSubscribed(IS_GROUP_SUBSCRIBED(group.mMeta.mSubscribeFlags)); + ui->subscribeToolButton->setEnabled(true); + + RetroShareLink link; + + if (IS_GROUP_SUBSCRIBED(group.mMeta.mSubscribeFlags)) + ui->subscribeToolButton->setText(tr("Subscribed") + " " + QString::number(group.mMeta.mPop) ); + else + ui->subscribeToolButton->setText(tr("Subscribe")); + + ui->infoPosts->setText(QString::number(group.mMeta.mVisibleMsgCount)); + + if(group.mMeta.mLastPost==0) + ui->infoLastPost->setText(tr("Never")); + else + ui->infoLastPost->setText(DateTime::formatLongDateTime(group.mMeta.mLastPost)); + QString formatDescription = QString::fromUtf8(group.mDescription.c_str()); + + unsigned int formatFlag = RSHTML_FORMATTEXT_EMBED_LINKS; + + // embed smileys ? + if (Settings->valueFromGroup(QString("ChannelPostsWidget"), QString::fromUtf8("Emoteicons_ChannelDecription"), true).toBool()) + formatFlag |= RSHTML_FORMATTEXT_EMBED_SMILEYS; + + formatDescription = RsHtml().formatText(NULL, formatDescription, formatFlag); + + ui->infoDescription->setText(formatDescription); + ui->infoAdministrator->setId(group.mMeta.mAuthorId) ; + + link = RetroShareLink::createMessage(group.mMeta.mAuthorId, ""); + ui->infoAdministrator->setText(link.toHtml()); + + ui->createdinfolabel->setText(DateTime::formatLongDateTime(group.mMeta.mPublishTs)); + + QString distrib_string ( "[unknown]" ); + + switch(group.mMeta.mCircleType) + { + case GXS_CIRCLE_TYPE_PUBLIC: distrib_string = tr("Public") ; + break ; + case GXS_CIRCLE_TYPE_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 GXS_CIRCLE_TYPE_YOUR_EYES_ONLY: distrib_string = tr("Your eyes only"); + break ; + case GXS_CIRCLE_TYPE_LOCAL: distrib_string = tr("You and your friend nodes"); + break ; + default: + std::cerr << "(EE) badly initialised group distribution ID = " << group.mMeta.mCircleType << std::endl; + } + + ui->infoDistribution->setText(distrib_string); +#ifdef TODO + ui->infoWidget->show(); + ui->feedWidget->hide(); + ui->fileWidget->hide(); + + //ui->feedToolButton->setEnabled(false); + //ui->fileToolButton->setEnabled(false); +#endif +} + +#ifdef TODO +int PostedListWidgetWithModel::viewMode() +{ + if (ui->feedToolButton->isChecked()) { + return VIEW_MODE_FEEDS; + } else if (ui->fileToolButton->isChecked()) { + return VIEW_MODE_FILES; + } + + /* Default */ + return VIEW_MODE_FEEDS; +} +#endif + +#ifdef TODO +/*static*/ bool PostedListWidgetWithModel::filterItem(FeedItem *feedItem, const QString &text, int filter) +{ + GxsChannelPostItem *item = dynamic_cast(feedItem); + if (!item) { + return true; + } + + bool bVisible = text.isEmpty(); + + if (!bVisible) + { + switch(filter) + { + case FILTER_TITLE: + bVisible = item->getTitleLabel().contains(text,Qt::CaseInsensitive); + break; + case FILTER_MSG: + bVisible = item->getMsgLabel().contains(text,Qt::CaseInsensitive); + break; + case FILTER_FILE_NAME: + { + std::list fileItems = item->getFileItems(); + std::list::iterator lit; + for(lit = fileItems.begin(); lit != fileItems.end(); ++lit) + { + SubFileItem *fi = *lit; + QString fileName = QString::fromUtf8(fi->FileName().c_str()); + bVisible = (bVisible || fileName.contains(text,Qt::CaseInsensitive)); + } + break; + } + default: + bVisible = true; + break; + } + } + + return bVisible; +} + +void PostedListWidget::createPostItemFromMetaData(const RsGxsMsgMetaData& meta,bool related) +{ + GxsChannelPostItem *item = NULL; + RsGxsChannelPost post; + + if(!meta.mOrigMsgId.isNull()) + { + FeedItem *feedItem = ui->feedWidget->findFeedItem(GxsChannelPostItem::computeIdentifier(meta.mOrigMsgId)) ; + item = dynamic_cast(feedItem); + + if(item) + { + post = feedItem->post(); + ui->feedWidget->removeFeedItem(item) ; + + post.mOlderVersions.insert(post.mMeta.mMsgId); + + GxsChannelPostItem *item = new GxsChannelPostItem(this, 0, post, true, false,post.mOlderVersions); + ui->feedWidget->addFeedItem(item, ROLE_PUBLISH, QDateTime::fromTime_t(post.mMeta.mPublishTs)); + + return ; + } + } + + if (related) + { + FeedItem *feedItem = ui->feedWidget->findFeedItem(GxsChannelPostItem::computeIdentifier(meta.mMsgId)) ; + item = dynamic_cast(feedItem); + } + if (item) + { + item->setPost(post); + ui->feedWidget->setSort(item, ROLE_PUBLISH, QDateTime::fromTime_t(meta.mPublishTs)); + } + else + { + GxsChannelPostItem *item = new GxsChannelPostItem(this, 0, meta.mGroupId,meta.mMsgId, true, true); + ui->feedWidget->addFeedItem(item, ROLE_PUBLISH, QDateTime::fromTime_t(post.mMeta.mPublishTs)); + } +#ifdef TODO + ui->fileWidget->addFiles(post, related); +#endif +} + +void PostedListWidget::createPostItem(const RsGxsChannelPost& post, bool related) +{ + GxsChannelPostItem *item = NULL; + + const RsMsgMetaData& meta(post.mMeta); + + if(!meta.mOrigMsgId.isNull()) + { + FeedItem *feedItem = ui->feedWidget->findFeedItem(GxsChannelPostItem::computeIdentifier(meta.mOrigMsgId)) ; + item = dynamic_cast(feedItem); + + if(item) + { + std::set older_versions(item->olderVersions()); // we make a copy because the item will be deleted + ui->feedWidget->removeFeedItem(item) ; + + older_versions.insert(meta.mMsgId); + + GxsChannelPostItem *item = new GxsChannelPostItem(this, 0, mGroup.mMeta,meta.mMsgId, true, false,older_versions); + ui->feedWidget->addFeedItem(item, ROLE_PUBLISH, QDateTime::fromTime_t(meta.mPublishTs)); + + return ; + } + } + + if (related) + { + FeedItem *feedItem = ui->feedWidget->findFeedItem(GxsChannelPostItem::computeIdentifier(meta.mMsgId)) ; + item = dynamic_cast(feedItem); + } + if (item) + { + item->setPost(post); + ui->feedWidget->setSort(item, ROLE_PUBLISH, QDateTime::fromTime_t(meta.mPublishTs)); + } + else + { + GxsChannelPostItem *item = new GxsChannelPostItem(this, 0, mGroup.mMeta,meta.mMsgId, true, true); + ui->feedWidget->addFeedItem(item, ROLE_PUBLISH, QDateTime::fromTime_t(meta.mPublishTs)); + } + + ui->fileWidget->addFiles(post, related); +} + +void PostedListWidget::fillThreadCreatePost(const QVariant &post, bool related, int current, int count) +{ + /* show fill progress */ + if (count) { + ui->progressBar->setValue(current * ui->progressBar->maximum() / count); + } + + if (!post.canConvert()) { + return; + } + + createPostItem(post.value(), related); +} +#endif + +void PostedListWidgetWithModel::blank() +{ +#ifdef TODO + ui->postButton->setEnabled(false); + ui->subscribeToolButton->setEnabled(false); + + ui->channelName_LB->setText(tr("No Channel Selected")); + ui->logoLabel->setPixmap(FilesDefs::getPixmapFromQtResourcePath(":/icons/png/channels.png")); + ui->infoPosts->setText(""); + ui->infoLastPost->setText(""); + ui->infoAdministrator->setText(""); + ui->infoDistribution->setText(""); + ui->infoCreated->setText(""); + ui->infoDescription->setText(""); + + mChannelPostsModel->clear(); + mChannelPostFilesModel->clear(); + ui->postDetails_TE->clear(); + ui->postLogo_LB->hide(); + ui->postName_LB->hide(); + ui->postTime_LB->hide(); +#endif + groupNameChanged(QString()); +} + +bool PostedListWidgetWithModel::navigate(const RsGxsMessageId& msgId) +{ + QModelIndex index = mPostedPostsModel->getIndexOfMessage(msgId); + + if(!index.isValid()) + { + std::cerr << "(EE) Cannot navigate to msg " << msgId << " in board " << mGroup.mMeta.mGroupId << ": index unknown. Setting mNavigatePendingMsgId." << std::endl; + + mSelectedPost = 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. + } + + ui->postsTree->selectionModel()->setCurrentIndex(index,QItemSelectionModel::ClearAndSelect); + ui->postsTree->scrollTo(index);//May change if model reloaded + ui->postsTree->setFocus(); + + return true; +} + +void PostedListWidgetWithModel::subscribeGroup(bool subscribe) +{ + RsGxsGroupId grpId(groupId()); + if (grpId.isNull()) return; + + RsThread::async([=]() + { + uint32_t token; + rsPosted->subscribeToGroup(token,grpId, subscribe); + } ); +} + +void PostedListWidgetWithModel::voteMsg(RsGxsGrpMsgIdPair msg,bool up_or_down) +{ + RsGxsId voter_id ; + if(ui->idChooser->getChosenId(voter_id) != GxsIdChooser::KnowId) + { + std::cerr << "(EE) No id returned by GxsIdChooser. Somthing's wrong?" << std::endl; + return; + } + + rsPosted->voteForPost(up_or_down,msg.first,msg.second,voter_id); +} + +#ifdef TODO +class PostedPostsReadData +{ +public: + PostedPostsReadData(bool read) + { + mRead = read; + mLastToken = 0; + } + +public: + bool mRead; + uint32_t mLastToken; +}; + +static void setAllMessagesReadCallback(FeedItem *feedItem, void *data) +{ + GxsChannelPostItem *channelPostItem = dynamic_cast(feedItem); + if (!channelPostItem) { + return; + } + + GxsChannelPostsReadData *readData = (GxsChannelPostsReadData*) data; + bool isRead = !channelPostItem->isUnread() ; + + if(channelPostItem->isLoaded() && (isRead == readData->mRead)) + return ; + + RsGxsGrpMsgIdPair msgPair = std::make_pair(channelPostItem->groupId(), channelPostItem->messageId()); + rsGxsChannels->setMessageReadStatus(readData->mLastToken, msgPair, readData->mRead); +} + +void PostedListWidgetWithModel::setAllMessagesReadDo(bool read, uint32_t &token) +{ + if (groupId().isNull() || !IS_GROUP_SUBSCRIBED(mGroup.mMeta.mSubscribeFlags)) { + return; + } + + GxsChannelPostsReadData data(read); + //ui->feedWidget->withAll(setAllMessagesReadCallback, &data); + + token = data.mLastToken; +} + +#endif diff --git a/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.h b/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.h new file mode 100644 index 000000000..3b8ebc7df --- /dev/null +++ b/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.h @@ -0,0 +1,175 @@ +/******************************************************************************* + * retroshare-gui/src/gui/gxschannels/BoardPostsWidget.h * + * * + * Copyright 2013 by Robert Fernie * + * * + * 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 . * + * * + *******************************************************************************/ + +#ifndef _GXS_CHANNELPOSTSWIDGET_H +#define _GXS_CHANNELPOSTSWIDGET_H + +#include + +#include + +#include "retroshare/rsposted.h" + +#include "gui/gxs/GxsMessageFramePostWidget.h" +#include "gui/feeds/FeedHolder.h" +#include "gui/Posted/BoardPostDisplayWidget.h" + +namespace Ui { +class PostedListWidgetWithModel; +} + +class QTreeWidgetItem; +class QSortFilterProxyModel; +class RsPostedPostsModel; +class PostedListWidgetWithModel; + +class PostedPostDelegate: public QAbstractItemDelegate +{ + Q_OBJECT + +public: + PostedPostDelegate(PostedListWidgetWithModel *p,QObject *parent=0) : QAbstractItemDelegate(parent),mCellWidthPix(100),mPostListWidget(p),mDisplayMode(BoardPostDisplayWidget_compact::DISPLAY_MODE_COMPACT){} + virtual ~PostedPostDelegate(){} + + void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const override; + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + + void setCellWidth(int pix) { mCellWidthPix = pix; } + void setDisplayMode(BoardPostDisplayWidget_compact::DisplayMode dm) { mDisplayMode = dm; } + BoardPostDisplayWidget_compact::DisplayMode getDisplayMode() const { return mDisplayMode; } + +public slots: + void expandItem(RsGxsMessageId msgId,bool expanded); + //void commentItem(RsGxsMessageId msgId,bool comment); + +private: + // The class keeps a list of expanded items. Because items are constantly re-created, it is not possible + // to let the items themselves hold that information. + + uint8_t displayFlags(const RsGxsMessageId& id) const; + + int mCellWidthPix; + PostedListWidgetWithModel *mPostListWidget; // used for sending vote signals and so on. + BoardPostDisplayWidget_compact::DisplayMode mDisplayMode; + std::set mExpandedItems; + std::set mShowCommentItems; +}; + +class PostedListWidgetWithModel: public GxsMessageFrameWidget +{ + Q_OBJECT + +public: + /* Filters */ + enum Filter { + SORT_TITLE = 1, + SORT_MSG = 2, + SORT_FILE_NAME = 3 + }; + +public: + /** Default Constructor */ + PostedListWidgetWithModel(const RsGxsGroupId &postedId, QWidget *parent = 0); + /** Default Destructor */ + ~PostedListWidgetWithModel(); + + /* GxsMessageFrameWidget */ + virtual QIcon groupIcon() override; + virtual void groupIdChanged() override { updateDisplay(true); } + virtual QString groupName(bool) override ; + virtual bool navigate(const RsGxsMessageId&) override; + + void updateDisplay(bool complete) ; + void forceRedraw(); // does not re-load the data, but makes sure the underlying model triggers a full redraw, recomputes sizes, etc. + +#ifdef TODO + /* FeedHolder */ + virtual QScrollArea *getScrollArea(); + virtual void deleteFeedItem(FeedItem *feedItem, uint32_t type); + virtual void openChat(const RsPeerId& peerId); +#endif +public slots: + virtual void openComments(const RsGxsMessageId &msgId); + virtual void changeReadStatus(const RsGxsMessageId& msgId,bool b); + +protected: + /* GxsMessageFramePostWidget */ + virtual void groupNameChanged(const QString &name); + +#ifdef TODO + virtual bool insertGroupData(const RsGxsGenericGroupData *data) override; +#endif + virtual void blank() override; + +#ifdef TODO + virtual bool getGroupData(RsGxsGenericGroupData *& data) override; + virtual void getMsgData(const std::set& msgIds,std::vector& posts) override; + virtual void getAllMsgData(std::vector& posts) override; + virtual void insertPosts(const std::vector& posts) override; + virtual void insertAllPosts(const std::vector& posts, GxsMessageFramePostThread *thread) override; +#endif + + /* GxsMessageFrameWidget */ + virtual void setAllMessagesReadDo(bool read, uint32_t &token) override; + +private slots: + void showAuthorInPeople(); + void tabCloseRequested(int index); + void updateSorting(int); + void switchDisplayMode(); + void updateGroupData(); + void createMsg(); + void subscribeGroup(bool subscribe); + void settingsChanged(); + void postPostLoad(); + void postContextMenu(const QPoint&); + void copyMessageLink(); + void next10Posts(); + void prev10Posts(); + void filterItems(QString s); + +public slots: + void handlePostsTreeSizeChange(QSize size); + void voteMsg(RsGxsGrpMsgIdPair msg,bool up_or_down); + void markCurrentPostAsRead(); + +private: + void processSettings(bool load); + int viewMode(); + + void insertBoardDetails(const RsPostedGroup &group); + void handleEvent_main_thread(std::shared_ptr event); + +private: + RsPostedGroup mGroup; + RsEventsHandlerId_t mEventHandlerId ; + + RsPostedPostsModel *mPostedPostsModel; + PostedPostDelegate *mPostedPostsDelegate; + + RsGxsMessageId mSelectedPost; + + /* UI - from Designer */ + Ui::PostedListWidgetWithModel *ui; +}; + +#endif diff --git a/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.ui b/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.ui new file mode 100644 index 000000000..615021115 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.ui @@ -0,0 +1,560 @@ + + + PostedListWidgetWithModel + + + + 0 + 0 + 731 + 593 + + + + Form + + + + + + 1 + + + true + + + + Details + + + + + + false + + + + + + Board Details + + + false + + + false + + + + 6 + + + + + 0 + + + + + + 75 + true + + + + Popularity + + + + + + + + 75 + true + + + + 0 + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Posts + + + + + + + + 75 + true + + + + 0 + + + + + + + + 75 + true + + + + Created + + + + + + + unknown + + + + + + + + 75 + true + + + + Administrator: + + + + + + + unknown + + + true + + + + + + + + 75 + true + + + + Distribution: + + + + + + + unknown + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Last Post: + + + + + + + unknown + + + + + + + + 64 + 64 + + + + + 64 + 64 + + + + :/icons/png/postedlinks.png + + + true + + + + + + + + 14 + + + + TextLabel + + + + + + + + + + 0 + 0 + + + + + 50 + false + false + + + + + + + Subscribe + + + false + + + + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8pt;">Description</span></p></body></html> + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + true + + + true + + + + + + + + + + + Posts + + + + + + QFrame::Box + + + QFrame::Sunken + + + + 6 + + + 2 + + + 6 + + + 2 + + + + + + 0 + 0 + + + + Create Post + + + + :/images/write.png:/images/write.png + + + + 24 + 24 + + + + Qt::ToolButtonTextBesideIcon + + + true + + + + + + + <html><head/><body><p><span style=" font-family:'-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol'; font-size:14px; color:#24292e; background-color:#ffffff;">Select sorting</span></p></body></html> + + + + + + 0 + + + + 24 + 24 + + + + + New + + + + :/icons/png/new.png:/icons/png/new.png + + + + + Top + + + + :/icons/png/top.png:/icons/png/top.png + + + + + Hot + + + + :/icons/png/flame.png:/icons/png/flame.png + + + + + + + + Search + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 0 + + + + + + true + + + + Classic view + + + + :/images/classic.png:/images/classic.png + + + + 24 + 24 + + + + true + + + true + + + + + + + + + Previous + + + + + + + :/icons/png/arrow-left.png:/icons/png/arrow-left.png + + + + + + + 1-10 + + + + + + + Next + + + + + + + :/icons/png/arrow-right.png:/icons/png/arrow-right.png + + + + + + + <html><head/><body><p>Default identity used when voting</p></body></html> + + + + + + + + + + Qt::CustomContextMenu + + + QAbstractItemView::CurrentChanged|QAbstractItemView::SelectedClicked + + + QAbstractItemView::ScrollPerPixel + + + 0 + + + false + + + false + + + + + + + + + + + + GxsIdLabel + QLabel +
gui/gxs/GxsIdLabel.h
+
+ + SubscribeToolButton + QToolButton +
gui/common/SubscribeToolButton.h
+
+ + RSTreeView + QTreeView +
gui/common/RSTreeView.h
+
+ + LineEditClear + QLineEdit +
gui/common/LineEditClear.h
+
+ + GxsIdChooser + QComboBox +
gui/gxs/GxsIdChooser.h
+
+ + RSTabWidget + QTabWidget +
gui/common/RSTabWidget.h
+ 1 +
+
+ + + + + +
diff --git a/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp b/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp new file mode 100644 index 000000000..afd71d8b1 --- /dev/null +++ b/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp @@ -0,0 +1,794 @@ +/******************************************************************************* + * retroshare-gui/src/gui/gxschannels/GxsChannelPostsModel.cpp * + * * + * Copyright 2020 by Cyril Soler * + * * + * 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 "retroshare/rsgxsflags.h" +#include "retroshare/rsexpr.h" + +#include "gui/common/FilesDefs.h" +#include "util/qtthreadsutils.h" +#include "util/HandleRichText.h" +#include "util/DateTime.h" + +#include "PostedPostsModel.h" + +//#define DEBUG_CHANNEL_MODEL + +Q_DECLARE_METATYPE(RsMsgMetaData) +Q_DECLARE_METATYPE(RsPostedPost) + +const uint32_t RsPostedPostsModel::DEFAULT_DISPLAYED_NB_POSTS = 10; + +std::ostream& operator<<(std::ostream& o, const QModelIndex& i);// defined elsewhere + +RsPostedPostsModel::RsPostedPostsModel(QObject *parent) + : QAbstractItemModel(parent), mTreeMode(TREE_MODE_PLAIN) +{ + initEmptyHierarchy(); + + mEventHandlerId = 0; + mSortingStrategy = SORT_NEW_SCORE; + + // 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_POSTED); +} + +RsPostedPostsModel::~RsPostedPostsModel() +{ + rsEvents->unregisterEventsHandler(mEventHandlerId); +} + +void RsPostedPostsModel::handleEvent_main_thread(std::shared_ptr event) +{ + const RsGxsPostedEvent *e = dynamic_cast(event.get()); + + if(!e) + return; + + switch(e->mPostedEventCode) + { + case RsPostedEventCode::UPDATED_MESSAGE: + case RsPostedEventCode::READ_STATUS_CHANGED: + case RsPostedEventCode::MESSAGE_VOTES_UPDATED: + case RsPostedEventCode::NEW_MESSAGE: + { + // Normally we should just emit dataChanged() on the index of the data that has changed: + // + // We need to update the data! + + if(e->mPostedGroupId == mPostedGroup.mMeta.mGroupId) + RsThread::async([this, e]() + { + // 1 - get message data from p3GxsChannels + + std::vector posts; + std::vector comments; + std::vector votes; + + if(!rsPosted->getBoardContent(mPostedGroup.mMeta.mGroupId,std::set{ e->mPostedMsgId }, posts,comments,votes)) + { + std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve channel message data for channel/msg " << e->mPostedGroupId << "/" << e->mPostedMsgId << std::endl; + return; + } + + // 2 - update the model in the UI thread. + + RsQThreadUtils::postToObject( [posts,comments,votes,this]() + { + for(uint32_t i=0;i 0) + return 0; + + if(mFilteredPosts.empty()) // security. Should never happen. + return 0; + + if(!parent.isValid()) + return mDisplayedNbPosts; + + RsErr() << __PRETTY_FUNCTION__ << " rowCount cannot figure out the porper number of rows." << std::endl; + return 0; +} + +int RsPostedPostsModel::columnCount(const QModelIndex &/*parent*/) const +{ + return 1; +} + +bool RsPostedPostsModel::getPostData(const QModelIndex& i,RsPostedPost& fmpe) const +{ + if(!i.isValid()) + return true; + + quintptr ref = i.internalId(); + uint32_t entry = 0; + + if(!convertRefPointerToTabEntry(ref,entry)) + return false ; + + fmpe = mPosts[mFilteredPosts[entry]]; + + return true; + +} + +bool RsPostedPostsModel::hasChildren(const QModelIndex &parent) const +{ + if(!parent.isValid()) + return true; + + return false; // by default, no post has children +} + +bool RsPostedPostsModel::convertTabEntryToRefPointer(uint32_t entry,quintptr& ref) +{ + // the pointer is formed the following way: + // + // [ 32 bits ] + // + // This means that the whole software has the following build-in limitation: + // * 4 B simultaenous posts. Should be enough ! + + ref = (intptr_t)(entry+1); + + return true; +} + +bool RsPostedPostsModel::convertRefPointerToTabEntry(quintptr ref, uint32_t& entry) +{ + intptr_t val = (intptr_t)ref; + + if(val > (1<<30)) // make sure the pointer is an int that fits in 32bits and not too big which would look suspicious + { + RsErr() << "(EE) trying to make a ChannelPostsModelIndex out of a number that is larger than 2^32-1 !" << std::endl; + return false ; + } + if(val==0) + { + RsErr() << "(EE) trying to make a ChannelPostsModelIndex out of index 0." << std::endl; + return false; + } + + entry = val - 1; + + return true; +} + +QModelIndex RsPostedPostsModel::index(int row, int column, const QModelIndex & parent) const +{ + if(row < 0 || column < 0) + return QModelIndex(); + + quintptr ref = getChildRef(parent.internalId(),row); + +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "index-3(" << row << "," << column << " parent=" << parent << ") : " << createIndex(row,column,ref) << std::endl; +#endif + return createIndex(row,column,ref) ; +} + +QModelIndex RsPostedPostsModel::parent(const QModelIndex& index) const +{ + if(!index.isValid()) + return QModelIndex(); + + return QModelIndex(); // there's no hierarchy here. So nothing to do! +} + +quintptr RsPostedPostsModel::getChildRef(quintptr ref,int index) const +{ + if (index < 0) + return 0; + + if(ref == quintptr(0)) + { + quintptr new_ref; + convertTabEntryToRefPointer(index+mDisplayedStartIndex,new_ref); + return new_ref; + } + else + return 0 ; +} + +quintptr RsPostedPostsModel::getParentRow(quintptr ref,int& row) const +{ + PostedPostsModelIndex ref_entry; + + if(!convertRefPointerToTabEntry(ref,ref_entry) || ref_entry >= mFilteredPosts.size()) + return 0 ; + + if(ref_entry == 0) + { + RsErr() << "getParentRow() shouldn't be asked for the parent of NULL" << std::endl; + row = 0; + } + else + row = ref_entry-1; + + return 0; +} + +int RsPostedPostsModel::getChildrenCount(quintptr ref) const +{ + uint32_t entry = 0 ; + + if(ref == quintptr(0)) + return rowCount()-1; + + return 0; +} + +QVariant RsPostedPostsModel::data(const QModelIndex& index, int role) const +{ +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "calling data(" << index << ") role=" << role << std::endl; +#endif + + if(!index.isValid()) + return QVariant(); + + switch(role) + { + case Qt::SizeHintRole: return sizeHintRole(index.column()) ; + case Qt::StatusTipRole:return QVariant(); + default: break; + } + + quintptr ref = (index.isValid())?index.internalId():0 ; + uint32_t entry = 0; + +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "data(" << index << ")" ; +#endif + + if(!ref) + { +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << " [empty]" << std::endl; +#endif + return QVariant() ; + } + + if(!convertRefPointerToTabEntry(ref,entry) || entry >= mFilteredPosts.size()) + { +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "Bad pointer: " << (void*)ref << std::endl; +#endif + return QVariant() ; + } + + const RsPostedPost& fmpe(mPosts[mFilteredPosts[entry]]); + + switch(role) + { + case Qt::DisplayRole: return displayRole (fmpe,index.column()) ; + case Qt::UserRole: return userRole (fmpe,index.column()) ; + default: + return QVariant(); + } +} + +QVariant RsPostedPostsModel::sizeHintRole(int col) const +{ + float factor = QFontMetricsF(QApplication::font()).height()/14.0f ; + + return QVariant( QSize(factor * 170, factor*14 )); +} + +QVariant RsPostedPostsModel::displayRole(const RsPostedPost& fmpe,int col) const +{ + switch(col) + { + default: + return QString::fromUtf8(fmpe.mMeta.mMsgName.c_str()); + } + + + return QVariant("[ERROR]"); +} + +QVariant RsPostedPostsModel::userRole(const RsPostedPost& fmpe,int col) const +{ + switch(col) + { + default: + return QVariant::fromValue(fmpe); + } +} + +const RsGxsGroupId& RsPostedPostsModel::currentGroupId() const +{ + return mPostedGroup.mMeta.mGroupId; +} + +void RsPostedPostsModel::updateBoard(const RsGxsGroupId& board_group_id) +{ + if(board_group_id.isNull()) + return; + + update_posts(board_group_id); +} + +void RsPostedPostsModel::clear() +{ + preMods(); + + initEmptyHierarchy(); + + postMods(); + emit boardPostsLoaded(); +} + +class PostSorter +{ +public: + + PostSorter(RsPostedPostsModel::SortingStrategy s) : mSortingStrategy(s) {} + + bool operator()(const RsPostedPost& p1,const RsPostedPost& p2) const + { + switch(mSortingStrategy) + { + default: + case RsPostedPostsModel::SORT_NEW_SCORE : return p1.mNewScore > p2.mNewScore; + case RsPostedPostsModel::SORT_TOP_SCORE : return p1.mTopScore > p2.mTopScore; + case RsPostedPostsModel::SORT_HOT_SCORE : return p1.mHotScore > p2.mHotScore; + } + } + +private: + RsPostedPostsModel::SortingStrategy mSortingStrategy; +}; + +Qt::ItemFlags RsPostedPostsModel::flags(const QModelIndex& index) const +{ + if (!index.isValid()) + return 0; + + return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; +} + +void RsPostedPostsModel::setSortingStrategy(RsPostedPostsModel::SortingStrategy s) +{ + preMods(); + + mSortingStrategy = s; + std::sort(mPosts.begin(),mPosts.end(), PostSorter(s)); + + postMods(); +} + +void RsPostedPostsModel::setPostsInterval(int start,int nb_posts) +{ + if(start >= mFilteredPosts.size()) + return; + + preMods(); + + uint32_t old_nb_rows = rowCount() ; + + mDisplayedNbPosts = (uint32_t)std::min(nb_posts,(int)mFilteredPosts.size() - (start+1)); + mDisplayedStartIndex = start; + + beginRemoveRows(QModelIndex(),mDisplayedNbPosts,old_nb_rows); + endRemoveRows(); + + beginInsertRows(QModelIndex(),old_nb_rows,mDisplayedNbPosts); + endInsertRows(); + + postMods(); +} + +void RsPostedPostsModel::deepUpdate() +{ + auto posts(mPosts); + setPosts(mPostedGroup,posts); +} + +void RsPostedPostsModel::setPosts(const RsPostedGroup& group, std::vector& posts) +{ + preMods(); + + beginRemoveRows(QModelIndex(),0,rowCount()-1); + endRemoveRows(); + + mPosts.clear(); + mPostedGroup = group; + + createPostsArray(posts); + + std::sort(mPosts.begin(),mPosts.end(), PostSorter(mSortingStrategy)); + + uint32_t tmpval; + setFilter(QStringList(),tmpval); + + mDisplayedNbPosts = std::min((uint32_t)mFilteredPosts.size(),DEFAULT_DISPLAYED_NB_POSTS); + mDisplayedStartIndex = 0; + + beginInsertRows(QModelIndex(),0,rowCount()-1); + endInsertRows(); + + postMods(); + + emit boardPostsLoaded(); +} + +void RsPostedPostsModel::update_posts(const RsGxsGroupId& group_id) +{ + if(group_id.isNull()) + return; + + RsThread::async([this, group_id]() + { + // 1 - get message data from p3GxsChannels + + std::list groupIds; + std::vector msg_metas; + std::vector groups; + + groupIds.push_back(group_id); + + if(!rsPosted->getBoardsInfo(groupIds,groups) || groups.size() != 1) + { + std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve channel group info for channel " << group_id << std::endl; + return; + } + + RsPostedGroup group = groups[0]; + + // We use the heap because the arrays need to be stored accross async + + std::vector *posts = new std::vector(); + std::vector *comments = new std::vector(); + std::vector *votes = new std::vector(); + + if(!rsPosted->getBoardAllContent(group_id, *posts,*comments,*votes)) + { + std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve channel messages for channel " << group_id << std::endl; + return; + } + + // 2 - update the model in the UI thread. + + RsQThreadUtils::postToObject( [group,posts,comments,votes,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, note that + * Qt::QueuedConnection is important! + */ + + setPosts(group,*posts) ; + + delete posts; + delete comments; + delete votes; + + }, this ); + + }); +} + +static bool decreasing_time_comp(const std::pair& e1,const std::pair& e2) { return e2.first < e1.first ; } + +void RsPostedPostsModel::createPostsArray(std::vector& posts) +{ +#ifdef TODO + // Collect new versions of posts if any. For now Boards do not have versions, but if that ever happens, we'll be handlign it already. This code + // is a blind copy of the channel code. + +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "Inserting channel posts" << std::endl; +#endif + + std::vector new_versions ; + for (uint32_t i=0;i search_map ; + for (uint32_t i=0;i versions ; + std::map::const_iterator vit ; + + while(search_map.end() != (vit=search_map.find(posts[current_index].mMeta.mOrigMsgId))) + { +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << " post at index " << current_index << " replaces a post at position " << vit->second ; +#endif + + // Now replace the post only if the new versionis more recent. It may happen indeed that the same post has been corrected multiple + // times. In this case, we only need to replace the post with the newest version + + //uint32_t prev_index = current_index ; + current_index = vit->second ; + + if(posts[current_index].mMeta.mMsgId.isNull()) // This handles the branching situation where this post has been already erased. No need to go down further. + { +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << " already erased. Stopping." << std::endl; +#endif + break ; + } + + if(posts[current_index].mMeta.mPublishTs < posts[source_index].mMeta.mPublishTs) + { +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << " and is more recent => following" << std::endl; +#endif + for(std::set::const_iterator itt(posts[current_index].mOlderVersions.begin());itt!=posts[current_index].mOlderVersions.end();++itt) + posts[source_index].mOlderVersions.insert(*itt); + + posts[source_index].mOlderVersions.insert(posts[current_index].mMeta.mMsgId); + posts[current_index].mMeta.mMsgId.clear(); // clear the msg Id so the post will be ignored + } +#ifdef DEBUG_CHANNEL_MODEL + else + std::cerr << " but is older -> Stopping" << std::endl; +#endif + } + } + } +#endif + +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << "Now adding " << posts.size() << " posts into array structure..." << std::endl; +#endif + + mPosts.clear(); + + for (std::vector::const_reverse_iterator it = posts.rbegin(); it != posts.rend(); ++it) + { + if(!(*it).mMeta.mMsgId.isNull()) + { +#ifdef DEBUG_CHANNEL_MODEL + std::cerr << " adding post \"" << (*it).mMeta.mMsgName << "\"" << std::endl; +#endif + mPosts.push_back(*it); + } +#ifdef DEBUG_CHANNEL_MODEL + else + std::cerr << " skipped older version post \"" << (*it).mMeta.mMsgName << "\"" << std::endl; +#endif + } +} + +void RsPostedPostsModel::setAllMsgReadStatus(bool read) +{ + // make a temporary listof pairs + + std::list pairs; + + for(uint32_t i=0;isetPostReadStatus(p,read); + } ); +} +void RsPostedPostsModel::setMsgReadStatus(const QModelIndex& i,bool read_status) +{ + if(!i.isValid()) + return ; + + // no need to call preMods()/postMods() here because we'renot changing the model + + quintptr ref = i.internalId(); + uint32_t entry = 0; + + if(!convertRefPointerToTabEntry(ref,entry) || entry >= mFilteredPosts.size()) + return ; + + RsGxsGroupId grpId = mPosts[entry].mMeta.mGroupId; + RsGxsMessageId msgId = mPosts[entry].mMeta.mMsgId; + + bool current_read_status = !(IS_MSG_UNREAD(mPosts[entry].mMeta.mMsgStatus) || IS_MSG_NEW(mPosts[entry].mMeta.mMsgStatus)); + + if(current_read_status != read_status) + RsThread::async([msgId,grpId,read_status]() + { + // Call blocking API + + rsPosted->setPostReadStatus(RsGxsGrpMsgIdPair(grpId,msgId),read_status); + } ); +} + +QModelIndex RsPostedPostsModel::getIndexOfMessage(const RsGxsMessageId& mid) const +{ + // Brutal search. This is not so nice, so dont call that in a loop! If too costly, we'll use a map. + + RsGxsMessageId postId = mid; + + for(uint32_t i=mDisplayedStartIndex;i * + * * + * 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 "retroshare/rsposted.h" +#include "retroshare/rsgxsifacetypes.h" +#include "retroshare/rsevents.h" + +#include +#include + +// This class holds the actual hierarchy of posts, represented by identifiers +// It is responsible for auto-updating when necessary and holds a mutex to allow the Model to +// safely access the data. + +// The model contains a post in place 0 that is the parent of all posts. + +// The model contains 3 layers: +// +// Layer 1: list of all posts +// +// * this list is sorted according to the current sorting strategy +// * Variables: mPosts +// +// Layer 2: list of post filtered by search +// +// * depending on which chunk of posts are actually displayed, this list contains +// the subset of the general list of posts +// * Variables: mFilteredPosts +// +// Layer 3: start and end of posts actually displayed in the previous list +// +// * Variables: mDisplayedStartIndex, mDisplayedNbPosts +// +// The array below indicates which variables are updated depending on the type of data/view change: +// +// | Global list (mPosts) | Filtered List | Displayed list (mDisplayedStartIndex, mDisplayedNbPosts) +// -----------+-----------------------+------------------+---------------------------------------------------------- +// New group | X | X | X (updated because FilteredList may change) +// Sort order | X | | +// Filter Str | | X | X (updated because FilteredList may change) +// Chunk chng | | X | X +// +// In the model, indexes internal refs are pointer casts of the index in the mFilteredPosts tab. Another possible choice +// was to use indexes in the tab of displayed indices, but this leads to a more complex impleemntation. + +typedef uint32_t PostedPostsModelIndex; + +// struct ChannelPostsModelPostEntry +// { +// ChannelPostsModelPostEntry() : mPublishTs(0),mPostFlags(0),mMsgStatus(0),prow(0) {} +// +// enum { // flags for display of posts. To be used in mPostFlags +// FLAG_POST_IS_PINNED = 0x0001, +// FLAG_POST_IS_MISSING = 0x0002, +// FLAG_POST_IS_REDACTED = 0x0004, +// FLAG_POST_HAS_UNREAD_CHILDREN = 0x0008, +// FLAG_POST_HAS_READ_CHILDREN = 0x0010, +// FLAG_POST_PASSES_FILTER = 0x0020, +// FLAG_POST_CHILDREN_PASSES_FILTER = 0x0040, +// }; +// +// std::string mTitle ; +// RsGxsMessageId mMsgId; +// uint32_t mPublishTs; +// uint32_t mPostFlags; +// int mMsgStatus; +// +// int prow ;// parent row, which basically means position in the array of posts +// }; + +// This class is the item model used by Qt to display the information + +class RsPostedPostsModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit RsPostedPostsModel(QObject *parent = NULL); + virtual ~RsPostedPostsModel() override; + + static const uint32_t COLUMN_THREAD_NB_COLUMNS = 0x01; + static const uint32_t DEFAULT_DISPLAYED_NB_POSTS ; + + enum SortingStrategy { + SORT_UNKNOWN = 0x00, + SORT_NEW_SCORE = 0x01, + SORT_TOP_SCORE = 0x02, + SORT_HOT_SCORE = 0x03 + }; + + enum Columns { + COLUMN_POSTS =0x00, + COLUMN_THREAD_MSGID =0x01, + COLUMN_THREAD_DATA =0x02, + }; + + enum Roles{ SortRole = Qt::UserRole+1, + StatusRole = Qt::UserRole+2, + FilterRole = Qt::UserRole+3, + }; + + enum TreeMode{ TREE_MODE_UNKWN = 0x00, + TREE_MODE_PLAIN = 0x01, + TREE_MODE_FILES = 0x02, + }; + +#ifdef TODO + enum SortMode{ SORT_MODE_PUBLISH_TS = 0x00, + SORT_MODE_CHILDREN_PUBLISH_TS = 0x01, + }; +#endif + + QModelIndex root() const{ return createIndex(0,0,(void*)NULL) ;} + QModelIndex getIndexOfMessage(const RsGxsMessageId& mid) const; + + // This method will asynchroneously update the data + + void updateBoard(const RsGxsGroupId& posted_group_id); + const RsGxsGroupId& currentGroupId() const; + + // Triggers a data change for all items. This can be used to redraw the view without re-loading the data. + void update(); + + // Triggers a preMod, begin/end remove rows and data update. Could be useful if update is not enough to reset cell sizes. + void deepUpdate(); + + // same without data update, which is far less costly + void triggerRedraw(); + +#ifdef TODO + void setSortMode(SortMode mode) ; + + void setTextColorRead (QColor color) { mTextColorRead = color;} + void setTextColorUnread (QColor color) { mTextColorUnread = color;} + void setTextColorUnreadChildren(QColor color) { mTextColorUnreadChildren = color;} + void setTextColorNotSubscribed (QColor color) { mTextColorNotSubscribed = color;} + void setTextColorMissing (QColor color) { mTextColorMissing = color;} +#endif + + void setAllMsgReadStatus(bool read); + void setMsgReadStatus(const QModelIndex &i, bool read_status); + void setFilter(const QStringList &strings, uint32_t &count) ; + void setSortingStrategy(SortingStrategy s); + void setPostsInterval(int start,int nb_posts); + +#ifdef TODO + void setAuthorOpinion(const QModelIndex& indx,RsOpinion op); +#endif + + // Helper functions + + bool getPostData(const QModelIndex& i,RsPostedPost& fmpe) const ; + uint32_t totalPostsCount() const { return mPosts.size() ; } + uint32_t filteredPostsCount() const { return mFilteredPosts.size() ; } + uint32_t displayedStartPostIndex() const { return mDisplayedStartIndex ; } + void clear() ; + + // AbstractItemModel functions. + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& child) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + // Custom item roles + + QVariant sizeHintRole (int col) const; + QVariant displayRole (const RsPostedPost& fmpe, int col) const; + QVariant toolTipRole (const RsPostedPost& fmpe, int col) const; + QVariant userRole (const RsPostedPost& fmpe, int col) const; +#ifdef TODO + QVariant decorationRole(const RsPostedPost& fmpe, int col) const; + QVariant pinnedRole (const RsPostedPost& fmpe, int col) const; + QVariant missingRole (const RsPostedPost& fmpe, int col) const; + QVariant statusRole (const RsPostedPost& fmpe, int col) const; + QVariant authorRole (const RsPostedPost& fmpe, int col) const; + QVariant sortRole (const RsPostedPost& fmpe, int col) const; + QVariant fontRole (const RsPostedPost& fmpe, int col) const; + QVariant filterRole (const RsPostedPost& fmpe, int col) const; + QVariant textColorRole (const RsPostedPost& fmpe, int col) const; + QVariant backgroundRole(const RsPostedPost& fmpe, int col) const; +#endif + + /*! + * \brief debug_dump + * Dumps the hierarchy of posts in the terminal, to allow checking whether the internal representation is correct. + */ + void debug_dump(); + +signals: + void boardPostsLoaded(); // emitted after the posts have been loaded. + +private: + RsPostedGroup mPostedGroup; + + TreeMode mTreeMode; + + void preMods() ; + void postMods() ; + + quintptr getParentRow(quintptr ref,int& row) const; + quintptr getChildRef(quintptr ref, int index) const; + int getChildrenCount(quintptr ref) const; + + static bool convertTabEntryToRefPointer(uint32_t entry, quintptr &ref); + static bool convertRefPointerToTabEntry(quintptr ref,uint32_t& entry); + static void computeReputationLevel(uint32_t forum_sign_flags, RsPostedPost& entry); + + void update_posts(const RsGxsGroupId& group_id); + +#ifdef TODO + void setForumMessageSummary(const std::vector& messages); + void recursUpdateReadStatusAndTimes(ChannelPostsModelIndex i,bool& has_unread_below,bool& has_read_below); + uint32_t recursUpdateFilterStatus(ChannelPostsModelIndex i,int column,const QStringList& strings); + void recursSetMsgReadStatus(ChannelPostsModelIndex i,bool read_status,bool with_children); +#endif + + void createPostsArray(std::vector &posts); + void setPosts(const RsPostedGroup& group, std::vector &posts); + void initEmptyHierarchy(); + void handleEvent_main_thread(std::shared_ptr event); + + std::vector mPosts ; + std::vector mFilteredPosts; + uint32_t mDisplayedStartIndex; + uint32_t mDisplayedNbPosts; + SortingStrategy mSortingStrategy; + + RsEventsHandlerId_t mEventHandlerId ; +}; diff --git a/retroshare-gui/src/gui/Posted/Posted_images.qrc b/retroshare-gui/src/gui/Posted/Posted_images.qrc index 0710a04e6..72cb3c40c 100644 --- a/retroshare-gui/src/gui/Posted/Posted_images.qrc +++ b/retroshare-gui/src/gui/Posted/Posted_images.qrc @@ -5,6 +5,8 @@ images/down-arrow.png images/up-arrow.png images/comments.png + images/comments_blue.png + images/thumb-blocked.png images/thumb-default.png images/thumb-link.png images/share.png diff --git a/retroshare-gui/src/gui/Posted/images/comments_blue.png b/retroshare-gui/src/gui/Posted/images/comments_blue.png new file mode 100644 index 000000000..07c15a1a0 Binary files /dev/null and b/retroshare-gui/src/gui/Posted/images/comments_blue.png differ diff --git a/retroshare-gui/src/gui/Posted/images/thumb-blocked.png b/retroshare-gui/src/gui/Posted/images/thumb-blocked.png new file mode 100644 index 000000000..3ff44d7fa Binary files /dev/null and b/retroshare-gui/src/gui/Posted/images/thumb-blocked.png differ diff --git a/retroshare-gui/src/gui/TheWire/PulseAddDialog.cpp b/retroshare-gui/src/gui/TheWire/PulseAddDialog.cpp index 0bdd3c338..b7e8b6054 100644 --- a/retroshare-gui/src/gui/TheWire/PulseAddDialog.cpp +++ b/retroshare-gui/src/gui/TheWire/PulseAddDialog.cpp @@ -22,6 +22,7 @@ #include #include "PulseReply.h" +#include "gui/common/FilesDefs.h" #include "PulseAddDialog.h" diff --git a/retroshare-gui/src/gui/TheWire/PulseTopLevel.cpp b/retroshare-gui/src/gui/TheWire/PulseTopLevel.cpp index c8e26d987..e8c830a0b 100644 --- a/retroshare-gui/src/gui/TheWire/PulseTopLevel.cpp +++ b/retroshare-gui/src/gui/TheWire/PulseTopLevel.cpp @@ -24,6 +24,7 @@ #include #include "PulseTopLevel.h" +#include "gui/common/FilesDefs.h" #include #include diff --git a/retroshare-gui/src/gui/TheWire/PulseViewGroup.cpp b/retroshare-gui/src/gui/TheWire/PulseViewGroup.cpp index 27b085a01..547fabbab 100644 --- a/retroshare-gui/src/gui/TheWire/PulseViewGroup.cpp +++ b/retroshare-gui/src/gui/TheWire/PulseViewGroup.cpp @@ -26,6 +26,7 @@ #include "PulseViewGroup.h" #include "gui/gxs/GxsIdDetails.h" +#include "gui/common/FilesDefs.h" #include "util/DateTime.h" /** Constructor */ diff --git a/retroshare-gui/src/gui/TheWire/PulseViewItem.cpp b/retroshare-gui/src/gui/TheWire/PulseViewItem.cpp index 2025baf85..5a309f94b 100644 --- a/retroshare-gui/src/gui/TheWire/PulseViewItem.cpp +++ b/retroshare-gui/src/gui/TheWire/PulseViewItem.cpp @@ -26,6 +26,7 @@ #include "PulseViewItem.h" #include "gui/gxs/GxsIdDetails.h" +#include "gui/common/FilesDefs.h" #include "util/DateTime.h" /** Constructor */ diff --git a/retroshare-gui/src/gui/TheWire/WireGroupDialog.cpp b/retroshare-gui/src/gui/TheWire/WireGroupDialog.cpp index f29aee712..43303cecf 100644 --- a/retroshare-gui/src/gui/TheWire/WireGroupDialog.cpp +++ b/retroshare-gui/src/gui/TheWire/WireGroupDialog.cpp @@ -22,6 +22,7 @@ #include "WireGroupExtra.h" #include "WireGroupDialog.h" +#include "gui/common/FilesDefs.h" #include "gui/gxs/GxsIdDetails.h" #include diff --git a/retroshare-gui/src/gui/TheWire/WireGroupItem.cpp b/retroshare-gui/src/gui/TheWire/WireGroupItem.cpp index 81df64dd2..aa4f63696 100644 --- a/retroshare-gui/src/gui/TheWire/WireGroupItem.cpp +++ b/retroshare-gui/src/gui/TheWire/WireGroupItem.cpp @@ -25,6 +25,7 @@ #include "WireGroupItem.h" #include "gui/gxs/GxsIdDetails.h" +#include "gui/common/FilesDefs.h" #include #include diff --git a/retroshare-gui/src/gui/gxs/GxsCommentContainer.cpp b/retroshare-gui/src/gui/gxs/GxsCommentContainer.cpp index c1590fba4..b80539cea 100644 --- a/retroshare-gui/src/gui/gxs/GxsCommentContainer.cpp +++ b/retroshare-gui/src/gui/gxs/GxsCommentContainer.cpp @@ -60,7 +60,7 @@ void GxsCommentContainer::commentLoad(const RsGxsGroupId &grpId, const std::set< comments += "..."; } - GxsCommentDialog *commentDialog = new GxsCommentDialog(this, getTokenService(), getCommentService()); + GxsCommentDialog *commentDialog = new GxsCommentDialog(this, RsGxsId(),getTokenService(), getCommentService()); QWidget *commentHeader = createHeaderWidget(grpId, msgId); commentDialog->setCommentHeader(commentHeader); diff --git a/retroshare-gui/src/gui/gxs/GxsCommentDialog.cpp b/retroshare-gui/src/gui/gxs/GxsCommentDialog.cpp index 43dc43d13..23fc18654 100644 --- a/retroshare-gui/src/gui/gxs/GxsCommentDialog.cpp +++ b/retroshare-gui/src/gui/gxs/GxsCommentDialog.cpp @@ -30,24 +30,24 @@ #include /** Constructor */ -GxsCommentDialog::GxsCommentDialog(QWidget *parent, RsTokenService *token_service, RsGxsCommentService *comment_service) +GxsCommentDialog::GxsCommentDialog(QWidget *parent, const RsGxsId &default_author, RsTokenService *token_service, RsGxsCommentService *comment_service) : QWidget(parent), ui(new Ui::GxsCommentDialog) { /* Invoke the Qt Designer generated QObject setup routine */ ui->setupUi(this); setTokenService(token_service,comment_service); - init(); + init(default_author); } -void GxsCommentDialog::init() +void GxsCommentDialog::init(const RsGxsId& default_author) { /* Set header resize modes and initial section sizes */ QHeaderView * ttheader = ui->treeWidget->header () ; ttheader->resizeSection (0, 440); /* fill in the available OwnIds for signing */ - ui->idChooser->loadIds(IDCHOOSER_ID_REQUIRED, RsGxsId()); + ui->idChooser->loadIds(IDCHOOSER_ID_REQUIRED, default_author); connect(ui->refreshButton, SIGNAL(clicked()), this, SLOT(refresh())); connect(ui->idChooser, SIGNAL(currentIndexChanged( int )), this, SLOT(voterSelectionChanged( int ))); @@ -70,13 +70,13 @@ void GxsCommentDialog::setTokenService(RsTokenService *token_service, RsGxsComme ui->treeWidget->setup(token_service, comment_service); } -GxsCommentDialog::GxsCommentDialog(QWidget *parent) +GxsCommentDialog::GxsCommentDialog(QWidget *parent,const RsGxsId &default_author) : QWidget(parent), ui(new Ui::GxsCommentDialog) { /* Invoke the Qt Designer generated QObject setup routine */ ui->setupUi(this); - init(); + init(default_author); } GxsCommentDialog::~GxsCommentDialog() @@ -84,7 +84,7 @@ GxsCommentDialog::~GxsCommentDialog() delete(ui); } -void GxsCommentDialog::commentLoad(const RsGxsGroupId &grpId, const std::set& msg_versions,const RsGxsMessageId& most_recent_msgId) +void GxsCommentDialog::commentLoad(const RsGxsGroupId &grpId, const std::set& msg_versions,const RsGxsMessageId& most_recent_msgId,bool use_cache) { std::cerr << "GxsCommentDialog::commentLoad(" << grpId << ", most recent msg version: " << most_recent_msgId << ")"; std::cerr << std::endl; @@ -93,7 +93,8 @@ void GxsCommentDialog::commentLoad(const RsGxsGroupId &grpId, const std::settreeWidget->requestComments(mGrpId,msg_versions,most_recent_msgId); + ui->treeWidget->setUseCache(use_cache); + ui->treeWidget->requestComments(mGrpId,msg_versions,most_recent_msgId); } void GxsCommentDialog::notifyCommentsLoaded(int n) @@ -151,10 +152,10 @@ void GxsCommentDialog::setCommentHeader(QWidget *header) //header->setParent(ui->postFrame); //ui->postFrame->setVisible(true); - QLayout *alayout = ui->postFrame->layout(); +#if 0 + QLayout *alayout = ui->postFrame->layout(); alayout->addWidget(header); -#if 0 ui->postFrame->setVisible(true); QDateTime qtime; diff --git a/retroshare-gui/src/gui/gxs/GxsCommentDialog.h b/retroshare-gui/src/gui/gxs/GxsCommentDialog.h index 00982fdbb..987fd0c07 100644 --- a/retroshare-gui/src/gui/gxs/GxsCommentDialog.h +++ b/retroshare-gui/src/gui/gxs/GxsCommentDialog.h @@ -32,19 +32,21 @@ class GxsCommentDialog: public QWidget Q_OBJECT public: - GxsCommentDialog(QWidget *parent); - GxsCommentDialog(QWidget *parent, RsTokenService *token_service, RsGxsCommentService *comment_service); + GxsCommentDialog(QWidget *parent=nullptr,const RsGxsId& default_author=RsGxsId()); + GxsCommentDialog(QWidget *parent,const RsGxsId& default_author, RsTokenService *token_service, RsGxsCommentService *comment_service); virtual ~GxsCommentDialog(); void setTokenService(RsTokenService *token_service, RsGxsCommentService *comment_service); void setCommentHeader(QWidget *header); - void commentLoad(const RsGxsGroupId &grpId, const std::set &msg_versions, const RsGxsMessageId &most_recent_msgId); + void commentLoad(const RsGxsGroupId &grpId, const std::set &msg_versions, const RsGxsMessageId &most_recent_msgId, bool use_cache=false); RsGxsGroupId groupId() { return mGrpId; } RsGxsMessageId messageId() { return mMostRecentMsgId; } +public slots: + void refresh(); + private slots: - void refresh(); void idChooserReady(); void voterSelectionChanged( int index ); void sortComments(int); @@ -54,7 +56,7 @@ signals: void commentsLoaded(int); private: - void init(); + void init(const RsGxsId &default_author); RsGxsGroupId mGrpId; RsGxsMessageId mMostRecentMsgId; diff --git a/retroshare-gui/src/gui/gxs/GxsCommentDialog.ui b/retroshare-gui/src/gui/gxs/GxsCommentDialog.ui index c349c6eb7..734e490ca 100644 --- a/retroshare-gui/src/gui/gxs/GxsCommentDialog.ui +++ b/retroshare-gui/src/gui/gxs/GxsCommentDialog.ui @@ -7,38 +7,68 @@ 0 0 632 - 547 + 248
+ + + 0 + 0 + + Form - - + + - + 0 0 - - - 1 + + true + + + + Comment - - 1 + + + + Author - - 1 + + + + Date - - 1 + + + + Score - + + + + UpVotes + + + + + DownVotes + + + + + OwnVote + + - + @@ -141,48 +171,6 @@ - - - - true - - - - Comment - - - - - Author - - - - - Date - - - - - Score - - - - - UpVotes - - - - - DownVotes - - - - - OwnVote - - - -
diff --git a/retroshare-gui/src/gui/gxs/GxsCommentTreeWidget.cpp b/retroshare-gui/src/gui/gxs/GxsCommentTreeWidget.cpp index cc2087044..fcc13e851 100644 --- a/retroshare-gui/src/gui/gxs/GxsCommentTreeWidget.cpp +++ b/retroshare-gui/src/gui/gxs/GxsCommentTreeWidget.cpp @@ -64,6 +64,9 @@ #define IMAGE_VOTEUP ":/images/vote_up.png" #define IMAGE_VOTEDOWN ":/images/vote_down.png" +std::map > GxsCommentTreeWidget::mCommentsCache; +QMutex GxsCommentTreeWidget::mCacheMutex; + // This class allows to draw the item using an appropriate size class MultiLinesCommentDelegate: public QStyledItemDelegate @@ -148,6 +151,8 @@ GxsCommentTreeWidget::GxsCommentTreeWidget(QWidget *parent) commentsRole = new RSTreeWidgetItemCompareRole; commentsRole->setRole(PCITEM_COLUMN_DATE, ROLE_SORT); + mUseCache = false; + // QFont font = QFont("ARIAL", 10); // font.setBold(true); @@ -318,7 +323,7 @@ void GxsCommentTreeWidget::banUser() void GxsCommentTreeWidget::makeComment() { - GxsCreateCommentDialog pcc(mCommentService, std::make_pair(mGroupId,mLatestMsgId), mLatestMsgId, this); + GxsCreateCommentDialog pcc(mCommentService, std::make_pair(mGroupId,mLatestMsgId), mLatestMsgId, mVoterId,this); pcc.exec(); } @@ -327,7 +332,7 @@ void GxsCommentTreeWidget::replyToComment() RsGxsGrpMsgIdPair msgId; msgId.first = mGroupId; msgId.second = mCurrentCommentMsgId; - GxsCreateCommentDialog pcc(mCommentService, msgId, mLatestMsgId, this); + GxsCreateCommentDialog pcc(mCommentService, msgId, mLatestMsgId, mVoterId,this); pcc.loadComment(mCurrentCommentText, mCurrentCommentAuthor, mCurrentCommentAuthorId); pcc.exec(); @@ -362,6 +367,19 @@ void GxsCommentTreeWidget::requestComments(const RsGxsGroupId& group, const std: mMsgVersions = message_versions ; mLatestMsgId = most_recent_message; + if(mUseCache) + { + QMutexLocker lock(&mCacheMutex); + + auto it = mCommentsCache.find(most_recent_message); + + if(it != mCommentsCache.end()) + { + std::cerr << "Got " << it->second.size() << " comments from cache." << std::endl; + insertComments(it->second); + completeItems(); + } + } service_requestComments(group,message_versions); } @@ -581,11 +599,28 @@ void GxsCommentTreeWidget::service_loadThread(const uint32_t &token) std::vector comments; mCommentService->getRelatedComments(token, comments); - std::vector::iterator vit; + // This is inconsistent since we cannot know here that all comments are for the same thread. However they are only + // requested in requestComments() where a single MsgId is used. - for(vit = comments.begin(); vit != comments.end(); ++vit) + if(mUseCache) + { + QMutexLocker lock(&mCacheMutex); + + if(!comments.empty()) + { + std::cerr << "Updating cache with " << comments.size() << " for thread " << comments[0].mMeta.mThreadId << std::endl; + mCommentsCache[comments[0].mMeta.mThreadId] = comments; + } + } + + insertComments(comments); +} + +void GxsCommentTreeWidget::insertComments(const std::vector& comments) +{ + for(auto vit = comments.begin(); vit != comments.end(); ++vit) { - RsGxsComment &comment = *vit; + const RsGxsComment &comment = *vit; /* convert to a QTreeWidgetItem */ std::cerr << "GxsCommentTreeWidget::service_loadThread() Got Comment: " << comment.mMeta.mMsgId; std::cerr << std::endl; @@ -636,8 +671,6 @@ void GxsCommentTreeWidget::service_loadThread(const uint32_t &token) addItem(comment.mMeta.mMsgId, comment.mMeta.mParentId, item); } - - return; } QTreeWidgetItem *GxsCommentTreeWidget::service_createMissingItem(const RsGxsMessageId& parent) diff --git a/retroshare-gui/src/gui/gxs/GxsCommentTreeWidget.h b/retroshare-gui/src/gui/gxs/GxsCommentTreeWidget.h index cdf6063af..7aef13337 100644 --- a/retroshare-gui/src/gui/gxs/GxsCommentTreeWidget.h +++ b/retroshare-gui/src/gui/gxs/GxsCommentTreeWidget.h @@ -22,6 +22,7 @@ #define _GXS_COMMENT_TREE_WIDGET_H #include +#include #include "util/TokenQueue.h" #include @@ -45,6 +46,7 @@ public: void loadRequest(const TokenQueue *queue, const TokenRequest &req); void setVoteId(const RsGxsId &voterId); + void setUseCache(bool b) { mUseCache = b ;} protected: /* to be overloaded */ @@ -60,8 +62,8 @@ protected: void loadThread(const uint32_t &token); + void insertComments(const std::vector& comments); void addItem(RsGxsMessageId itemId, RsGxsMessageId parentId, QTreeWidgetItem *item); - public slots: void customPopUpMenu(const QPoint& point); void setCurrentCommentMsgId(QTreeWidgetItem* current, QTreeWidgetItem* previous); @@ -108,6 +110,9 @@ protected: RsTokenService *mRsTokenService; RsGxsCommentService *mCommentService; + bool mUseCache; + static std::map > mCommentsCache; + static QMutex mCacheMutex; }; diff --git a/retroshare-gui/src/gui/gxs/GxsCreateCommentDialog.cpp b/retroshare-gui/src/gui/gxs/GxsCreateCommentDialog.cpp index dd3f2f97c..e084635c3 100644 --- a/retroshare-gui/src/gui/gxs/GxsCreateCommentDialog.cpp +++ b/retroshare-gui/src/gui/gxs/GxsCreateCommentDialog.cpp @@ -26,7 +26,7 @@ #include #include -GxsCreateCommentDialog::GxsCreateCommentDialog(RsGxsCommentService *service, const RsGxsGrpMsgIdPair &parentId, const RsGxsMessageId& threadId, QWidget *parent) : +GxsCreateCommentDialog::GxsCreateCommentDialog(RsGxsCommentService *service, const RsGxsGrpMsgIdPair &parentId, const RsGxsMessageId& threadId, const RsGxsId& default_author,QWidget *parent) : QDialog(parent), ui(new Ui::GxsCreateCommentDialog), mCommentService(service), mParentId(parentId), mThreadId(threadId) { @@ -35,7 +35,7 @@ GxsCreateCommentDialog::GxsCreateCommentDialog(RsGxsCommentService *service, co connect(ui->buttonBox, SIGNAL(rejected()), this, SLOT(close())); /* fill in the available OwnIds for signing */ - ui->idChooser->loadIds(IDCHOOSER_ID_REQUIRED, RsGxsId()); + ui->idChooser->loadIds(IDCHOOSER_ID_REQUIRED, default_author); } void GxsCreateCommentDialog::loadComment(const QString &msgText, const QString &msgAuthor, const RsGxsId &msgAuthorId) diff --git a/retroshare-gui/src/gui/gxs/GxsCreateCommentDialog.h b/retroshare-gui/src/gui/gxs/GxsCreateCommentDialog.h index 967649179..ce394bf5e 100644 --- a/retroshare-gui/src/gui/gxs/GxsCreateCommentDialog.h +++ b/retroshare-gui/src/gui/gxs/GxsCreateCommentDialog.h @@ -36,7 +36,7 @@ class GxsCreateCommentDialog : public QDialog Q_OBJECT public: - explicit GxsCreateCommentDialog(RsGxsCommentService *service, const RsGxsGrpMsgIdPair& parentId, const RsGxsMessageId& threadId, QWidget *parent = 0); + explicit GxsCreateCommentDialog(RsGxsCommentService *service, const RsGxsGrpMsgIdPair& parentId, const RsGxsMessageId& threadId, const RsGxsId& default_author=RsGxsId(),QWidget *parent = 0); ~GxsCreateCommentDialog(); void loadComment(const QString &msgText, const QString &msgAuthor, const RsGxsId &msgAuthorId); diff --git a/retroshare-gui/src/gui/gxs/GxsGroupFrameDialog.cpp b/retroshare-gui/src/gui/gxs/GxsGroupFrameDialog.cpp index 556d31760..14c903d56 100644 --- a/retroshare-gui/src/gui/gxs/GxsGroupFrameDialog.cpp +++ b/retroshare-gui/src/gui/gxs/GxsGroupFrameDialog.cpp @@ -731,7 +731,7 @@ void GxsGroupFrameDialog::loadComment(const RsGxsGroupId &grpId, const QVectorgetTokenService(), commentService); + commentDialog = new GxsCommentDialog(this,RsGxsId(), mInterface->getTokenService(), commentService); QWidget *commentHeader = createCommentHeaderWidget(grpId, most_recent_msgId); if (commentHeader) { diff --git a/retroshare-gui/src/gui/gxs/GxsIdChooser.cpp b/retroshare-gui/src/gui/gxs/GxsIdChooser.cpp index 6235c025f..9321c4de4 100644 --- a/retroshare-gui/src/gui/gxs/GxsIdChooser.cpp +++ b/retroshare-gui/src/gui/gxs/GxsIdChooser.cpp @@ -313,7 +313,7 @@ void GxsIdChooser::setDefaultItem() } if (def >= 0) { - setCurrentIndex(def); + whileBlocking(this)->setCurrentIndex(def); #ifdef IDCHOOSER_DEBUG std::cerr << "GxsIdChooser-002" << (void*)this << " setting current index to " << def << std::endl; #endif diff --git a/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.cpp b/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.cpp index 087e29300..f7f8ed726 100644 --- a/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.cpp +++ b/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.cpp @@ -65,10 +65,8 @@ CreateGxsChannelMsg::CreateGxsChannelMsg(const RsGxsGroupId &cId, RsGxsMessageId setAttribute ( Qt::WA_DeleteOnClose, true ); - buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Post")); - - connect(buttonBox, SIGNAL(accepted()), this, SLOT(sendMsg())); - connect(buttonBox, SIGNAL(rejected()), this, SLOT(cancelMsg())); + connect(postButton, SIGNAL(clicked()), this, SLOT(sendMsg())); + connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelMsg())); connect(addFileButton, SIGNAL(clicked() ), this , SLOT(addExtraFile())); connect(addfilepushButton, SIGNAL(clicked() ), this , SLOT(addExtraFile())); @@ -599,8 +597,8 @@ void CreateGxsChannelMsg::checkAttachmentReady() * recognized by librs but not correctly by gui (can't * formally remove it) */ - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + postButton->setEnabled(false); + cancelButton->setEnabled(false); break; } } @@ -608,8 +606,8 @@ void CreateGxsChannelMsg::checkAttachmentReady() if (fit == mAttachments.end()) { - buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true); + postButton->setEnabled(true); + cancelButton->setEnabled(true); } /* repeat... */ diff --git a/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.ui b/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.ui index 9d296666c..185639054 100644 --- a/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.ui +++ b/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.ui @@ -329,7 +329,7 @@ p, li { white-space: pre-wrap; } 0 0 - 842 + 81 24 @@ -431,6 +431,9 @@ p, li { white-space: pre-wrap; } + + 9 + @@ -449,15 +452,29 @@ p, li { white-space: pre-wrap; } - - - - + Qt::Horizontal - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + 40 + 20 + + + + + + + + Post + + + + + + + Cancel diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelFilesWidget.ui b/retroshare-gui/src/gui/gxschannels/GxsChannelFilesWidget.ui deleted file mode 100644 index 4990b3259..000000000 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelFilesWidget.ui +++ /dev/null @@ -1,78 +0,0 @@ - - - GxsChannelFilesWidget - - - - 0 - 0 - 400 - 300 - - - - Form - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - false - - - false - - - true - - - true - - - - 1 - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.cpp b/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.cpp index 7c4bc62da..ea887212c 100644 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.cpp +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.cpp @@ -18,12 +18,16 @@ * * *******************************************************************************/ +#include + #include #include #include "gui/common/FilesDefs.h" #include "gui/gxschannels/GxsChannelPostThumbnail.h" +// #define DEBUG_GXSCHANNELPOSTTHUMBNAIL 1 + const float ChannelPostThumbnailView::DEFAULT_SIZE_IN_FONT_HEIGHT = 5.0; const float ChannelPostThumbnailView::FONT_SCALE_FACTOR = 1.5; @@ -50,7 +54,9 @@ void ChannelPostThumbnailView::setText(const QString& s) { if(mPostTitle == NULL) { +#ifdef DEBUG_GXSCHANNELPOSTTHUMBNAIL std::cerr << "(EE) calling setText on a ChannelPostThumbnailView without SHOW_TEXT flag!"<< std::endl; +#endif return; } @@ -246,6 +252,8 @@ void ZoomableLabel::mousePressEvent(QMouseEvent *me) mMoving = true; mLastX = me->x(); mLastY = me->y(); + + emit clicked(); } void ZoomableLabel::mouseReleaseEvent(QMouseEvent *) { @@ -292,7 +300,11 @@ QPixmap ZoomableLabel::extractCroppedScaledPicture() const void ZoomableLabel::setPicture(const QPixmap& pix) { +#ifdef DEBUG_GXSCHANNELPOSTTHUMBNAIL + std::cerr << "Setting new picture of size " << pix.width() << " x " << pix.height() << std::endl; +#endif mFullImage = pix; + setScaledContents(true); reset(); updateView(); @@ -312,7 +324,15 @@ void ZoomableLabel::updateView() // - the original center is preferred // - if the crop overlaps the image border, the center is moved. - QRect rect(mCenterX - 0.5 * width()*mZoomFactor, mCenterY - 0.5 * height()*mZoomFactor, width()*mZoomFactor, height()*mZoomFactor); + QRect rect(mCenterX - 0.5 * width()*mZoomFactor, mCenterY - 0.5 * height()*mZoomFactor, floor(width()*mZoomFactor), floor(height()*mZoomFactor)); + +#ifdef DEBUG_GXSCHANNELPOSTTHUMBNAIL + std::cerr << "Updating view: mCenterX=" << mCenterX << ", mCenterY=" << mCenterY << ", mZoomFactor=" << mZoomFactor << std::endl; + std::cerr << " Image size: " << mFullImage.width() << " x " << mFullImage.height() << ", window size: " << width() << " x " << height() << std::endl; + std::cerr << " cropped image: " << rect.left() << "," << rect.top() << "+" << rect.width() << "+" << rect.height() << std::endl; + std::cerr << " saving crop to pix2.png" << std::endl; + mFullImage.copy(rect).save("pix2.png","PNG"); +#endif QLabel::setPixmap(mFullImage.copy(rect)); } diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.h b/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.h index b231fbe0a..d89fd5eca 100644 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.h +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.h @@ -37,8 +37,10 @@ class ZoomableLabel: public QLabel { + Q_OBJECT + public: - ZoomableLabel(QWidget *parent): QLabel(parent),mZoomFactor(1.0),mCenterX(0.0),mCenterY(0.0),mZoomEnabled(true) {} + ZoomableLabel(QWidget *parent): QLabel(parent),mUseStyleSheet(true),mZoomFactor(1.0),mCenterX(0.0),mCenterY(0.0),mZoomEnabled(true) {} void setPicture(const QPixmap& pix); void setEnableZoom(bool b) { mZoomEnabled = b; } @@ -48,6 +50,9 @@ public: const QPixmap& originalImage() const { return mFullImage ; } +signals: + void clicked(); + protected: void mousePressEvent(QMouseEvent *ev) override; void mouseReleaseEvent(QMouseEvent *ev) override; @@ -55,11 +60,16 @@ protected: void resizeEvent(QResizeEvent *ev) override; void wheelEvent(QWheelEvent *me) override; + void enterEvent(QEvent * /* ev */ ) override { if(mUseStyleSheet) setStyleSheet("QLabel { border: 2px solid #039bd5; }");} + void leaveEvent(QEvent * /* ev */ ) override { if(mUseStyleSheet) setStyleSheet("QLabel { border: 2px solid #CCCCCC; border-radius: 3px; }");} + + bool mUseStyleSheet; + QPixmap mFullImage; + float mZoomFactor; float mCenterX; float mCenterY; - float mZoomFactor; int mLastX,mLastY; bool mMoving; bool mZoomEnabled; diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.cpp b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.cpp index 046678ad9..e45e333d0 100644 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.cpp +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.cpp @@ -1069,6 +1069,8 @@ void GxsChannelPostsWidgetWithModel::insertChannelDetails(const RsGxsChannelGrou RetroShareLink link = RetroShareLink::createMessage(group.mMeta.mAuthorId, ""); ui->infoAdministrator->setText(link.toHtml()); } + else + ui->infoAdministrator->setText("[No contact author]"); ui->infoCreated->setText(DateTime::formatLongDateTime(group.mMeta.mPublishTs)); @@ -1165,6 +1167,9 @@ bool GxsChannelPostsWidgetWithModel::navigate(const RsGxsMessageId& msgId) ui->postsTree->scrollTo(index);//May change if model reloaded ui->postsTree->setFocus(); + ui->channel_TW->setCurrentIndex(CHANNEL_TABS_POSTS); + ui->details_TW->setCurrentIndex(CHANNEL_TABS_DETAILS); + return true; } diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.ui b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.ui index 46683b31c..5bf481a44 100644 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.ui +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostsWidgetWithModel.ui @@ -431,7 +431,7 @@ p, li { white-space: pre-wrap; } - 1 + 0 diff --git a/retroshare-gui/src/gui/qss/stylesheet/Standard.qss b/retroshare-gui/src/gui/qss/stylesheet/Standard.qss index 7ab47c667..018239298 100644 --- a/retroshare-gui/src/gui/qss/stylesheet/Standard.qss +++ b/retroshare-gui/src/gui/qss/stylesheet/Standard.qss @@ -115,7 +115,23 @@ GxsCreateCommentDialog QFrame#frame { background: white; } +CreateGxsChannelMsg QPushButton#postButton { + font: bold; + font-size: 15px; + color: white; + background: #0099cc; + border-radius: 4px; + max-height: 27px; + min-width: 4em; + padding: 2px; +} +CreateGxsChannelMsg QPushButton#postButton:hover { + background: #03b1f3; + border-radius: 4px; + min-width: 4em; + padding: 2px; +} /* Forums */ @@ -699,16 +715,16 @@ GenCertDialog QFrame#profileframe{ border-width: 0px; } -PostedListWidget QComboBox#comboBox { +PostedListWidgetWithModel QComboBox#sortStrategy_CB { font: bold; color: #0099cc; } -PostedListWidget QToolButton#submitPostButton { +PostedListWidgetWithModel QToolButton#submitPostButton { font: bold; } -PostedListWidget QToolButton#subscribeToolButton { +PostedListWidgetWithModel QToolButton#subscribeToolButton { font: bold; font-size: 15px; color: white; @@ -717,12 +733,12 @@ PostedListWidget QToolButton#subscribeToolButton { max-height: 27px; } -PostedListWidget QToolButton#subscribeToolButton:hover { +PostedListWidgetWithModel QToolButton#subscribeToolButton:hover { background: #03b1f3; border-radius: 4px; } -PostedListWidget QFrame#headerFrame { +PostedListWidgetWithModel QFrame#headerFrame { background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #FEFEFE, stop:1 #E8E8E8); border: 1px solid #CCCCCC; } @@ -807,50 +823,45 @@ GxsGroupDialog QLabel#groupLogo{ border-radius: 3px; } -PostedItem QFrame#frame_notes { - background: white; -} -PostedItem QFrame#mainFrame { +BoardPostDisplayWidget_compact QFrame#mainFrame { background-color: white; } -PostedItem QLabel#notes { - -} - PostedItem QFrame#voteFrame { background: #f8f9fa; } -PostedItem QFrame#mainFrame [new=false]{ +BoardPostDisplayWidget_compact > QFrame#mainFrame [new=false]{ background: white; } -PostedItem > QFrame#mainFrame[new=true] { +BoardPostDisplayWidget_compact > QFrame#mainFrame[new=true] { background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #F0F8FD, stop:0.8 #E6F2FD, stop: 0.81 #E6F2FD, stop: 1 #D2E7FD); } -PostedItem QFrame#frame_picture{ - background: white; -} - -PostedItem QLabel#thumbnailLabel{ +BoardPostDisplayWidget_compact QLabel#pictureLabel{ border: 2px solid #CCCCCC; border-radius: 3px; } -PostedItem QLabel#fromBoldLabel, QLabel#fromLabel, QLabel#dateLabel, QLabel#siteBoldLabel { +BoardPostDisplayWidget_compact QLabel#fromBoldLabel , +BoardPostDisplayWidget_card QLabel#fromBoldLabel { + font: bold; +} + +BoardPostDisplayWidget_compact QLabel#fromBoldLabel, QLabel#fromLabel, QLabel#dateLabel, QLabel#siteBoldLabel , +BoardPostDisplayWidget_card QLabel#fromBoldLabel, QLabel#fromLabel, QLabel#dateLabel, QLabel#siteBoldLabel{ color: #787c7e; } -PostedItem QLabel#newLabel { +BoardPostDisplayWidget_compact QLabel#newLabel { border: 1px solid #167BE7; background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #2291E0, stop: 1 #3EB3FF); border-radius: 3px; } -PostedCardView QLabel#newLabel { +BoardPostDisplayWidget_card QLabel#newLabel { border: 1px solid #167BE7; background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #2291E0, stop: 1 #3EB3FF); border-radius: 3px; @@ -860,17 +871,30 @@ PostedCardView QFrame#voteFrame { background: #f8f9fa; } -PostedCardView QFrame#mainFrame { +BoardPostDisplayWidget_card QFrame#mainFrame{ background: white; } +BoardPostDisplayWidget_card > QFrame#mainFrame [new=false]{ + background: white; +} + +BoardPostDisplayWidget_card > QFrame#mainFrame[new=true] { + background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #F0F8FD, stop:0.8 #E6F2FD, stop: 0.81 #E6F2FD, stop: 1 #D2E7FD); +} + +BoardPostDisplayWidget_compact QLabel#titleLabel, +BoardPostDisplayWidget_card QLabel#titleLabel{ + font-size: 14px; + font: bold; +} GxsCommentDialog QComboBox#sortBox { font: bold; color: #0099cc; } -PostedListWidget QTextBrowser#infoDescription { +PostedListWidgetWithModel QTextBrowser#infoDescription { background: transparent; border: none; } diff --git a/retroshare-gui/src/gui/settings/WebuiPage.h b/retroshare-gui/src/gui/settings/WebuiPage.h index f38662f5e..9ba8886ac 100644 --- a/retroshare-gui/src/gui/settings/WebuiPage.h +++ b/retroshare-gui/src/gui/settings/WebuiPage.h @@ -20,7 +20,8 @@ #pragma once -#include +#include "retroshare-gui/configpage.h" +#include "gui/common/FilesDefs.h" #include "ui_WebuiPage.h" namespace resource_api{ diff --git a/retroshare-gui/src/qss/qdarkstyle-v2.qss b/retroshare-gui/src/qss/qdarkstyle-v2.qss index 4f6ee1cae..e96df4876 100644 --- a/retroshare-gui/src/qss/qdarkstyle-v2.qss +++ b/retroshare-gui/src/qss/qdarkstyle-v2.qss @@ -1983,7 +1983,7 @@ QTreeView [new=true]{ /* changes for the subscribe Button */ -PostedListWidget QToolButton#subscribeToolButton { +PostedListWidgetWithModel QToolButton#subscribeToolButton { font: bold; font-size: 15px; color: white; @@ -1992,7 +1992,7 @@ PostedListWidget QToolButton#subscribeToolButton { max-height: 27px; } -PostedListWidget QToolButton#subscribeToolButton:hover { +PostedListWidgetWithModel QToolButton#subscribeToolButton:hover { background: #03b1f3; border-radius: 4px; } @@ -2011,7 +2011,7 @@ GxsForumThreadWidget QToolButton#subscribeToolButton:hover { border-radius: 4px; } -GxsChannelPostsWidget QToolButton#subscribeToolButton { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton { font: bold; font-size: 14px; color: white; @@ -2020,18 +2020,18 @@ GxsChannelPostsWidget QToolButton#subscribeToolButton { max-height: 27px; } -GxsChannelPostsWidget QToolButton#subscribeToolButton:hover { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton:hover { background: #03b1f3; border-radius: 4px; } -GxsChannelPostsWidget QToolButton#subscribeToolButton:pressed { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton:pressed { background: #03b1f3; border-radius: 4px; border: 1px solid gray; } -GxsChannelPostsWidget QToolButton#subscribeToolButton:disabled { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton:disabled { background: gray; border-radius: 4px; border: 1px solid gray; @@ -2039,19 +2039,37 @@ GxsChannelPostsWidget QToolButton#subscribeToolButton:disabled { } /* only for MenuButtonPopup */ -GxsChannelPostsWidget QToolButton#subscribeToolButton[popupMode="1"] { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton[popupMode="1"] { padding-right: 0px; } -GxsChannelPostsWidget QToolButton#subscribeToolButton::menu-arrow { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton::menu-arrow { image: none; } -GxsChannelPostsWidget QToolButton#subscribeToolButton::menu-button { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton::menu-button { image: none; } +CreateGxsChannelMsg QPushButton#postButton { + font: bold; + font-size: 15px; + color: white; + background: #0099cc; + border-radius: 4px; + max-height: 27px; + min-width: 4em; + padding: 2px; +} + +CreateGxsChannelMsg QPushButton#postButton:hover { + background: #03b1f3; + border-radius: 4px; + min-width: 4em; + padding: 2px; +} + QTabBar#smTab::tab{ height: 32px; width: 32px; @@ -2074,73 +2092,86 @@ PostedCreatePostDialog QPushButton#submitButton:hover { } -PostedItem QFrame#mainFrame { +BoardPostDisplayWidget_compact QFrame#mainFrame { border-radius: 4px; border: 1px solid #32414B; background-color: #19232D; +} +BoardPostDisplayWidget_compact QFrame#mainFrame [new=false]{ + background: #19232D; +} + +BoardPostDisplayWidget_compact QFrame#mainFrame[new=true] { + background-color: #1464a0; } GxsChannelPostItem QFrame#mainFrame { border-radius: 4px; border: 1px solid #32414B; background-color: #19232D; - } -PostedItem QPushButton#shareButton -{ +BoardPostDisplayWidget_compact QPushButton#shareButton { background-color: transparent; min-width: 80px; max-height: 22px; - } -PostedItem QLabel#scoreLabel -{ - background-color: transparent; - -} - -PostedItem QFrame#voteFrame { +BoardPostDisplayWidget_compact QFrame#voteFrame { background: #141415; } -PostedItem QToolButton#voteDownButton, QToolButton#voteUpButton +BoardPostDisplayWidget_compact QToolButton#voteDownButton, QToolButton#voteUpButton, +BoardPostDisplayWidget_card QToolButton#voteDownButton, QToolButton#voteUpButton { border: none; - } -PostedItem QLabel#thumbnailLabel{ +BoardPostDisplayWidget_compact QLabel#pictureLabel{ border: 2px solid #CCCCCC; border-radius: 3px; } -PostedCardView QPushButton#shareButton -{ - background-color: transparent; - min-width: 80px; - max-height: 22px; - +BoardPostDisplayWidget_compact QLabel#scoreLabel, QLabel#titleLabel, QLabel#fromBoldLabel , QLabel#fromLabel, QLabel#dateLabel, +BoardPostDisplayWidget_card QLabel#scoreLabel, QLabel#titleLabel{ + background-color: transparent; } -PostedCardView QFrame#voteFrame { +BoardPostDisplayWidget_compact QLabel#newLabel, +BoardPostDisplayWidget_card QLabel#newLabel { + border: 1px solid #00B712; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #5AFF15, stop: 1 #00B712); + border-radius: 3px; + color: black; +} + +BoardPostDisplayWidget_card QPushButton#shareButton{ + background-color: transparent; + min-width: 80px; + max-height: 22px; +} + +BoardPostDisplayWidget_card QFrame#voteFrame { background: #141415; } -PostedCardView QFrame#mainFrame { - - background-color: #19232D; - +BoardPostDisplayWidget_card QFrame#mainFrame { + background-color: #19232D; } -PostedCardView QFrame#mainFrame [new=false]{ +BoardPostDisplayWidget_card QFrame#mainFrame [new=false]{ background: #19232D; } -PostedCardView > QFrame#mainFrame[new=true] { - background-color: #005000; +BoardPostDisplayWidget_card QFrame#mainFrame[new=true] { + background-color: #1464a0; +} + +BoardPostDisplayWidget_compact QLabel#titleLabel, +BoardPostDisplayWidget_card QLabel#titleLabel{ + font-size: 14px; + font: bold; } WireGroupItem QFrame#wire_frame{ @@ -2162,3 +2193,5 @@ PulseTopLevel QFrame#frame, PulseViewGroup QFrame#frame, PulseReply QFrame#frame border: 2px solid #38444d; border-radius: 6px; } + + diff --git a/retroshare-gui/src/qss/qdarkstyle.qss b/retroshare-gui/src/qss/qdarkstyle.qss index af56cfc8d..e301f6c53 100644 --- a/retroshare-gui/src/qss/qdarkstyle.qss +++ b/retroshare-gui/src/qss/qdarkstyle.qss @@ -1148,7 +1148,7 @@ OpModeStatus[opMode="Minimal"] { /* changes for the subscribe Button */ -PostedListWidget QToolButton#subscribeToolButton { +PostedListWidgetWithModel QToolButton#subscribeToolButton { font: bold; font-size: 15px; color: white; @@ -1157,7 +1157,7 @@ PostedListWidget QToolButton#subscribeToolButton { max-height: 27px; } -PostedListWidget QToolButton#subscribeToolButton:hover { +PostedListWidgetWithModel QToolButton#subscribeToolButton:hover { background: #03b1f3; border-radius: 4px; } @@ -1176,7 +1176,7 @@ GxsForumThreadWidget QToolButton#subscribeToolButton:hover { border-radius: 4px; } -GxsChannelPostsWidget QToolButton#subscribeToolButton { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton { font: bold; font-size: 14px; color: white; @@ -1185,18 +1185,18 @@ GxsChannelPostsWidget QToolButton#subscribeToolButton { max-height: 27px; } -GxsChannelPostsWidget QToolButton#subscribeToolButton:hover { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton:hover { background: #03b1f3; border-radius: 4px; } -GxsChannelPostsWidget QToolButton#subscribeToolButton:pressed { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton:pressed { background: #03b1f3; border-radius: 4px; border: 1px solid gray; } -GxsChannelPostsWidget QToolButton#subscribeToolButton:disabled { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton:disabled { background: gray; border-radius: 4px; border: 1px solid gray; @@ -1204,19 +1204,37 @@ GxsChannelPostsWidget QToolButton#subscribeToolButton:disabled { } /* only for MenuButtonPopup */ -GxsChannelPostsWidget QToolButton#subscribeToolButton[popupMode="1"] { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton[popupMode="1"] { padding-right: 0px; } -GxsChannelPostsWidget QToolButton#subscribeToolButton::menu-arrow { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton::menu-arrow { image: none; } -GxsChannelPostsWidget QToolButton#subscribeToolButton::menu-button { +GxsChannelPostsWidgetWithModel QToolButton#subscribeToolButton::menu-button { image: none; } +CreateGxsChannelMsg QPushButton#postButton { + font: bold; + font-size: 15px; + color: white; + background: #0099cc; + border-radius: 4px; + max-height: 27px; + min-width: 4em; + padding: 2px; +} + +CreateGxsChannelMsg QPushButton#postButton:hover { + background: #03b1f3; + border-radius: 4px; + min-width: 4em; + padding: 2px; +} + QTabBar#smTab::tab{ height: 32px; width: 32px; @@ -1247,26 +1265,26 @@ GxsForumThreadWidget QLabel#forumName font: bold; } -PostedItem QPushButton#shareButton +BoardPostDisplayWidget_compact QPushButton#shareButton { - background-color: transparent; + background-color: transparent; border: none; min-width: 75px; max-height: 22px; } -PostedCardView QPushButton#shareButton +BoardPostDisplayWidget_card QPushButton#shareButton { - background-color: transparent; + background-color: transparent; border: none; min-width: 75px; } -PostedItem QFrame#voteFrame { +BoardPostDisplayWidget_compact QFrame#voteFrame { background: #141415; } -PostedCardView QFrame#voteFrame { +BoardPostDisplayWidget_card QFrame#voteFrame { background: #141415; } @@ -1275,30 +1293,63 @@ QPushButton#shareButton:hover, QPushButton#shareButton::menu-button:hover { border: 1px solid gray; } -PostedItem QToolButton#voteDownButton, QToolButton#voteUpButton, QToolButton#expandButton, QToolButton#readButton, +BoardPostDisplayWidget_compact QToolButton#voteDownButton, QToolButton#voteUpButton, QToolButton#expandButton, QToolButton#readButton, QToolButton#commentButton, QToolButton#notesButton { border: none; } -PostedItem QLabel#thumbnailLabel{ +BoardPostDisplayWidget_compact QLabel#pictureLabel{ border: 2px solid #CCCCCC; border-radius: 3px; } -PostedCardView QToolButton#voteDownButton, QToolButton#voteUpButton +BoardPostDisplayWidget_compact > QFrame#frame [new=false]{ + background: #302F2F; +} + +BoardPostDisplayWidget_compact > QFrame#frame[new=true] { + background-color: #005000; +} + +BoardPostDisplayWidget_card QToolButton#voteDownButton, QToolButton#voteUpButton { border: none; } -PostedCardView QFrame#mainFrame [new=false]{ +BoardPostDisplayWidget_card QFrame#frame{ background: #302F2F; } -PostedCardView > QFrame#mainFrame[new=true] { +BoardPostDisplayWidget_card > QFrame#frame [new=false]{ + background: #302F2F; +} + +BoardPostDisplayWidget_card > QFrame#frame[new=true] { background-color: #005000; } +BoardPostDisplayWidget_compact QLabel#newLabel { + border: 1px solid #00B712; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #5AFF15, stop: 1 #00B712); + border-radius: 3px; + color: black; +} + +BoardPostDisplayWidget_card QLabel#newLabel { + border: 1px solid #00B712; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #5AFF15, stop: 1 #00B712); + border-radius: 3px; + color: black; +} + +BoardPostDisplayWidget_compact QLabel#titleLabel, +BoardPostDisplayWidget_card QLabel#titleLabel{ + font-size: 14px; + font: bold; +} + + WireGroupItem QFrame#wire_frame { border: 1px solid #38444d; diff --git a/retroshare-gui/src/retroshare-gui.pro b/retroshare-gui/src/retroshare-gui.pro index c03332148..960666d4e 100644 --- a/retroshare-gui/src/retroshare-gui.pro +++ b/retroshare-gui/src/retroshare-gui.pro @@ -1376,8 +1376,8 @@ gxschannels { gui/gxschannels/GxsChannelPostsWidgetWithModel.cpp \ gui/gxschannels/GxsChannelPostsModel.cpp \ gui/gxschannels/GxsChannelPostFilesModel.cpp \ + gui/gxschannels/GxsChannelFilesStatusWidget.cpp \ gui/gxschannels/GxsChannelPostThumbnail.cpp \ - gui/gxschannels/GxsChannelFilesStatusWidget.cpp \ gui/gxschannels/GxsChannelGroupDialog.cpp \ gui/gxschannels/CreateGxsChannelMsg.cpp \ gui/feeds/GxsChannelGroupItem.cpp \ @@ -1389,7 +1389,9 @@ gxschannels { posted { HEADERS += gui/Posted/PostedDialog.h \ - gui/Posted/PostedListWidget.h \ + gui/Posted/PostedListWidgetWithModel.h \ + gui/Posted/PostedPostsModel.h \ + gui/Posted/BoardPostDisplayWidget.h \ gui/Posted/PostedItem.h \ gui/Posted/PostedCardView.h \ gui/Posted/PostedGroupDialog.h \ @@ -1401,18 +1403,22 @@ posted { #gui/Posted/PostedCreateCommentDialog.h \ #gui/Posted/PostedComments.h \ - FORMS += gui/Posted/PostedListWidget.ui \ + FORMS += gui/Posted/PostedListWidgetWithModel.ui \ gui/feeds/PostedGroupItem.ui \ + gui/Posted/BoardPostDisplayWidget_compact.ui \ + gui/Posted/BoardPostDisplayWidget_card.ui \ gui/Posted/PostedItem.ui \ gui/Posted/PostedCardView.ui \ gui/Posted/PostedCreatePostDialog.ui \ - gui/Posted/PhotoView.ui + gui/Posted/PhotoView.ui \ #gui/Posted/PostedDialog.ui \ #gui/Posted/PostedComments.ui \ #gui/Posted/PostedCreateCommentDialog.ui SOURCES += gui/Posted/PostedDialog.cpp \ - gui/Posted/PostedListWidget.cpp \ + gui/Posted/PostedListWidgetWithModel.cpp \ + gui/Posted/BoardPostDisplayWidget.cpp \ + gui/Posted/PostedPostsModel.cpp \ gui/feeds/PostedGroupItem.cpp \ gui/Posted/PostedItem.cpp \ gui/Posted/PostedCardView.cpp \ diff --git a/retroshare-gui/src/util/ClickableLabel.cpp b/retroshare-gui/src/util/ClickableLabel.cpp index 7b7ccb5fb..e12e01c8d 100644 --- a/retroshare-gui/src/util/ClickableLabel.cpp +++ b/retroshare-gui/src/util/ClickableLabel.cpp @@ -32,4 +32,4 @@ ClickableLabel::~ClickableLabel() { void ClickableLabel::mousePressEvent(QMouseEvent* event) { emit clicked(); -} \ No newline at end of file +} diff --git a/retroshare-gui/src/util/ClickableLabel.h b/retroshare-gui/src/util/ClickableLabel.h index 65070fd67..de4dcce92 100644 --- a/retroshare-gui/src/util/ClickableLabel.h +++ b/retroshare-gui/src/util/ClickableLabel.h @@ -32,16 +32,17 @@ public: explicit ClickableLabel(QWidget* parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags()); ~ClickableLabel(); + void setUseStyleSheet(bool b){ mUseStyleSheet=b ; update();} signals: void clicked(); protected: - void mousePressEvent(QMouseEvent* event); + void mousePressEvent(QMouseEvent* event) override; - void enterEvent(QEvent * /* ev */ ) override { setStyleSheet("QLabel { border: 2px solid #039bd5; }");} - - void leaveEvent(QEvent * /* ev */ ) override { setStyleSheet("QLabel { border: 2px solid #CCCCCC; border-radius: 3px; }");} + void enterEvent(QEvent * /* ev */ ) override { if(mUseStyleSheet) setStyleSheet("QLabel { border: 2px solid #039bd5; }");} + void leaveEvent(QEvent * /* ev */ ) override { if(mUseStyleSheet) setStyleSheet("QLabel { border: 2px solid #CCCCCC; border-radius: 3px; }");} + bool mUseStyleSheet; }; #endif // CLICKABLELABEL_H