diff --git a/src/attica/atticaprovider.cpp b/src/attica/atticaprovider.cpp --- a/src/attica/atticaprovider.cpp +++ b/src/attica/atticaprovider.cpp @@ -55,6 +55,7 @@ SLOT(authenticationCredentialsMissing(Provider))); connect(this, &Provider::loadComments, this, &AtticaProvider::loadComments); connect(this, &Provider::loadPerson, this, &AtticaProvider::loadPerson); + connect(this, &Provider::postComment, this, &AtticaProvider::postComment); } AtticaProvider::AtticaProvider(const Attica::Provider &provider, const QStringList &categories, const QString& additionalAgentInformation) @@ -423,6 +424,22 @@ emit personLoaded(author); } +void AtticaProvider::postComment(const EntryInternal &entry, const QString& title, const QString& text, const QString& replyTo) +{ + ItemPostJob* job = m_provider.addNewComment(Attica::Comment::ContentComment, entry.uniqueId(), QStringLiteral(""), replyTo, title, text); + connect(job, &BaseJob::finished, this, &AtticaProvider::postedComment); + job->start(); +} + +void AtticaProvider::postedComment(Attica::BaseJob *baseJob) +{ + if (!jobSuccess(baseJob)) { + return; + } + + emit commentPosted(baseJob->metadata().statusCode()); +} + void AtticaProvider::accountBalanceLoaded(Attica::BaseJob *baseJob) { if (!jobSuccess(baseJob)) { @@ -484,6 +501,10 @@ return entries; } +bool AtticaProvider::userCanVote() { + return m_provider.hasCredentials(); +} + void AtticaProvider::vote(const EntryInternal &entry, uint rating) { PostJob *job = m_provider.voteForContent(entry.uniqueId(), rating); diff --git a/src/attica/atticaprovider_p.h b/src/attica/atticaprovider_p.h --- a/src/attica/atticaprovider_p.h +++ b/src/attica/atticaprovider_p.h @@ -75,11 +75,13 @@ * @see Provider::loadPerson(const QString &username) */ Q_SLOT void loadPerson(const QString &username); + /** + * The slot which causes the Attica provider to attempt to post a comment + * @see Provider::postComment(const EntryInternal &entry, const QString& title, const QString& text, const QString& replyTo); + */ + Q_SLOT void postComment(const EntryInternal &entry, const QString& title, const QString& text, const QString& replyTo); - bool userCanVote() override - { - return true; - } + bool userCanVote() override; void vote(const EntryInternal &entry, uint rating) override; bool userCanBecomeFan() override @@ -100,6 +102,7 @@ void detailsLoaded(Attica::BaseJob *job); void loadedComments(Attica::BaseJob *job); void loadedPerson(Attica::BaseJob *job); + void postedComment(Attica::BaseJob *job); private: void checkForUpdates(); diff --git a/src/core/commentsmodel.h b/src/core/commentsmodel.h --- a/src/core/commentsmodel.h +++ b/src/core/commentsmodel.h @@ -93,6 +93,33 @@ void setEntry(const KNSCore::EntryInternal &newEntry); Q_SIGNAL void entryChanged(); + /** + * @brief Add a new comment to an entry + * + * Creates a comment with a title and text on the entry with the requested ID + * + * @param title the title of the comment + * @param text the main text of the comment + * @param id the ID of the parent comment, if this comment is a reply + */ + Q_INVOKABLE void createComment(QString title, QString text, QString id); + + /** + * @brief Convenience function to send a comment and a vote immediately after each other + * + * Setting score on an entry immediately following creating a comment will be perceived + * as marking that comment as a review, and set the score of the review accordingly. + * This function is a convenienct way of ensuring this will definitely happen in the + * correct order and with the expected timing. + * + * @note there is no way to call this with a parent comment, since reviews should always be a top level comment + * + * @param title The title of the review + * @param text The main text of the review + * @param rating The rating you wish to set for the review (0 means remove this user's vote, 1-100 is the score the user wishes to give) + * @see KNSCore::CommmentsModel::createComment(QString title, QString text) + */ + Q_INVOKABLE void createReview(QString title, QString text, int rating); private: class Private; Private *d; diff --git a/src/core/commentsmodel.cpp b/src/core/commentsmodel.cpp --- a/src/core/commentsmodel.cpp +++ b/src/core/commentsmodel.cpp @@ -223,3 +223,27 @@ d->fetch(Private::ClearModel); emit entryChanged(); } + +void KNSCore::CommentsModel::createComment(QString title, QString text, QString id) +{ + if (d->engine && d->entry.isValid() && d->engine->userCanVote(d->entry)) { + QSharedPointer provider = d->engine->provider(d->entry.providerId()); + provider->postComment(d->entry, title, text, id); + } +} + +void KNSCore::CommentsModel::createReview(QString title, QString text, int rating) +{ + if (d->engine && d->entry.isValid() && d->engine->userCanVote(d->entry)) { + QSharedPointer provider = d->engine->provider(d->entry.providerId()); + QMetaObject::Connection * const connection = new QMetaObject::Connection; + *connection = connect(provider.data(), &Provider::commentPosted, [this, connection, provider, rating](int status){ + QObject::disconnect(*connection); + if (status == 100) { + provider->vote(d->entry, rating); + } + delete connection; + }); + provider->postComment(d->entry, title, text, QStringLiteral("")); + } +} diff --git a/src/core/provider.h b/src/core/provider.h --- a/src/core/provider.h +++ b/src/core/provider.h @@ -171,6 +171,21 @@ * @since 5.63 */ Q_SIGNAL void loadPerson(const QString &username); + /** + * Request posting of a comment on an entry, optionally as a reply to another comment + * The engine listens to the commentPosted() signal for the result + * + * @note Implementation detail: All subclasses should connect to this signal + * and point it at a slot which does the actual work, if they support comments. + * + * TODO: KF6 This should be a virtual function, but can't do it now because BIC + * @param entry The entry to comment on + * @param title The title of the comment + * @param text The main text of the comment + * @param replyTo Some identifier for a comment this is intended as a reply to. If this is an empty string, consider it not a reply + * @since 5.67 + */ + Q_SIGNAL void postComment(const EntryInternal &entry, const QString& title, const QString& text, const QString& replyTo); virtual bool userCanVote() { @@ -239,6 +254,16 @@ * @since 5.63 */ void personLoaded(const std::shared_ptr author); + /** + * Fired when an attempt to post a comment has completed. The result of the attempt can + * be gleaned from the status parameter, and should be interpreted as follows: + * 100 - successful + * 101 - content must not be empty + * 102 - message or subject must not be empty + * 103 - no permission to add a comment + * @param status Whether or not the comment was posted successfully (see above for possible values) + */ + void commentPosted(int status); void signalInformation(const QString &) const; void signalError(const QString &) const; diff --git a/src/qtquick/quickitemsmodel.h b/src/qtquick/quickitemsmodel.h --- a/src/qtquick/quickitemsmodel.h +++ b/src/qtquick/quickitemsmodel.h @@ -98,6 +98,7 @@ InstalledFilesRole, UnInstalledFilesRole, RatingRole, + UserCanVoteRole, NumberOfCommentsRole, DownloadCountRole, NumberFansRole, @@ -169,10 +170,21 @@ * * @note This will simply fail quietly if the item is not installed * - * @param index The intex of the item to be adopted + * @param index The index of the item to be adopted */ Q_INVOKABLE void adoptItem(int index); + /** + * @brief Vote for an item + * + * If the user is able, this will rate the item + * + * @param index The index of the item to vote for + * @param rating The rating you wish to set for the item (0 means remove this user's vote, 1-100 is the score the user wishes to give) + * @see KNSCore::CommentsModel::createReview( + */ + Q_INVOKABLE void voteForItem(int index, int rating); + /** * @brief Fired when an entry's data changes * diff --git a/src/qtquick/quickitemsmodel.cpp b/src/qtquick/quickitemsmodel.cpp --- a/src/qtquick/quickitemsmodel.cpp +++ b/src/qtquick/quickitemsmodel.cpp @@ -122,6 +122,7 @@ {InstalledFilesRole, "installedFiles"}, {UnInstalledFilesRole, "uninstalledFiles"}, {RatingRole, "rating"}, + {UserCanVoteRole, "userCanVote"}, ///<@ While it seems strange to hold this here when core has it on Engine, this information is conceptually per-Entry {NumberOfCommentsRole, "numberOfComments"}, {DownloadCountRole, "downloadCount"}, {NumberFansRole, "numberFans"}, @@ -243,6 +244,9 @@ case RatingRole: data.setValue(entry.rating()); break; + case UserCanVoteRole: + data.setValue(d->coreEngine->userCanVote(entry)); + break; case NumberOfCommentsRole: data.setValue(entry.numberOfComments()); break; @@ -429,3 +433,13 @@ } } } + +void ItemsModel::voteForItem(int index, int rating) +{ + if (d->coreEngine) { + KNSCore::EntryInternal entry = d->model->data(d->model->index(index), Qt::UserRole).value(); + if (d->coreEngine->userCanVote(entry)) { + d->coreEngine->vote(entry, rating); + } + } +} diff --git a/src/ui/entrydetailsdialog.cpp b/src/ui/entrydetailsdialog.cpp --- a/src/ui/entrydetailsdialog.cpp +++ b/src/ui/entrydetailsdialog.cpp @@ -127,8 +127,10 @@ // Most of the voting is 20 - 80, so rate 20 as 0 stars and 80 as 5 stars int rating = qMax(0, qMin(10, (m_entry.rating() - 20) / 6)); ui->ratingWidget->setRating(rating); - connect(ui->ratingWidget, static_cast(&KRatingWidget::ratingChanged), - this, &EntryDetails::ratingChanged); + if (m_engine->userCanVote(entry)) { + connect(ui->ratingWidget, static_cast(&KRatingWidget::ratingChanged), + this, &EntryDetails::ratingChanged); + } } else { ui->ratingWidget->setVisible(false); } diff --git a/src/ui/itemsviewdelegate.cpp b/src/ui/itemsviewdelegate.cpp --- a/src/ui/itemsviewdelegate.cpp +++ b/src/ui/itemsviewdelegate.cpp @@ -81,9 +81,7 @@ rating->setMaxRating(10); rating->setHalfStepsEnabled(true); list << rating; - const KNSCore::EntryInternal entry = index.data(Qt::UserRole).value(); - connect(rating, static_cast(&KRatingWidget::ratingChanged), - this, [this, entry](unsigned int newRating){m_engine->vote(entry, newRating * 10);}); + // Not supposed to be able to actually vote from the listview, so we're not connecting the widget to anything return list; }