diff --git a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp index 2c0227cf1..9e3764623 100644 --- a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp +++ b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.cpp @@ -41,6 +41,7 @@ #include #include +#include #include #include #include "gui/common/FilesDefs.h" @@ -55,9 +56,9 @@ const int MAXMESSAGESIZE = 199000; -PostedCreatePostDialog::PostedCreatePostDialog(RsPosted *posted, const RsGxsGroupId& grpId, const RsGxsId& default_author, QWidget *parent): +PostedCreatePostDialog::PostedCreatePostDialog(RsPosted *posted, const RsGxsGroupId& grpId, const RsGxsId& default_author, const RsGxsMessageId& existingPostId, QWidget *parent): QDialog(parent, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint), - mPosted(posted), mGrpId(grpId), + mPosted(posted), mGrpId(grpId), mOrigPostId(existingPostId), ui(new Ui::PostedCreatePostDialog) { ui->setupUi(this); @@ -69,7 +70,14 @@ PostedCreatePostDialog::PostedCreatePostDialog(RsPosted *posted, const RsGxsGrou connect(ui->RichTextEditWidget, SIGNAL(textSizeOk(bool)),ui->postButton, SLOT(setEnabled(bool))); ui->headerFrame->setHeaderImage(FilesDefs::getPixmapFromQtResourcePath(":/icons/png/postedlinks.png")); - ui->headerFrame->setHeaderText(tr("Create a new Post")); + + if(!mOrigPostId.isNull()) + { + ui->headerFrame->setHeaderText(tr("Edit Post")); + ui->postButton->setText(tr("Update")); + } + else + ui->headerFrame->setHeaderText(tr("Create a new Post")); setAttribute ( Qt::WA_DeleteOnClose, true ); @@ -107,6 +115,61 @@ PostedCreatePostDialog::PostedCreatePostDialog(RsPosted *posted, const RsGxsGrou // should we save the ID in the settings here? I'm not sure we want this. } + + if(!mOrigPostId.isNull()) + loadExistingPost(); +} + +void PostedCreatePostDialog::loadExistingPost() +{ + RsGxsGroupId grpId = mGrpId; + RsGxsMessageId postId = mOrigPostId; + + RsThread::async([this,grpId,postId]() + { + std::vector posts; + std::vector comments; + std::vector votes; + std::set s({ postId }); + + bool ok = mPosted->getBoardContent(grpId,s,posts,comments,votes) && posts.size()==1; + + RsQThreadUtils::postToObject( [ok,posts,this]() + { + if(!ok) + { + QMessageBox::warning(this, tr("RetroShare"), tr("This post could not be loaded, it may not be available locally."), QMessageBox::Ok, QMessageBox::Ok); + return; + } + + const RsPostedPost& post(posts[0]); + + setTitle(QString::fromUtf8(post.mMeta.mMsgName.c_str())); + setLink(QString::fromUtf8(post.mLink.c_str())); + ui->RichTextEditWidget->setText(QString::fromUtf8(post.mNotes.c_str())); + + if(post.mImage.mSize > 0 && post.mImage.mData != NULL) + { + imagebytes = QByteArray((const char*)post.mImage.mData, post.mImage.mSize); + + QPixmap pix; + if(pix.loadFromData(imagebytes)) + { + ui->imageLabel->setPixmap(pix); + ui->removeButton->show(); + ui->stackedWidgetPicture->setCurrentIndex(IMG_PICTURE); + } + } + + // Default to editing with the original author's identity when we + // own it (typical case: the author edits their own post). When + // editing someone else's post as board admin, the edit will be + // signed with whichever own identity is currently selected. + + if(rsIdentity->isOwnId(post.mMeta.mAuthorId)) + ui->idChooser->setChosenId(post.mMeta.mAuthorId); + }, this ); + }); } PostedCreatePostDialog::~PostedCreatePostDialog() @@ -160,42 +223,46 @@ void PostedCreatePostDialog::createPost() return; }//switch (ui->idChooser->getChosenId(authorId)) - RsPostedPost post; - post.mMeta.mGroupId = mGrpId; - post.mLink = std::string(ui->linkEdit->text().toUtf8()); - + std::string link = std::string(ui->linkEdit->text().toUtf8()); + std::string notes; + if(!ui->RichTextEditWidget->toPlainText().trimmed().isEmpty()) { QString text; text = ui->RichTextEditWidget->toHtml(); - post.mNotes = std::string(text.toUtf8()); + notes = std::string(text.toUtf8()); } - post.mMeta.mAuthorId = authorId; - post.mMeta.mMsgName = std::string(ui->titleEdit->text().toUtf8()); - + std::string title = std::string(ui->titleEdit->text().toUtf8()); + + RsGxsImage image; if(imagebytes.size() > 0) { // send posted image - post.mImage.copy((uint8_t *) imagebytes.data(), imagebytes.size()); - } + image.copy((uint8_t *) imagebytes.data(), imagebytes.size()); + } - int msgsize = post.mLink.length() + post.mMeta.mMsgName.length() + post.mNotes.length() + imagebytes.size(); + int msgsize = link.length() + title.length() + notes.length() + imagebytes.size(); if(msgsize > MAXMESSAGESIZE) { QString errormessage = QString(tr("Message is too large.
actual size: %1 bytes, maximum size: %2 bytes.")).arg(msgsize).arg(MAXMESSAGESIZE); QMessageBox::warning(this, "RetroShare", errormessage, QMessageBox::Ok, QMessageBox::Ok); return; } - RsThread::async([this,post]() + RsGxsGroupId grpId = mGrpId; + RsGxsMessageId origPostId = mOrigPostId; + + RsThread::async([this,grpId,title,link,notes,authorId,image,origPostId]() { RsGxsMessageId post_id; + std::string error_message; - bool res = rsPosted->createPost(post,post_id); + bool res = rsPosted->createPostV2(grpId,title,RsUrl(link),notes,authorId,image,post_id,error_message,origPostId); - RsQThreadUtils::postToObject( [res,this]() + RsQThreadUtils::postToObject( [res,error_message,this]() { if(!res) - QMessageBox::information(nullptr,tr("Error while creating post"),tr("An error occurred while creating the post.")); + QMessageBox::information(nullptr,tr("Error while creating post"), + tr("An error occurred while creating the post: ") + QString::fromUtf8(error_message.c_str())); accept(); }, this ); diff --git a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h index 5ac2c2e7d..8582d55b1 100644 --- a/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h +++ b/retroshare-gui/src/gui/Posted/PostedCreatePostDialog.h @@ -37,8 +37,12 @@ public: /*! * @param tokenQ parent callee token * @param posted + * @param existingPostId When non null, the dialog edits the given + * already existing post instead of creating a new one. Editing + * a post requires either being its original author, or being + * the board admin. */ - explicit PostedCreatePostDialog(RsPosted* posted, const RsGxsGroupId& grpId, const RsGxsId& default_author=RsGxsId(),QWidget *parent = 0); + explicit PostedCreatePostDialog(RsPosted* posted, const RsGxsGroupId& grpId, const RsGxsId& default_author=RsGxsId(), const RsGxsMessageId& existingPostId=RsGxsMessageId(), QWidget *parent = 0); ~PostedCreatePostDialog(); void setTitle(const QString& title); @@ -62,12 +66,14 @@ private slots: void setPage(int viewMode); private: void processSettings(bool load); + void loadExistingPost(); int viewMode(); QString mLink; QString mNotes; RsPosted* mPosted; RsGxsGroupId mGrpId; + RsGxsMessageId mOrigPostId; Ui::PostedCreatePostDialog *ui; }; diff --git a/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.cpp b/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.cpp index c5ed31892..b8dc409dc 100644 --- a/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.cpp +++ b/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.cpp @@ -366,12 +366,26 @@ void PostedListWidgetWithModel::postContextMenu(const QPoint& point) menu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_AUTHOR), tr("Show author in People tab"), this, SLOT(showAuthorInPeople()))->setData(index); -#ifdef TODO - // This feature is not implemented yet in libretroshare. + // Own posts can always be edited by their author. Board admins can also + // edit any post, similarly to what is already done for forums. - if(IS_GROUP_PUBLISHER(mGroup.mMeta.mSubscribeFlags)) - menu.addAction(FilesDefs::getIconFromQtResourcePath(":/images/edit_16.png"), tr("Edit"), this, SLOT(editPost())); -#endif + bool can_edit = rsIdentity->isOwnId(post.mMeta.mAuthorId) + || IS_GROUP_ADMIN(mGroup.mMeta.mSubscribeFlags); + + if(can_edit) + menu.addAction(FilesDefs::getIconFromQtResourcePath(":/images/edit_16.png"), tr("Edit"), this, SLOT(editPost()))->setData(index); + + // Sticking ("pinning") a post to the top of the feed is an admin-only + // moderation action, similarly to what is already done for forums. + + if(IS_GROUP_ADMIN(mGroup.mMeta.mSubscribeFlags)) + { + bool is_pinned = mGroup.mPinnedPosts.ids.find(post.mMeta.mMsgId) != mGroup.mPinnedPosts.ids.end(); + + menu.addAction( FilesDefs::getIconFromQtResourcePath(":/images/pin32.png"), + is_pinned ? tr("Un-pin this post") : tr("Pin this post up"), + this, SLOT(togglePinPost()))->setData(index); + } menu.exec(QCursor::pos()); } @@ -537,16 +551,48 @@ void PostedListWidgetWithModel::copyMessageLink() } } -#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); + if(!index.isValid()) + return; + + RsPostedPost post = index.data(Qt::UserRole).value() ; + + if(post.mMeta.mMsgId.isNull()) + return; + + PostedCreatePostDialog *msgDialog = new PostedCreatePostDialog(rsPosted,post.mMeta.mGroupId,post.mMeta.mAuthorId,post.mMeta.mMsgId); msgDialog->show(); + + /* window will destroy itself! */ +} + +void PostedListWidgetWithModel::togglePinPost() +{ + QModelIndex index = ui->postsTree->selectionModel()->currentIndex(); + + if(!index.isValid()) + return; + + RsPostedPost post = index.data(Qt::UserRole).value() ; + + if(post.mMeta.mMsgId.isNull()) + return; + + if(mGroup.mPinnedPosts.ids.find(post.mMeta.mMsgId) == mGroup.mPinnedPosts.ids.end()) + mGroup.mPinnedPosts.ids.insert(post.mMeta.mMsgId); + else + mGroup.mPinnedPosts.ids.erase(post.mMeta.mMsgId); + + uint32_t token; + rsPosted->updateGroup(token,mGroup); + + // The actual model/view update will be triggered by libretroshare using + // the rsEvent system once the board data is updated (RsPostedEventCode + // ::PINNED_POSTS_CHANGED / UPDATED_POSTED_GROUP). } -#endif void PostedListWidgetWithModel::handleEvent_main_thread(std::shared_ptr event) { @@ -576,6 +622,7 @@ void PostedListWidgetWithModel::handleEvent_main_thread(std::shared_ptrmPostedGroupId == groupId()) diff --git a/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.h b/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.h index 0432820a3..15f22c02f 100644 --- a/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.h +++ b/retroshare-gui/src/gui/Posted/PostedListWidgetWithModel.h @@ -137,6 +137,8 @@ private slots: void showPostDetails(); #endif void postContextMenu(const QPoint&); + void editPost(); + void togglePinPost(); void showAuthorInPeople(); void tabCloseRequested(int index); void updateSorting(int); diff --git a/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp b/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp index e827a2fbb..af7a80244 100644 --- a/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp +++ b/retroshare-gui/src/gui/Posted/PostedPostsModel.cpp @@ -480,10 +480,21 @@ class PostSorter { public: - PostSorter(RsPostedPostsModel::SortingStrategy s) : mSortingStrategy(s) {} + PostSorter(RsPostedPostsModel::SortingStrategy s, const RsTlvGxsMsgIdSet& pinned_posts) + : mSortingStrategy(s), mPinnedPosts(pinned_posts) {} bool operator()(const RsPostedPost& p1,const RsPostedPost& p2) const { + // Pinned ("stuck") posts are always shown on top of the feed, + // regardless of the currently selected sorting strategy. This + // mirrors the behaviour already implemented for forums. + + bool p1_pinned = mPinnedPosts.ids.find(p1.mMeta.mMsgId) != mPinnedPosts.ids.end(); + bool p2_pinned = mPinnedPosts.ids.find(p2.mMeta.mMsgId) != mPinnedPosts.ids.end(); + + if(p1_pinned != p2_pinned) + return p1_pinned; + switch(mSortingStrategy) { default: @@ -495,6 +506,7 @@ public: private: RsPostedPostsModel::SortingStrategy mSortingStrategy; + const RsTlvGxsMsgIdSet& mPinnedPosts; }; Qt::ItemFlags RsPostedPostsModel::flags(const QModelIndex& index) const @@ -510,7 +522,7 @@ void RsPostedPostsModel::setSortingStrategy(RsPostedPostsModel::SortingStrategy preMods(); mSortingStrategy = s; - std::sort(mPosts.begin(),mPosts.end(), PostSorter(s)); + std::sort(mPosts.begin(),mPosts.end(), PostSorter(s,mPostedGroup.mPinnedPosts)); postMods(); } @@ -564,7 +576,7 @@ void RsPostedPostsModel::setPosts(const RsPostedGroup& group, std::vector