diff --git a/microblogs/mastodon/CMakeLists.txt b/microblogs/mastodon/CMakeLists.txt index 5b6e6139..6a81fbb7 100644 --- a/microblogs/mastodon/CMakeLists.txt +++ b/microblogs/mastodon/CMakeLists.txt @@ -1,41 +1,43 @@ include_directories( ${CHOQOK_INCLUDES} ) set(choqok_mastodon_SRCS mastodonaccount.cpp + mastodoncomposerwidget.cpp mastodondebug.cpp + mastodondmessagedialog.cpp mastodoneditaccountwidget.cpp mastodonmicroblog.cpp mastodonoauth.cpp mastodonoauthreplyhandler.cpp mastodonpost.cpp mastodonpostwidget.cpp ) ki18n_wrap_ui(choqok_mastodon_SRCS mastodoneditaccountwidget.ui ) add_library(choqok_mastodon MODULE ${choqok_mastodon_SRCS}) kcoreaddons_desktop_to_json(choqok_mastodon choqok_mastodon.desktop) target_link_libraries(choqok_mastodon PUBLIC Qt5::Core Qt5::Gui Qt5::NetworkAuth Qt5::Widgets KF5::I18n KF5::KIOCore KF5::KIOWidgets KF5::WidgetsAddons qca-qt5 choqok ) install(TARGETS choqok_mastodon DESTINATION ${PLUGIN_INSTALL_DIR}) install(FILES choqok_mastodon.desktop DESTINATION ${SERVICES_INSTALL_DIR}) add_subdirectory(icons) diff --git a/microblogs/mastodon/mastodonaccount.cpp b/microblogs/mastodon/mastodonaccount.cpp index e17c69f8..36770cf3 100644 --- a/microblogs/mastodon/mastodonaccount.cpp +++ b/microblogs/mastodon/mastodonaccount.cpp @@ -1,130 +1,168 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2017 Andrea Scarpino This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see http://www.gnu.org/licenses/ */ #include "mastodonaccount.h" #include #include "passwordmanager.h" #include "mastodonmicroblog.h" class MastodonAccount::Private { public: QString consumerKey; QString consumerSecret; QString host; - QString acct; + uint id; QString tokenSecret; + QStringList followers; QStringList following; QVariantList lists; MastodonOAuth *oAuth; QStringList timelineNames; }; MastodonAccount::MastodonAccount(MastodonMicroBlog *parent, const QString &alias): Account(parent, alias), d(new Private) { d->host = configGroup()->readEntry("Host", QString()); - d->acct = configGroup()->readEntry("Acct", QString()); + d->id = configGroup()->readEntry("Id", uint()); + d->followers = configGroup()->readEntry("Followers", QStringList()); + d->following = configGroup()->readEntry("Following", QStringList()); + d->lists = configGroup()->readEntry("Lists", QVariantList()); d->tokenSecret = Choqok::PasswordManager::self()->readPassword(QStringLiteral("%1_tokenSecret").arg(alias)); d->consumerKey = configGroup()->readEntry("ConsumerKey", QString()); d->consumerSecret = Choqok::PasswordManager::self()->readPassword(QStringLiteral("%1_consumerSecret").arg(alias)); d->oAuth = new MastodonOAuth(this); d->oAuth->setToken(d->tokenSecret); setPostCharLimit(500); + + parent->fetchFollowers(this, false); + parent->fetchFollowing(this, false); } MastodonAccount::~MastodonAccount() { d->oAuth->deleteLater(); delete d; } void MastodonAccount::writeConfig() { configGroup()->writeEntry("Host", d->host); - configGroup()->writeEntry("Acct", d->acct); + configGroup()->writeEntry("Id", d->id); configGroup()->writeEntry("ConsumerKey", d->consumerKey); + configGroup()->writeEntry("Followers", d->followers); + configGroup()->writeEntry("Following", d->following); + configGroup()->writeEntry("Lists", d->lists); + Choqok::PasswordManager::self()->writePassword(QStringLiteral("%1_consumerSecret").arg(alias()), d->consumerSecret); Choqok::PasswordManager::self()->writePassword(QStringLiteral("%1_tokenSecret").arg(alias()), d->tokenSecret); Choqok::Account::writeConfig(); } QString MastodonAccount::host() { return d->host; } void MastodonAccount::setHost(const QString &host) { d->host = host; } -QString MastodonAccount::acct() +uint MastodonAccount::id() { - return d->acct; + return d->id; } -void MastodonAccount::setAcct(const QString &acct) +void MastodonAccount::setId(const uint id) { - d->acct = acct; + d->id = id; } QString MastodonAccount::consumerKey() { return d->consumerKey; } void MastodonAccount::setConsumerKey(const QString &consumerKey) { d->consumerKey = consumerKey; } QString MastodonAccount::consumerSecret() { return d->consumerSecret; } void MastodonAccount::setConsumerSecret(const QString &consumerSecret) { d->consumerSecret = consumerSecret; } QString MastodonAccount::tokenSecret() { return d->tokenSecret; } void MastodonAccount::setTokenSecret(const QString &tokenSecret) { d->tokenSecret = tokenSecret; } MastodonOAuth *MastodonAccount::oAuth() { return d->oAuth; } + +QStringList MastodonAccount::followers() { + return d->followers; +} + +void MastodonAccount::setFollowers(const QStringList &followers) { + d->followers = followers; + writeConfig(); +} + +QStringList MastodonAccount::following() { + return d->following; +} + +void MastodonAccount::setFollowing(const QStringList &following) { + d->following = following; + writeConfig(); +} + +QVariantList MastodonAccount::lists() { + return d->lists; +} + +void MastodonAccount::setLists(const QVariantList &lists) { + d->lists = lists; + writeConfig(); +} diff --git a/microblogs/mastodon/mastodonaccount.h b/microblogs/mastodon/mastodonaccount.h index 05dea4eb..7ba4e2b8 100644 --- a/microblogs/mastodon/mastodonaccount.h +++ b/microblogs/mastodon/mastodonaccount.h @@ -1,65 +1,74 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2017 Andrea Scarpino This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see http://www.gnu.org/licenses/ */ #ifndef MASTODONACCOUNT_H #define MASTODONACCOUNT_H #include "account.h" #include "choqoktypes.h" #include "mastodonoauth.h" class MastodonMicroBlog; class MastodonAccount : public Choqok::Account { Q_OBJECT public: explicit MastodonAccount(MastodonMicroBlog *parent, const QString &alias); ~MastodonAccount(); virtual void writeConfig() override; QString host(); void setHost(const QString &host); - QString acct(); - void setAcct(const QString &acct); + uint id(); + void setId(const uint id); QString consumerKey(); void setConsumerKey(const QString &consumerKey); QString consumerSecret(); void setConsumerSecret(const QString &consumerSecret); QString tokenSecret(); void setTokenSecret(const QString &tokenSecret); MastodonOAuth *oAuth(); + QStringList followers(); + void setFollowers(const QStringList &followers); + + QStringList following(); + void setFollowing(const QStringList &following); + + QVariantList lists(); + void setLists(const QVariantList &lists); + private: class Private; Private *d; }; #endif // MASTODONACCOUNT_H diff --git a/microblogs/mastodon/mastodoncomposerwidget.cpp b/microblogs/mastodon/mastodoncomposerwidget.cpp new file mode 100644 index 00000000..32e525c5 --- /dev/null +++ b/microblogs/mastodon/mastodoncomposerwidget.cpp @@ -0,0 +1,169 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ +*/ + +#include "mastodoncomposerwidget.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "account.h" +#include "choqoktextedit.h" +#include "shortenmanager.h" + +#include "mastodondebug.h" +#include "mastodonmicroblog.h" +#include "mastodonpost.h" + +class MastodonComposerWidget::Private +{ +public: + QString mediumToAttach; + QPushButton *btnAttach; + QPointer mediumName; + QPointer btnCancel; + QGridLayout *editorLayout; +}; + +MastodonComposerWidget::MastodonComposerWidget(Choqok::Account *account, QWidget *parent) + : ComposerWidget(account, parent) + , d(new Private) +{ + d->editorLayout = qobject_cast(editorContainer()->layout()); + d->btnAttach = new QPushButton(editorContainer()); + d->btnAttach->setIcon(QIcon::fromTheme(QLatin1String("mail-attachment"))); + d->btnAttach->setToolTip(i18n("Attach a file")); + d->btnAttach->setMaximumWidth(d->btnAttach->height()); + connect(d->btnAttach, &QPushButton::clicked, this, &MastodonComposerWidget::attachMedia); + QVBoxLayout *vLayout = new QVBoxLayout; + vLayout->addWidget(d->btnAttach); + vLayout->addSpacerItem(new QSpacerItem(1, 1, QSizePolicy::Preferred, QSizePolicy::MinimumExpanding)); + d->editorLayout->addItem(vLayout, 0, 1); +} + +MastodonComposerWidget::~MastodonComposerWidget() +{ + delete d; +} + +void MastodonComposerWidget::submitPost(const QString &text) +{ + qCDebug(CHOQOK); + editorContainer()->setEnabled(false); + QString txt = text; + if (currentAccount()->postCharLimit() && + txt.size() > (int) currentAccount()->postCharLimit()) { + txt = Choqok::ShortenManager::self()->parseText(txt); + } + setPostToSubmit(nullptr); + setPostToSubmit(new Choqok::Post); + postToSubmit()->content = txt; + if (!replyToId.isEmpty()) { + postToSubmit()->replyToPostId = replyToId; + } + connect(currentAccount()->microblog(), &Choqok::MicroBlog::postCreated, this, + &MastodonComposerWidget::slotPostSubmited); + connect(currentAccount()->microblog(), &Choqok::MicroBlog::errorPost, this, + &MastodonComposerWidget::slotErrorPost); + btnAbort = new QPushButton(QIcon::fromTheme(QLatin1String("dialog-cancel")), i18n("Abort"), this); + layout()->addWidget(btnAbort); + connect(btnAbort, &QPushButton::clicked, this, &MastodonComposerWidget::abort); + + MastodonMicroBlog *mBlog = qobject_cast(currentAccount()->microblog()); + if (d->mediumToAttach.isEmpty()) { + if (replyToId.isEmpty()) { + currentAccount()->microblog()->createPost(currentAccount(), postToSubmit()); + } else { + // WTF? It seems we cannot cast postToSubmit to MastodonPost and then I'm copying its attributes + MastodonPost *pumpPost = new MastodonPost(); + pumpPost->content = postToSubmit()->content; + pumpPost->replyToPostId = postToSubmit()->replyToPostId; + setPostToSubmit(pumpPost); + + mBlog->createReply(currentAccount(), pumpPost); + } + } else { + // TODO + // mBlog->createPostWithMedia(currentAccount(), postToSubmit(), d->mediumToAttach); + } +} + +void MastodonComposerWidget::slotPostSubmited(Choqok::Account *theAccount, Choqok::Post *post) +{ + qCDebug(CHOQOK); + if (currentAccount() == theAccount && post == postToSubmit()) { + qCDebug(CHOQOK) << "Accepted"; + disconnect(currentAccount()->microblog(), &Choqok::MicroBlog::postCreated, + this, &MastodonComposerWidget::slotPostSubmited); + disconnect(currentAccount()->microblog(), &Choqok::MicroBlog::errorPost, + this, &MastodonComposerWidget::slotErrorPost); + if (btnAbort) { + btnAbort->deleteLater(); + } + editor()->clear(); + editorCleared(); + editorContainer()->setEnabled(true); + setPostToSubmit(nullptr); + cancelAttach(); + currentAccount()->microblog()->updateTimelines(currentAccount()); + } +} + +void MastodonComposerWidget::attachMedia() +{ + qCDebug(CHOQOK); + d->mediumToAttach = QFileDialog::getOpenFileName(this, i18n("Select Media to Upload"), + QString(), QStringLiteral("Images")); + if (d->mediumToAttach.isEmpty()) { + qCDebug(CHOQOK) << "No file selected"; + return; + } + const QString fileName = QUrl(d->mediumToAttach).fileName(); + if (!d->mediumName) { + d->mediumName = new QLabel(editorContainer()); + d->btnCancel = new QPushButton(editorContainer()); + d->btnCancel->setIcon(QIcon::fromTheme(QLatin1String("list-remove"))); + d->btnCancel->setToolTip(i18n("Discard Attachment")); + d->btnCancel->setMaximumWidth(d->btnCancel->height()); + connect(d->btnCancel, &QPushButton::clicked, this, &MastodonComposerWidget::cancelAttach); + + d->editorLayout->addWidget(d->mediumName, 1, 0); + d->editorLayout->addWidget(d->btnCancel, 1, 1); + } + d->mediumName->setText(i18n("Attaching %1", fileName)); + editor()->setFocus(); +} + +void MastodonComposerWidget::cancelAttach() +{ + qCDebug(CHOQOK); + delete d->mediumName; + d->mediumName = 0; + delete d->btnCancel; + d->btnCancel = 0; + d->mediumToAttach.clear(); +} diff --git a/microblogs/mastodon/mastodonpostwidget.h b/microblogs/mastodon/mastodoncomposerwidget.h similarity index 61% copy from microblogs/mastodon/mastodonpostwidget.h copy to microblogs/mastodon/mastodoncomposerwidget.h index 569d3244..003e7872 100644 --- a/microblogs/mastodon/mastodonpostwidget.h +++ b/microblogs/mastodon/mastodoncomposerwidget.h @@ -1,58 +1,48 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2017 Andrea Scarpino This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see http://www.gnu.org/licenses/ */ -#ifndef MASTODONPOSTWIDGET_H -#define MASTODONPOSTWIDGET_H +#ifndef MASTODONCOMPOSERWIDGET_H +#define MASTODONCOMPOSERWIDGET_H -#include "postwidget.h" +#include "composerwidget.h" -class MastodonPostWidget : public Choqok::UI::PostWidget +class MastodonComposerWidget : public Choqok::UI::ComposerWidget { Q_OBJECT public: - explicit MastodonPostWidget(Choqok::Account *account, Choqok::Post *post, QWidget *parent = 0); - virtual ~MastodonPostWidget(); - - virtual QString generateSign() override; - - virtual void initUi() override; + explicit MastodonComposerWidget(Choqok::Account *account, QWidget *parent = nullptr); + ~MastodonComposerWidget(); protected Q_SLOTS: - virtual void slotResendPost() override; - - void slotToggleFavorite(Choqok::Account *, Choqok::Post *); - - void toggleFavorite(); + virtual void submitPost(const QString &text) override; + virtual void slotPostSubmited(Choqok::Account *theAccount, Choqok::Post *post) override; -protected: - virtual QString getUsernameHyperlink(const Choqok::User &user) const; - - static const QIcon unFavIcon; + void cancelAttach(); + void attachMedia(); private: - void updateFavStat(); - class Private; Private *const d; + }; -#endif // MASTODONPOSTWIDGET_H +#endif // MASTODONCOMPOSERWIDGET_H diff --git a/microblogs/mastodon/mastodondmessagedialog.cpp b/microblogs/mastodon/mastodondmessagedialog.cpp new file mode 100644 index 00000000..7a6adb74 --- /dev/null +++ b/microblogs/mastodon/mastodondmessagedialog.cpp @@ -0,0 +1,190 @@ +/* + This file is part of Choqok, the KDE micro-blogging client + + Copyright (C) 2017 Andrea Scarpino + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see http://www.gnu.org/licenses/ + +*/ + +#include "mastodondmessagedialog.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "choqoktextedit.h" +#include "microblog.h" +#include "notifymanager.h" + +#include "mastodonaccount.h" +#include "mastodondebug.h" +#include "mastodonmicroblog.h" + +class MastodonDMessageDialog::Private +{ +public: + Private(MastodonAccount *theAccount) + : account(theAccount) + {} + QComboBox *comboFriendsList; + Choqok::UI::TextEdit *editor; + MastodonAccount *account; + Choqok::Post *sentPost; +}; + +MastodonDMessageDialog::MastodonDMessageDialog(MastodonAccount *theAccount, QWidget *parent, + Qt::WindowFlags flags) + : QDialog(parent, flags), d(new Private(theAccount)) +{ + setWindowTitle(i18n("Send Private Message")); + setAttribute(Qt::WA_DeleteOnClose); + setupUi(this); + KConfigGroup grp(KSharedConfig::openConfig(), "Mastodon"); + resize(grp.readEntry("DMessageDialogSize", QSize(300, 200))); + QStringList list = theAccount->followers(); + if (list.isEmpty()) { + reloadFriendslist(); + } else { + list.sort(Qt::CaseInsensitive); + d->comboFriendsList->addItems(list); + } +} + +MastodonDMessageDialog::~MastodonDMessageDialog() +{ + KConfigGroup grp(KSharedConfig::openConfig(), "Mastodon"); + grp.writeEntry("DMessageDialogSize", size()); + grp.sync(); + delete d; +} + +void MastodonDMessageDialog::setupUi(QWidget *mainWidget) +{ + QLabel *lblTo = new QLabel(i18nc("Send message to", "To:"), this); + d->comboFriendsList = new QComboBox(this); + d->comboFriendsList->setDuplicatesEnabled(false); + + QPushButton *btnReload = new QPushButton(this); + btnReload->setToolTip(i18n("Reload friends list")); + btnReload->setIcon(QIcon::fromTheme(QLatin1String("view-refresh"))); + btnReload->setMaximumWidth(25); + connect(btnReload, &QPushButton::clicked, this, &MastodonDMessageDialog::reloadFriendslist); + + QVBoxLayout *mainLayout = new QVBoxLayout(mainWidget); + + QHBoxLayout *toLayout = new QHBoxLayout; + toLayout->addWidget(lblTo); + toLayout->addWidget(d->comboFriendsList); + toLayout->addWidget(btnReload); + mainLayout->addLayout(toLayout); + + d->editor = new Choqok::UI::TextEdit(d->account->postCharLimit()); + connect(d->editor, &Choqok::UI::TextEdit::returnPressed, this, &MastodonDMessageDialog::submitPost); + mainLayout->addWidget(d->editor); + d->editor->setFocus(); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); + okButton->setDefault(true); + okButton->setShortcut(Qt::CTRL | Qt::Key_Return); + okButton->setText(i18nc("Send private message", "Send")); + connect(buttonBox, &QDialogButtonBox::accepted, this, &MastodonDMessageDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &MastodonDMessageDialog::reject); + mainLayout->addWidget(buttonBox); +} + +void MastodonDMessageDialog::setFriends(const QStringList friends) +{ + d->comboFriendsList->clear(); + d->comboFriendsList->addItems(friends); +} + +Choqok::UI::TextEdit *MastodonDMessageDialog::editor() +{ + return d->editor; +} + +MastodonAccount *MastodonDMessageDialog::account() +{ + return d->account; +} + +void MastodonDMessageDialog::reloadFriendslist() +{ + d->comboFriendsList->clear(); + MastodonMicroBlog *blog = qobject_cast(d->account->microblog()); + if (blog) { + connect(blog, &MastodonMicroBlog::followersUsernameListed, + this, &MastodonDMessageDialog::followersUsernameListed); + blog->fetchFollowers(d->account, true); + d->comboFriendsList->setCurrentText(i18n("Please wait...")); + } +} + +void MastodonDMessageDialog::accept() +{ + submitPost(d->editor->toPlainText()); +} + +void MastodonDMessageDialog::submitPost(QString text) +{ + if (d->account->following().isEmpty() || text.isEmpty() || d->comboFriendsList->currentText().isEmpty()) { + return; + } + hide(); + connect(d->account->microblog(), &Choqok::MicroBlog::errorPost, + this, &MastodonDMessageDialog::errorPost); + connect(d->account->microblog(), SIGNAL(postCreated(Choqok::Account*,Choqok::Post*)), + this, SLOT(postCreated(Choqok::Account*,Choqok::Post*))); + d->sentPost = new Choqok::Post; + d->sentPost->isPrivate = true; + d->sentPost->replyToUser.userName = d->comboFriendsList->currentText(); + d->sentPost->content = text; + d->account->microblog()->createPost(d->account, d->sentPost); +} + +void MastodonDMessageDialog::followersUsernameListed(MastodonAccount *theAccount, QStringList list) +{ + if (theAccount == d->account) { + d->comboFriendsList->clear(); + list.sort(Qt::CaseInsensitive); + d->comboFriendsList->addItems(list); + } +} + +void MastodonDMessageDialog::errorPost(Choqok::Account *theAccount, Choqok::Post *thePost, + Choqok::MicroBlog::ErrorType , QString , + Choqok::MicroBlog::ErrorLevel) +{ + if (theAccount == d->account && thePost == d->sentPost) { + qCDebug(CHOQOK); + show(); + } +} + +void MastodonDMessageDialog::setTo(const QString &username) +{ + d->comboFriendsList->setCurrentText(username); +} + diff --git a/microblogs/mastodon/mastodonpostwidget.h b/microblogs/mastodon/mastodondmessagedialog.h similarity index 53% copy from microblogs/mastodon/mastodonpostwidget.h copy to microblogs/mastodon/mastodondmessagedialog.h index 569d3244..0b139643 100644 --- a/microblogs/mastodon/mastodonpostwidget.h +++ b/microblogs/mastodon/mastodondmessagedialog.h @@ -1,58 +1,72 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2017 Andrea Scarpino This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see http://www.gnu.org/licenses/ + */ -#ifndef MASTODONPOSTWIDGET_H -#define MASTODONPOSTWIDGET_H +#ifndef MASTODONDMESSAGEDIALOG_H +#define MASTODONDMESSAGEDIALOG_H + +#include -#include "postwidget.h" +#include "microblog.h" -class MastodonPostWidget : public Choqok::UI::PostWidget +namespace Choqok { - Q_OBJECT -public: - explicit MastodonPostWidget(Choqok::Account *account, Choqok::Post *post, QWidget *parent = 0); - virtual ~MastodonPostWidget(); +class Account; +class Post; - virtual QString generateSign() override; + namespace UI + { + class TextEdit; + } +} - virtual void initUi() override; +class MastodonAccount; -protected Q_SLOTS: - virtual void slotResendPost() override; +class MastodonDMessageDialog : public QDialog +{ + Q_OBJECT +public: + explicit MastodonDMessageDialog(MastodonAccount *theAccount, QWidget *parent = 0, Qt::WindowFlags flags = 0); + ~MastodonDMessageDialog(); + void setTo(const QString &username); - void slotToggleFavorite(Choqok::Account *, Choqok::Post *); +protected Q_SLOTS: + virtual void accept() override; - void toggleFavorite(); + void followersUsernameListed(MastodonAccount *, QStringList); + void submitPost(QString); + void reloadFriendslist(); + void errorPost(Choqok::Account *, Choqok::Post *, Choqok::MicroBlog::ErrorType, + QString, Choqok::MicroBlog::ErrorLevel); protected: - virtual QString getUsernameHyperlink(const Choqok::User &user) const; - - static const QIcon unFavIcon; + void setupUi(QWidget *mainWidget); + void setFriends(const QStringList friends); + Choqok::UI::TextEdit *editor(); + MastodonAccount *account(); private: - void updateFavStat(); - class Private; Private *const d; }; -#endif // MASTODONPOSTWIDGET_H +#endif // MASTODONDMESSAGEDIALOG_H diff --git a/microblogs/mastodon/mastodoneditaccountwidget.cpp b/microblogs/mastodon/mastodoneditaccountwidget.cpp index 5599cd8a..c35579fe 100644 --- a/microblogs/mastodon/mastodoneditaccountwidget.cpp +++ b/microblogs/mastodon/mastodoneditaccountwidget.cpp @@ -1,222 +1,222 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2017 Andrea Scarpino This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see http://www.gnu.org/licenses/ */ #include "mastodoneditaccountwidget.h" #include #include #include #include #include #include #include #include #include #include #include "choqoktools.h" #include "accountmanager.h" #include "mastodonaccount.h" #include "mastodondebug.h" #include "mastodonmicroblog.h" #include "mastodonoauth.h" MastodonEditAccountWidget::MastodonEditAccountWidget(MastodonMicroBlog *microblog, MastodonAccount *account, QWidget *parent): ChoqokEditAccountWidget(account, parent) , m_account(account) { setupUi(this); connect(kcfg_authorize, &QPushButton::clicked, this, &MastodonEditAccountWidget::authorizeUser); if (m_account) { kcfg_alias->setText(m_account->alias()); - kcfg_acct->setText(m_account->acct()); + kcfg_acct->setText(m_account->username()); setAuthenticated(!m_account->tokenSecret().isEmpty()); } else { setAuthenticated(false); QString newAccountAlias = microblog->serviceName(); const QString servName = newAccountAlias; int counter = 1; while (Choqok::AccountManager::self()->findAccount(newAccountAlias)) { newAccountAlias = QStringLiteral("%1%2").arg(servName).arg(counter); counter++; } m_account = new MastodonAccount(microblog, newAccountAlias); setAccount(m_account); kcfg_alias->setText(newAccountAlias); } loadTimelinesTable(); } MastodonEditAccountWidget::~MastodonEditAccountWidget() { } Choqok::Account *MastodonEditAccountWidget::apply() { m_account->setAlias(kcfg_alias->text()); - m_account->setAcct(kcfg_acct->text()); + m_account->setUsername(MastodonMicroBlog::userNameFromAcct(kcfg_acct->text())); m_account->setTokenSecret(m_account->oAuth()->token()); m_account->writeConfig(); saveTimelinesTable(); return m_account; } void MastodonEditAccountWidget::authorizeUser() { qCDebug(CHOQOK); if (kcfg_acct->text().isEmpty() || !kcfg_acct->text().contains(QLatin1Char('@'))) { return; } if (m_account->consumerKey().isEmpty() || m_account->consumerSecret().isEmpty()) { registerClient(); } connect(m_account->oAuth(), &QAbstractOAuth::authorizeWithBrowser, &Choqok::openUrl); connect(m_account->oAuth(), &QAbstractOAuth::statusChanged, this, &MastodonEditAccountWidget::gotToken); m_account->oAuth()->grant(); QString verifier = QInputDialog::getText(this, i18n("coe"), i18n("Enter the code received from %1", m_account->host())); if (verifier.isEmpty()) { return; } m_account->oAuth()->getToken(verifier); } void MastodonEditAccountWidget::gotToken() { isAuthenticated = false; if (m_account->oAuth()->status() == QAbstractOAuth::Status::Granted) { setAuthenticated(true); KMessageBox::information(this, i18n("Choqok is authorized successfully."), i18n("Authorized")); } else { KMessageBox::detailedError(this, i18n("Authorization Error"), i18n("OAuth authorization error")); } } bool MastodonEditAccountWidget::validateData() { if (kcfg_alias->text().isEmpty() || kcfg_acct->text().isEmpty() || !kcfg_acct->text().contains(QLatin1Char('@')) || !isAuthenticated) { return false; } else { return true; } } void MastodonEditAccountWidget::setAuthenticated(bool authenticated) { isAuthenticated = authenticated; if (authenticated) { kcfg_authorize->setIcon(QIcon::fromTheme(QLatin1String("object-unlocked"))); kcfg_authenticateLed->on(); kcfg_authenticateStatus->setText(i18n("Authenticated")); } else { kcfg_authorize->setIcon(QIcon::fromTheme(QLatin1String("object-locked"))); kcfg_authenticateLed->off(); kcfg_authenticateStatus->setText(i18n("Not Authenticated")); } } void MastodonEditAccountWidget::loadTimelinesTable() { for (const QString &timeline: m_account->microblog()->timelineNames()) { int newRow = timelinesTable->rowCount(); timelinesTable->insertRow(newRow); timelinesTable->setItem(newRow, 0, new QTableWidgetItem(timeline)); QCheckBox *enable = new QCheckBox(timelinesTable); enable->setChecked(m_account->timelineNames().contains(timeline)); timelinesTable->setCellWidget(newRow, 1, enable); } } void MastodonEditAccountWidget::registerClient() { if (kcfg_acct->text().contains(QLatin1Char('@'))) { - m_account->setUsername(kcfg_acct->text().split(QLatin1Char('@'))[0]); - m_account->setHost(QLatin1String("https://") + kcfg_acct->text().split(QLatin1Char('@'))[1]); + m_account->setUsername(MastodonMicroBlog::userNameFromAcct(kcfg_acct->text())); + m_account->setHost(QLatin1String("https://") + MastodonMicroBlog::hostFromAcct(kcfg_acct->text())); m_account->oAuth()->setAccessTokenUrl(QUrl(m_account->host() + QLatin1String("/oauth/token"))); m_account->oAuth()->setAuthorizationUrl(QUrl(m_account->host() + QLatin1String("/oauth/authorize"))); QUrl url(m_account->host() + QLatin1String("/api/v1/apps")); QByteArray data; data += "client_name=" + QCoreApplication::applicationName().toLatin1(); data += "&redirect_uris=" + QUrl::toPercentEncoding(QLatin1String("urn:ietf:wg:oauth:2.0:oob")); data += "&scopes=" + QUrl::toPercentEncoding(QLatin1String("read write follow")); data += "&website=" + QUrl::toPercentEncoding(QLatin1String("https://choqok.kde.org/")); KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo); if (!job) { qCDebug(CHOQOK) << "Cannot create an http POST request!"; return; } job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/x-www-form-urlencoded")); QEventLoop loop; connect(job, &KIO::StoredTransferJob::result, &loop, &QEventLoop::quit); job->start(); loop.exec(); if (job->error()) { qCDebug(CHOQOK) << "An error occurred in Job"; return; } else { KIO::StoredTransferJob *stj = qobject_cast(job); const QJsonDocument json = QJsonDocument::fromJson(stj->data()); if (!json.isNull()) { const QVariantMap result = json.toVariant().toMap(); m_account->setConsumerKey(result[QLatin1String("client_id")].toString()); m_account->setConsumerSecret(result[QLatin1String("client_secret")].toString()); m_account->oAuth()->setClientIdentifier(m_account->consumerKey()); m_account->oAuth()->setClientIdentifierSharedKey(m_account->consumerSecret()); } else { qCDebug(CHOQOK) << "Cannot parse JSON reply"; } } } else { qCDebug(CHOQOK) << "username is not valid"; } } void MastodonEditAccountWidget::saveTimelinesTable() { QStringList timelines; for (int i = 0; i < timelinesTable->rowCount(); ++i) { QCheckBox *enable = qobject_cast(timelinesTable->cellWidget(i, 1)); if (enable && enable->isChecked()) { timelines.append(timelinesTable->item(i, 0)->text()); } } //m_account->setTimelineNames(timelines); } diff --git a/microblogs/mastodon/mastodonmicroblog.cpp b/microblogs/mastodon/mastodonmicroblog.cpp index d4643d0b..8a777820 100644 --- a/microblogs/mastodon/mastodonmicroblog.cpp +++ b/microblogs/mastodon/mastodonmicroblog.cpp @@ -1,782 +1,990 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2017 Andrea Scarpino This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see http://www.gnu.org/licenses/ */ #include "mastodonmicroblog.h" #include #include #include #include #include #include #include #include #include #include "accountmanager.h" #include "application.h" #include "choqokappearancesettings.h" #include "choqokbehaviorsettings.h" #include "notifymanager.h" #include "postwidget.h" #include "mastodonaccount.h" +#include "mastodoncomposerwidget.h" #include "mastodondebug.h" +#include "mastodondmessagedialog.h" #include "mastodoneditaccountwidget.h" #include "mastodonpost.h" #include "mastodonpostwidget.h" class MastodonMicroBlog::Private { public: Private(): countOfTimelinesToSave(0) {} int countOfTimelinesToSave; }; K_PLUGIN_FACTORY_WITH_JSON(MastodonMicroBlogFactory, "choqok_mastodon.json", registerPlugin < MastodonMicroBlog > ();) const QString MastodonMicroBlog::homeTimeline(QLatin1String("/api/v1/timelines/home")); const QString MastodonMicroBlog::publicTimeline(QLatin1String("/api/v1/timelines/public")); const QString MastodonMicroBlog::favouritesTimeline(QLatin1String("/api/v1/favourites")); MastodonMicroBlog::MastodonMicroBlog(QObject *parent, const QVariantList &args): MicroBlog(QStringLiteral("Mastodon") , parent), d(new Private) { Q_UNUSED(args) setServiceName(QLatin1String("Mastodon")); setServiceHomepageUrl(QLatin1String("https://mastodon.social")); QStringList timelineNames; timelineNames << QLatin1String("Home") << QLatin1String("Local") << QLatin1String("Federated") << QLatin1String("Favourites"); setTimelineNames(timelineNames); setTimelinesInfo(); } MastodonMicroBlog::~MastodonMicroBlog() { qDeleteAll(m_timelinesInfos); delete d; } void MastodonMicroBlog::aboutToUnload() { for (Choqok::Account *acc: Choqok::AccountManager::self()->accounts()) { if (acc->microblog() == this) { d->countOfTimelinesToSave += acc->timelineNames().count(); } } Q_EMIT saveTimelines(); } ChoqokEditAccountWidget *MastodonMicroBlog::createEditAccountWidget(Choqok::Account *account, QWidget *parent) { MastodonAccount *acc = qobject_cast(account); if (acc || !account) { return new MastodonEditAccountWidget(this, acc, parent); } else { qCDebug(CHOQOK) << "Account passed here was not a valid MastodonAccount!"; return 0; } } +Choqok::UI::ComposerWidget *MastodonMicroBlog::createComposerWidget(Choqok::Account *account, QWidget *parent) +{ + return new MastodonComposerWidget(account, parent); +} + void MastodonMicroBlog::createPost(Choqok::Account *theAccount, Choqok::Post *post) { if (!post || post->content.isEmpty()) { qCDebug(CHOQOK) << "ERROR: Status text is empty!"; Q_EMIT errorPost(theAccount, post, Choqok::MicroBlog::OtherError, i18n("Creating the new post failed. Text is empty."), MicroBlog::Critical); return; } MastodonAccount *acc = qobject_cast(theAccount); if (acc) { QVariantMap object; object.insert(QLatin1String("status"), post->content); const QByteArray data = QJsonDocument::fromVariant(object).toJson(); QUrl url(acc->host()); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QLatin1String("/api/v1/statuses")); KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo); job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json")); job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); if (!job) { qCDebug(CHOQOK) << "Cannot create an http POST request!"; return; } m_accountJobs[job] = acc; m_createPostJobs[job] = post; connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotCreatePost); job->start(); } else { qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; } } Choqok::Account *MastodonMicroBlog::createNewAccount(const QString &alias) { MastodonAccount *acc = qobject_cast( Choqok::AccountManager::self()->findAccount(alias)); if (!acc) { return new MastodonAccount(this, alias); } else { qCDebug(CHOQOK) << "Cannot create a new MastodonAccount!"; return 0; } } QString MastodonMicroBlog::lastTimelineId(Choqok::Account *theAccount, const QString &timeline) const { qCDebug(CHOQOK) << "Latest ID for timeline " << timeline << m_timelinesLatestIds[theAccount][timeline]; return m_timelinesLatestIds[theAccount][timeline]; } QList< Choqok::Post * > MastodonMicroBlog::readTimeline(const QByteArray &buffer) { QList posts; const QJsonDocument json = QJsonDocument::fromJson(buffer); if (!json.isNull()) { const QVariantList list = json.array().toVariantList(); for (const QVariant &element: list) { posts.prepend(readPost(element.toMap(), new MastodonPost)); } } else { qCDebug(CHOQOK) << "Cannot parse JSON reply"; } return posts; } Choqok::Post *MastodonMicroBlog::readPost(const QVariantMap &var, Choqok::Post *post) { MastodonPost *p = dynamic_cast< MastodonPost * >(post); if (p) { QVariantMap reblog = var[QLatin1String("reblog")].toMap(); QVariantMap status; if (reblog.isEmpty()) { status = var; } else { status = reblog; } QTextDocument content; content.setHtml(status[QLatin1String("spoiler_text")].toString() + QLatin1String("
") + status[QLatin1String("content")].toString()); p->content += content.toPlainText().trimmed(); p->creationDateTime = QDateTime::fromString(var[QLatin1String("created_at")].toString(), Qt::ISODate); p->creationDateTime.setTimeSpec(Qt::UTC); p->link = status[QLatin1String("url")].toUrl(); p->isFavorited = var[QLatin1String("favourited")].toBool(); if (p->isFavorited) { p->isRead = true; } p->postId = var[QLatin1String("id")].toString(); p->conversationId = var[QLatin1String("id")].toString(); QVariantMap application = var[QLatin1String("application")].toMap(); if (!application.isEmpty()) { const QString client = application[QLatin1String("name")].toString(); if (application[QLatin1String("website")].toString().isEmpty()) { p->source = client; } else { p->source = QStringLiteral("%2").arg(application[QLatin1String("website")].toString()).arg(client); } } if (var[QLatin1String("visibility")].toString().compare(QLatin1String("direct")) == 0) { p->isPrivate = true; } QVariantMap account = status[QLatin1Literal("account")].toMap(); - p->author.userId = account[QLatin1String("acct")].toString(); - p->author.userName = account[QLatin1String("username")].toString(); + p->author.userId = account[QLatin1String("id")].toString(); + p->author.userName = account[QLatin1String("acct")].toString(); p->author.realName = account[QLatin1String("display_name")].toString(); p->author.homePageUrl = account[QLatin1String("url")].toUrl(); QTextDocument description; description.setHtml(account[QLatin1String("note")].toString()); p->author.description = description.toPlainText().trimmed(); p->author.profileImageUrl = account[QLatin1String("avatar")].toUrl(); p->replyToPostId = var[QLatin1String("in_reply_to_id")].toString(); p->replyToUser.userId = var[QLatin1String("in_reply_to_account_id")].toString(); if (!reblog.isEmpty()) { p->repeatedDateTime = QDateTime::fromString(var[QLatin1String("created_at")].toString(), Qt::ISODate); p->repeatedDateTime.setTimeSpec(Qt::UTC); p->repeatedPostId = var[QLatin1String("id")].toString(); const QVariantMap repeatedFrom = var[QLatin1Literal("account")].toMap(); - p->repeatedFromUser.userId = repeatedFrom[QLatin1String("acct")].toString(); - p->repeatedFromUser.userName = repeatedFrom[QLatin1String("username")].toString(); + p->repeatedFromUser.userId = repeatedFrom[QLatin1String("id")].toString(); + p->repeatedFromUser.userName = repeatedFrom[QLatin1String("acct")].toString(); p->repeatedFromUser.homePageUrl = repeatedFrom[QLatin1String("url")].toUrl(); } return p; } else { qCDebug(CHOQOK) << "post is not a MastodonPost!"; return post; } } +void MastodonMicroBlog::createReply(Choqok::Account *theAccount, MastodonPost *post) +{ + MastodonAccount *acc = qobject_cast(theAccount); + if (acc) { + QVariantMap object; + object.insert(QLatin1String("status"), post->content); + + if (!post->replyToPostId.isEmpty()) { + object.insert(QLatin1String("in_reply_to_id"), post->replyToPostId); + } + + const QByteArray data = QJsonDocument::fromVariant(object).toJson(); + + QUrl url(acc->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + url.setPath(url.path() + QLatin1String("/api/v1/statuses")); + KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo); + job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json")); + job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); + if (!job) { + qCDebug(CHOQOK) << "Cannot create an http POST request!"; + return; + } + m_accountJobs[job] = acc; + m_createPostJobs[job] = post; + connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotCreatePost); + job->start(); + } else { + qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; + } +} + void MastodonMicroBlog::toggleReblog(Choqok::Account *theAccount, Choqok::Post *post) { MastodonAccount *acc = qobject_cast(theAccount); if (acc) { QUrl url(acc->host()); url = url.adjusted(QUrl::StripTrailingSlash); if (acc->username().compare(post->repeatedFromUser.userName) == 0) { url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/unreblog").arg(post->postId)); } else { url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/reblog").arg(post->postId)); } KIO::StoredTransferJob *job = KIO::storedHttpPost(QByteArray(), url, KIO::HideProgressInfo); job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json")); job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); if (!job) { qCDebug(CHOQOK) << "Cannot create an http POST request!"; return; } m_accountJobs[job] = acc; m_shareJobs[job] = post; connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotReblog); job->start(); } else { qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; } } void MastodonMicroBlog::slotReblog(KJob *job) { qCDebug(CHOQOK); if (!job) { qCDebug(CHOQOK) << "Job is null pointer"; return; } Choqok::Post *post = m_shareJobs.take(job); Choqok::Account *theAccount = m_accountJobs.take(job); if (!post || !theAccount) { qCDebug(CHOQOK) << "Account or Post is NULL pointer"; return; } int ret = 1; if (job->error()) { qCDebug(CHOQOK) << "Job Error:" << job->errorString(); } else { - Choqok::UI::Global::mainWindow()->showStatusMessage( - i18n("The post has been shared.")); + Choqok::UI::Global::mainWindow()->showStatusMessage(i18n("The post has been shared.")); KIO::StoredTransferJob *j = qobject_cast(job); const QJsonDocument json = QJsonDocument::fromJson(j->data()); if (!json.isNull()) { ret = 0; } else { qCDebug(CHOQOK) << "Cannot parse JSON reply"; } } if (ret) { Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError, i18n("Cannot share the post. %1", job->errorString())); } } void MastodonMicroBlog::toggleFavorite(Choqok::Account *theAccount, Choqok::Post *post) { MastodonAccount *acc = qobject_cast(theAccount); if (acc) { QUrl url(acc->host()); url = url.adjusted(QUrl::StripTrailingSlash); if (post->isFavorited) { url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/unfavourite").arg(post->postId)); } else { url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1/favourite").arg(post->postId)); } KIO::StoredTransferJob *job = KIO::storedHttpPost(QByteArray(), url, KIO::HideProgressInfo); job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json")); job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); if (!job) { qCDebug(CHOQOK) << "Cannot create an http POST request!"; return; } m_accountJobs[job] = acc; m_favoriteJobs[job] = post; connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotFavorite); job->start(); } else { qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; } } void MastodonMicroBlog::slotFavorite(KJob *job) { qCDebug(CHOQOK); if (!job) { qCDebug(CHOQOK) << "Job is null pointer"; return; } Choqok::Post *post = m_favoriteJobs.take(job); Choqok::Account *theAccount = m_accountJobs.take(job); if (!post || !theAccount) { qCDebug(CHOQOK) << "Account or Post is NULL pointer"; return; } if (job->error()) { qCDebug(CHOQOK) << "Job Error:" << job->errorString(); Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError, i18n("Cannot set/unset the post as favorite. %1", job->errorString())); } else { post->isFavorited = !post->isFavorited; Q_EMIT favorite(theAccount, post); } } void MastodonMicroBlog::setLastTimelineId(Choqok::Account *theAccount, const QString &timeline, const QString &id) { m_timelinesLatestIds[theAccount][timeline] = id; } void MastodonMicroBlog::setTimelinesInfo() { Choqok::TimelineInfo *t = new Choqok::TimelineInfo; t->name = i18nc("Timeline Name", "Home"); t->description = i18nc("Timeline description", "You and people you follow"); t->icon = QLatin1String("user-home"); m_timelinesInfos[QLatin1String("Home")] = t; m_timelinesPaths[QLatin1String("Home")] = homeTimeline; t = new Choqok::TimelineInfo; t->name = i18nc("Timeline Name", "Local"); t->description = i18nc("Timeline description", "Local timeline"); t->icon = QLatin1String("folder-public"); m_timelinesInfos[QLatin1String("Local")] = t; m_timelinesPaths[QLatin1String("Local")] = publicTimeline; t = new Choqok::TimelineInfo; t->name = i18nc("Timeline Name", "Federated"); t->description = i18nc("Timeline description", "Federated timelime"); t->icon = QLatin1String("folder-remote"); m_timelinesInfos[QLatin1String("Federated")] = t; m_timelinesPaths[QLatin1String("Federated")] = publicTimeline; t = new Choqok::TimelineInfo; t->name = i18nc("Timeline Name", "Favourites"); t->description = i18nc("Timeline description", "Favourites"); t->icon = QLatin1String("favorites"); m_timelinesInfos[QLatin1String("Favourites")] = t; m_timelinesPaths[QLatin1String("Favourites")] = favouritesTimeline; } void MastodonMicroBlog::removePost(Choqok::Account *theAccount, Choqok::Post *post) { MastodonAccount *acc = qobject_cast(theAccount); if (acc) { QUrl url(acc->host()); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QStringLiteral("/api/v1/statuses/%1").arg(post->postId)); KIO::TransferJob *job = KIO::http_delete(url, KIO::HideProgressInfo); job->addMetaData(QLatin1String("content-type"), QLatin1String("Content-Type: application/json")); job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); if (!job) { qCDebug(CHOQOK) << "Cannot create an http POST request!"; return; } m_accountJobs[job] = acc; m_removePostJobs[job] = post; connect(job, &KIO::TransferJob::result, this, &MastodonMicroBlog::slotRemovePost); job->start(); } else { qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; } } QList MastodonMicroBlog::loadTimeline(Choqok::Account *account, const QString &timelineName) { QList< Choqok::Post * > list; const QString fileName = Choqok::AccountManager::generatePostBackupFileName(account->alias(), timelineName); const KConfig postsBackup(fileName, KConfig::NoGlobals, QStandardPaths::DataLocation); const QStringList tmpList = postsBackup.groupList(); // don't load old archives if (tmpList.isEmpty() || !(QDateTime::fromString(tmpList.first()).isValid())) { return list; } QList groupList; for (const QString &str: tmpList) { groupList.append(QDateTime::fromString(str)); } qSort(groupList); MastodonPost *st; for (const QDateTime &datetime: groupList) { st = new MastodonPost; KConfigGroup grp(&postsBackup, datetime.toString()); st->creationDateTime = grp.readEntry("creationDateTime", QDateTime::currentDateTime()); st->postId = grp.readEntry("postId", QString()); st->link = grp.readEntry("link", QUrl()); st->content = grp.readEntry("content", QString()); st->source = grp.readEntry("source", QString()); st->isFavorited = grp.readEntry("favorited", false); st->author.userId = grp.readEntry("authorId", QString()); st->author.userName = grp.readEntry("authorUserName", QString()); st->author.realName = grp.readEntry("authorRealName", QString()); st->author.description = grp.readEntry("authorDescription" , QString()); st->author.profileImageUrl = grp.readEntry("authorProfileImageUrl", QUrl()); st->author.homePageUrl = grp.readEntry("authorHomePageUrl", QUrl()); st->isRead = grp.readEntry("isRead", true); st->conversationId = grp.readEntry("conversationId", QString()); st->replyToPostId = grp.readEntry("replyToPostId", QString()); st->replyToUser.userId = grp.readEntry("replyToUserId", QString()); st->repeatedFromUser.userId = grp.readEntry("repeatedFromUserId", QString()); st->repeatedFromUser.userName = grp.readEntry("repeatedFromUserName", QString()); st->repeatedFromUser.homePageUrl = grp.readEntry("repeatedFromUserHomePage", QUrl()); st->repeatedPostId = grp.readEntry("repeatedPostId", QString()); st->repeatedDateTime = grp.readEntry("repeatedDateTime", QDateTime()); list.append(st); } if (!list.isEmpty()) { setLastTimelineId(account, timelineName, list.last()->conversationId); } return list; } QUrl MastodonMicroBlog::profileUrl(Choqok::Account *account, const QString &username) const { if (username.contains(QLatin1Char('@'))) { return QUrl::fromUserInput(QStringLiteral("https://%1/@%2").arg(hostFromAcct(username)).arg(userNameFromAcct(username))); } else { MastodonAccount *acc = qobject_cast(account); QUrl url(acc->host()); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(QLatin1String("/@") + username); return url; } } QString MastodonMicroBlog::generateRepeatedByUserTooltip(const QString &username) const { if (Choqok::AppearanceSettings::showRetweetsInChoqokWay()) { return i18n("Boost of %1", username); } else { return i18n("Boosted by %1", username); } } +void MastodonMicroBlog::showDirectMessageDialog(MastodonAccount *theAccount, const QString &toUsername) +{ + qCDebug(CHOQOK); + if (!theAccount) { + QAction *act = qobject_cast(sender()); + theAccount = qobject_cast( + Choqok::AccountManager::self()->findAccount(act->data().toString())); + } + MastodonDMessageDialog *dmsg = new MastodonDMessageDialog(theAccount, Choqok::UI::Global::mainWindow()); + if (!toUsername.isEmpty()) { + dmsg->setTo(toUsername); + } + dmsg->show(); +} + QString MastodonMicroBlog::hostFromAcct(const QString &acct) { if (acct.contains(QLatin1Char('@'))) { return acct.split(QLatin1Char('@'))[1]; } else { return acct; } } QString MastodonMicroBlog::userNameFromAcct(const QString &acct) { if (acct.contains(QLatin1Char('@'))) { return acct.split(QLatin1Char('@'))[0]; } else { return acct; } } void MastodonMicroBlog::saveTimeline(Choqok::Account *account, const QString &timelineName, const QList< Choqok::UI::PostWidget * > &timeline) { const QString fileName = Choqok::AccountManager::generatePostBackupFileName(account->alias(), timelineName); KConfig postsBackup(fileName, KConfig::NoGlobals, QStandardPaths::DataLocation); ///Clear previous data: for (const QString &group: postsBackup.groupList()) { postsBackup.deleteGroup(group); } for (Choqok::UI::PostWidget *wd: timeline) { MastodonPost *post = dynamic_cast(wd->currentPost()); KConfigGroup grp(&postsBackup, post->creationDateTime.toString()); grp.writeEntry("creationDateTime", post->creationDateTime); grp.writeEntry("postId", post->postId); grp.writeEntry("link", post->link); grp.writeEntry("content", post->content); grp.writeEntry("source", post->source); grp.writeEntry("favorited", post->isFavorited); grp.writeEntry("authorId", post->author.userId); grp.writeEntry("authorRealName", post->author.realName); grp.writeEntry("authorUserName", post->author.userName); grp.writeEntry("authorDescription", post->author.description); grp.writeEntry("authorProfileImageUrl", post->author.profileImageUrl); grp.writeEntry("authorHomePageUrl", post->author.homePageUrl); grp.writeEntry("isRead", post->isRead); grp.writeEntry("conversationId", post->conversationId); grp.writeEntry("replyToPostId", post->replyToPostId); grp.writeEntry("replyToUserId", post->replyToUser.userId); grp.writeEntry("repeatedFromUserId", post->repeatedFromUser.userId); grp.writeEntry("repeatedFromUserName", post->repeatedFromUser.userName); grp.writeEntry("repeatedFromUserHomePage", post->repeatedFromUser.homePageUrl); grp.writeEntry("repeatedPostId", post->repeatedPostId); grp.writeEntry("repeatedDateTime", post->repeatedDateTime); } postsBackup.sync(); if (Choqok::Application::isShuttingDown()) { --d->countOfTimelinesToSave; if (d->countOfTimelinesToSave < 1) { Q_EMIT readyForUnload(); } } } Choqok::TimelineInfo *MastodonMicroBlog::timelineInfo(const QString &timelineName) { return m_timelinesInfos.value(timelineName); } void MastodonMicroBlog::updateTimelines(Choqok::Account *theAccount) { MastodonAccount *acc = qobject_cast(theAccount); if (acc) { for (const QString &timeline: acc->timelineNames()) { QUrl url(acc->host()); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QLatin1Char('/') + m_timelinesPaths[timeline]); QUrlQuery query; if (timeline.compare(QLatin1String("Local")) == 0) { query.addQueryItem(QLatin1String("local"), QLatin1String("true")); } url.setQuery(query); KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo); if (!job) { qCDebug(CHOQOK) << "Cannot create an http GET request!"; continue; } job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); m_timelinesRequests[job] = timeline; m_accountJobs[job] = acc; connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotUpdateTimeline); job->start(); } } else { qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; } } QString MastodonMicroBlog::authorizationMetaData(MastodonAccount *account) const { return QStringLiteral("Authorization: Bearer ") + account->oAuth()->token(); } Choqok::UI::PostWidget *MastodonMicroBlog::createPostWidget(Choqok::Account *account, Choqok::Post *post, QWidget *parent) { return new MastodonPostWidget(account, post, parent); } void MastodonMicroBlog::fetchPost(Choqok::Account *theAccount, Choqok::Post *post) { MastodonAccount *acc = qobject_cast(theAccount); if (acc) { if (!post->link.toDisplayString().startsWith(acc->host())) { qCDebug(CHOQOK) << "You can only fetch posts from your host!"; return; } QUrl url(post->link); KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo); if (!job) { qCDebug(CHOQOK) << "Cannot create an http GET request!"; return; } job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(acc)); m_accountJobs[job] = acc; connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotFetchPost); job->start(); } else { qCDebug(CHOQOK) << "theAccount is not a MastodonAccount!"; } } void MastodonMicroBlog::slotCreatePost(KJob *job) { qCDebug(CHOQOK); if (!job) { qCDebug(CHOQOK) << "Job is null pointer"; return; } Choqok::Post *post = m_createPostJobs.take(job); Choqok::Account *theAccount = m_accountJobs.take(job); if (!post || !theAccount) { qCDebug(CHOQOK) << "Account or Post is NULL pointer"; return; } int ret = 1; if (job->error()) { qCDebug(CHOQOK) << "Job Error:" << job->errorString(); } else { KIO::StoredTransferJob *j = qobject_cast(job); const QJsonDocument json = QJsonDocument::fromJson(j->data()); if (!json.isNull()) { const QVariantMap reply = json.toVariant().toMap(); if (!reply[QLatin1String("id")].toString().isEmpty()) { - Choqok::NotifyManager::success(i18n("New post submitted successfully")); + Choqok::NotifyManager::success(i18n("New post for account %1 submitted successfully.", + theAccount->alias())); ret = 0; Q_EMIT postCreated(theAccount, post); } } else { qCDebug(CHOQOK) << "Cannot parse JSON reply"; } } if (ret) { Q_EMIT errorPost(theAccount, post, Choqok::MicroBlog::CommunicationError, i18n("Creating the new post failed. %1", job->errorString()), MicroBlog::Critical); } } void MastodonMicroBlog::slotFetchPost(KJob *job) { qCDebug(CHOQOK); if (!job) { qCDebug(CHOQOK) << "Job is null pointer"; return; } Choqok::Account *theAccount = m_accountJobs.take(job); if (!theAccount) { qCDebug(CHOQOK) << "Account or postId is NULL pointer"; return; } int ret = 1; if (job->error()) { qCDebug(CHOQOK) << "Job Error:" << job->errorString(); } else { KIO::StoredTransferJob *j = qobject_cast(job); const QJsonDocument json = QJsonDocument::fromJson(j->data()); if (!json.isNull()) { const QVariantMap reply = json.toVariant().toMap(); MastodonPost *post = new MastodonPost; readPost(reply, post); ret = 0; Q_EMIT postFetched(theAccount, post); } else { qCDebug(CHOQOK) << "Cannot parse JSON reply"; } } if (ret) { Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError, i18n("Cannot fetch post. %1", job->errorString()), MicroBlog::Critical); } } void MastodonMicroBlog::slotRemovePost(KJob *job) { qCDebug(CHOQOK); if (!job) { qCDebug(CHOQOK) << "Job is null pointer"; return; } Choqok::Post *post = m_removePostJobs.take(job); Choqok::Account *theAccount = m_accountJobs.take(job); if (!post || !theAccount) { qCDebug(CHOQOK) << "Account or Post is NULL pointer"; return; } int ret = 1; if (job->error()) { qCDebug(CHOQOK) << "Job Error:" << job->errorString(); } else { KIO::TransferJob *j = qobject_cast(job); if (j->metaData().contains(QStringLiteral("responsecode"))) { int responseCode = j->queryMetaData(QStringLiteral("responsecode")).toInt(); if (responseCode == 200 || responseCode == 404) { ret = 0; Q_EMIT postRemoved(theAccount, post); } } } if (ret) { Q_EMIT errorPost(theAccount, post, Choqok::MicroBlog::CommunicationError, i18n("Removing the post failed. %1", job->errorString()), MicroBlog::Critical); } } void MastodonMicroBlog::slotUpdateTimeline(KJob *job) { qCDebug(CHOQOK); if (!job) { qCDebug(CHOQOK) << "Job is null pointer"; return; } Choqok::Account *account = m_accountJobs.take(job); if (!account) { qCDebug(CHOQOK) << "Account or Post is NULL pointer"; return; } if (job->error()) { qCDebug(CHOQOK) << "Job Error:" << job->errorString(); Q_EMIT error(account, Choqok::MicroBlog::CommunicationError, i18n("An error occurred when fetching the timeline")); } else { KIO::StoredTransferJob *j = qobject_cast(job); const QList list = readTimeline(j->data()); const QString timeline(m_timelinesRequests.take(job)); if (!list.isEmpty()) { setLastTimelineId(account, timeline, list.last()->conversationId); } Q_EMIT timelineDataReceived(account, timeline, list); } } +void MastodonMicroBlog::fetchFollowers(MastodonAccount* theAccount, bool active) +{ + qCDebug(CHOQOK); + QUrl url(theAccount->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + url.setPath(url.path() + QStringLiteral("/api/v1/accounts/%1/followers").arg(theAccount->id())); + + QUrlQuery urlQuery; + urlQuery.addQueryItem(QLatin1String("limit"), QLatin1String("80")); + url.setQuery(urlQuery); + + KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo); + if (!job) { + qCDebug(CHOQOK) << "Cannot create an http GET request!"; + return; + } + job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(theAccount)); + mJobsAccount[job] = theAccount; + if (active) { + connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowersScreenNameActive); + } else { + connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowersScreenNamePassive); + } + job->start(); + Choqok::UI::Global::mainWindow()->showStatusMessage(i18n("Updating followers list for account %1...", + theAccount->alias())); +} + +void MastodonMicroBlog::slotRequestFollowersScreenNameActive(KJob* job) +{ + finishRequestFollowersScreenName(job, true); +} + +void MastodonMicroBlog::slotRequestFollowersScreenNamePassive(KJob* job) +{ + finishRequestFollowersScreenName(job, false); +} + +void MastodonMicroBlog::finishRequestFollowersScreenName(KJob *job, bool active) +{ + qCDebug(CHOQOK); + if (!job) { + qCDebug(CHOQOK) << "Job is null pointer"; + return; + } + Choqok::MicroBlog::ErrorLevel level = active ? Critical : Low; + MastodonAccount *account = qobject_cast(mJobsAccount.take(job)); + if (!account) { + qCDebug(CHOQOK) << "Account or Post is NULL pointer"; + return; + } + + if (job->error()) { + qCDebug(CHOQOK) << "Job Error:" << job->errorString(); + Q_EMIT error(account, ServerError, i18n("Followers list for account %1 could not be updated:\n%2", + account->username(), job->errorString()), level); + return; + } else { + KIO::StoredTransferJob *j = qobject_cast(job); + + const QByteArray buffer = j->data(); + const QJsonDocument json = QJsonDocument::fromJson(buffer); + if (!json.isNull()) { + QStringList followers; + for (const QVariant &user: json.array().toVariantList()) { + followers.append(user.toMap()[QLatin1String("acct")].toString()); + } + + account->setFollowers(followers); + } else { + QString err = i18n("Retrieving the followers list failed. The data returned from the server is corrupted."); + qCDebug(CHOQOK) << "JSON parse error:the buffer is: \n" << buffer; + Q_EMIT error(account, ParsingError, err, Critical); + } + } +} + +void MastodonMicroBlog::fetchFollowing(MastodonAccount* theAccount, bool active) +{ + qCDebug(CHOQOK); + QUrl url(theAccount->host()); + url = url.adjusted(QUrl::StripTrailingSlash); + url.setPath(url.path() + QStringLiteral("/api/v1/accounts/%1/following").arg(theAccount->id())); + + QUrlQuery urlQuery; + urlQuery.addQueryItem(QLatin1String("limit"), QLatin1String("80")); + url.setQuery(urlQuery); + + KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo); + if (!job) { + qCDebug(CHOQOK) << "Cannot create an http GET request!"; + return; + } + job->addMetaData(QLatin1String("customHTTPHeader"), authorizationMetaData(theAccount)); + mJobsAccount[job] = theAccount; + if (active) { + connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowingScreenNameActive); + } else { + connect(job, &KIO::StoredTransferJob::result, this, &MastodonMicroBlog::slotRequestFollowingScreenNamePassive); + } + job->start(); + Choqok::UI::Global::mainWindow()->showStatusMessage(i18n("Updating following list for account %1...", + theAccount->alias())); +} + +void MastodonMicroBlog::slotRequestFollowingScreenNameActive(KJob* job) +{ + finishRequestFollowingScreenName(job, true); +} + +void MastodonMicroBlog::slotRequestFollowingScreenNamePassive(KJob* job) +{ + finishRequestFollowingScreenName(job, false); +} + +void MastodonMicroBlog::finishRequestFollowingScreenName(KJob *job, bool active) +{ + qCDebug(CHOQOK); + if (!job) { + qCDebug(CHOQOK) << "Job is null pointer"; + return; + } + Choqok::MicroBlog::ErrorLevel level = active ? Critical : Low; + MastodonAccount *account = qobject_cast(mJobsAccount.take(job)); + if (!account) { + qCDebug(CHOQOK) << "Account or Post is NULL pointer"; + return; + } + + if (job->error()) { + qCDebug(CHOQOK) << "Job Error:" << job->errorString(); + Q_EMIT error(account, ServerError, i18n("Following list for account %1 could not be updated:\n%2", + account->username(), job->errorString()), level); + return; + } else { + KIO::StoredTransferJob *j = qobject_cast(job); + + const QByteArray buffer = j->data(); + const QJsonDocument json = QJsonDocument::fromJson(buffer); + if (!json.isNull()) { + QStringList following; + for (const QVariant &user: json.array().toVariantList()) { + following.append(user.toMap()[QLatin1String("acct")].toString()); + } + + account->setFollowing(following); + } else { + QString err = i18n("Retrieving the following list failed. The data returned from the server is corrupted."); + qCDebug(CHOQOK) << "JSON parse error:the buffer is: \n" << buffer; + Q_EMIT error(account, ParsingError, err, Critical); + } + } +} + #include "mastodonmicroblog.moc" diff --git a/microblogs/mastodon/mastodonmicroblog.h b/microblogs/mastodon/mastodonmicroblog.h index 5830317e..d4308bce 100644 --- a/microblogs/mastodon/mastodonmicroblog.h +++ b/microblogs/mastodon/mastodonmicroblog.h @@ -1,123 +1,148 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2017 Andrea Scarpino This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see http://www.gnu.org/licenses/ */ #ifndef MASTODONMICROBLOG_H #define MASTODONMICROBLOG_H #include #include "microblog.h" class QUrl; class KJob; class MastodonAccount; class MastodonPost; class MastodonMicroBlog : public Choqok::MicroBlog { Q_OBJECT public: explicit MastodonMicroBlog(QObject *parent, const QVariantList &args); virtual ~MastodonMicroBlog(); virtual void aboutToUnload() override; virtual ChoqokEditAccountWidget *createEditAccountWidget(Choqok::Account *account, QWidget *parent) override; + virtual Choqok::UI::ComposerWidget *createComposerWidget(Choqok::Account *account, QWidget *parent) override; + virtual void createPost(Choqok::Account *theAccount, Choqok::Post *post) override; virtual Choqok::Account *createNewAccount(const QString &alias) override; virtual Choqok::UI::PostWidget *createPostWidget(Choqok::Account *account, Choqok::Post *post, QWidget *parent) override; virtual void fetchPost(Choqok::Account *theAccount, Choqok::Post *post) override; virtual QList loadTimeline(Choqok::Account *account, const QString &timelineName) override; virtual void removePost(Choqok::Account *theAccount, Choqok::Post *post) override; virtual QString generateRepeatedByUserTooltip(const QString &username) const; virtual QUrl profileUrl(Choqok::Account *account, const QString &username) const override; virtual void saveTimeline(Choqok::Account *account, const QString &timelineName, const QList< Choqok::UI::PostWidget * > &timeline) override; virtual Choqok::TimelineInfo *timelineInfo(const QString &timelineName) override; virtual void updateTimelines(Choqok::Account *theAccount) override; + void createReply(Choqok::Account *theAccount, MastodonPost *post); + void toggleReblog(Choqok::Account *theAccount, Choqok::Post *post); void toggleFavorite(Choqok::Account *theAccount, Choqok::Post *post); + void fetchFollowers(MastodonAccount *theAccount, bool active); + + void fetchFollowing(MastodonAccount *theAccount, bool active); + static QString userNameFromAcct(const QString &acct); static QString hostFromAcct(const QString &acct); Q_SIGNALS: void favorite(Choqok::Account *, Choqok::Post *); + void followersUsernameListed(MastodonAccount *theAccount, const QStringList &friendsList); + void followingUsernameListed(MastodonAccount *theAccount, const QStringList &friendsList); + +public Q_SLOTS: + virtual void showDirectMessageDialog(MastodonAccount *theAccount = 0, + const QString &toUsername = QString()); + + void slotRequestFollowersScreenNameActive(KJob *job); + void slotRequestFollowersScreenNamePassive(KJob *job); + + void slotRequestFollowingScreenNameActive(KJob *job); + void slotRequestFollowingScreenNamePassive(KJob *job); protected Q_SLOTS: void slotCreatePost(KJob *job); void slotFavorite(KJob *job); void slotFetchPost(KJob *job); void slotReblog(KJob *job); void slotRemovePost(KJob *job); void slotUpdateTimeline(KJob *job); protected: static const QString homeTimeline; static const QString publicTimeline; static const QString favouritesTimeline; QString authorizationMetaData(MastodonAccount *account) const; QString lastTimelineId(Choqok::Account *theAccount, const QString &timeline) const; Choqok::Post *readPost(const QVariantMap &var, Choqok::Post *post); QList readTimeline(const QByteArray &buffer); void setLastTimelineId(Choqok::Account *theAccount, const QString &timeline, const QString &id); void setTimelinesInfo(); + void finishRequestFollowersScreenName(KJob *job, bool active); + + void finishRequestFollowingScreenName(KJob *job, bool active); + QMap m_accountJobs; QMap m_createPostJobs; QMap m_favoriteJobs; QMap m_removePostJobs; QMap m_shareJobs; QMap m_timelinesInfos; QHash > m_timelinesLatestIds; + QMap mJobsAccount; QHash m_timelinesPaths; QMap m_timelinesRequests; private: class Private; Private *const d; }; #endif // MASTODONMICROBLOG_H diff --git a/microblogs/mastodon/mastodonpostwidget.cpp b/microblogs/mastodon/mastodonpostwidget.cpp index 74b06532..93f939a1 100644 --- a/microblogs/mastodon/mastodonpostwidget.cpp +++ b/microblogs/mastodon/mastodonpostwidget.cpp @@ -1,151 +1,201 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2017 Andrea Scarpino This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see http://www.gnu.org/licenses/ */ #include "mastodonpostwidget.h" #include #include #include #include #include "mediamanager.h" #include "textbrowser.h" #include "mastodonaccount.h" #include "mastodondebug.h" #include "mastodonmicroblog.h" #include "mastodonpost.h" const QIcon MastodonPostWidget::unFavIcon(Choqok::MediaManager::convertToGrayScale(QIcon::fromTheme(QLatin1String("rating")).pixmap(16))); class MastodonPostWidget::Private { public: QPushButton *btnFavorite; }; MastodonPostWidget::MastodonPostWidget(Choqok::Account *account, Choqok::Post *post, QWidget *parent): PostWidget(account, post, parent), d(new Private) { } MastodonPostWidget::~MastodonPostWidget() { delete d; } QString MastodonPostWidget::generateSign() { QString ss; MastodonPost *post = dynamic_cast(currentPost()); MastodonAccount *account = qobject_cast(currentAccount()); MastodonMicroBlog *microblog = qobject_cast(account->microblog()); if (post) { ss += QStringLiteral("%1 - ").arg(getUsernameHyperlink(currentPost()->author)); QDateTime time; if (post->repeatedDateTime.isNull()) { time = post->creationDateTime; } else { time = post->repeatedDateTime; } ss += QStringLiteral("%3").arg(post->link.toDisplayString()) .arg(post->creationDateTime.toString(Qt::DefaultLocaleLongDate)) .arg(formatDateTime(time)); if (!post->source.isEmpty()) { ss += QLatin1String(" - ") + post->source; } //ReTweet detection if (!currentPost()->repeatedFromUser.userName.isEmpty()) { const QString retweet = QLatin1String("
") + microblog->generateRepeatedByUserTooltip(QStringLiteral("%2") .arg(currentPost()->repeatedFromUser.homePageUrl.toDisplayString()) - .arg(currentPost()->repeatedFromUser.userName)); + .arg(microblog->userNameFromAcct(currentPost()->repeatedFromUser.userName))); ss.append(retweet); } } else { qCDebug(CHOQOK) << "post is not a MastodonPost!"; } return ss; } QString MastodonPostWidget::getUsernameHyperlink(const Choqok::User &user) const { return QStringLiteral("%3") .arg(user.homePageUrl.toDisplayString()) .arg(user.description.isEmpty() ? user.realName : user.description.toHtmlEscaped()) - .arg(user.userName); + .arg(MastodonMicroBlog::userNameFromAcct(user.userName)); } void MastodonPostWidget::initUi() { Choqok::UI::PostWidget::initUi(); if (isResendAvailable()) { buttons().value(QLatin1String("btnResend"))->setToolTip(i18nc("@info:tooltip", "Boost")); } + QPushButton *btnRe = addButton(QLatin1String("btnReply"), i18nc("@info:tooltip", "Reply"), QLatin1String("edit-undo")); + connect(btnRe, &QPushButton::clicked, this, &MastodonPostWidget::slotReply); + QMenu *menu = new QMenu(btnRe); + btnRe->setMenu(menu); + + QAction *actRep = new QAction(QIcon::fromTheme(QLatin1String("edit-undo")), i18n("Reply to %1", currentPost()->author.userName), menu); + menu->addAction(actRep); + menu->setDefaultAction(actRep); + connect(actRep, &QAction::triggered, this, &MastodonPostWidget::slotReply); + + QAction *actWrite = new QAction(QIcon::fromTheme(QLatin1String("document-edit")), i18n("Write to %1", currentPost()->author.userName), menu); + menu->addAction(actWrite); + connect(actWrite, &QAction::triggered, this, &MastodonPostWidget::slotWriteTo); + + if (!currentPost()->isPrivate) { + QAction *actReplytoAll = new QAction(i18n("Reply to all"), menu); + menu->addAction(actReplytoAll); + connect(actReplytoAll, &QAction::triggered, this, &MastodonPostWidget::slotReplyToAll); + } + d->btnFavorite = addButton(QLatin1String("btnFavorite"), i18nc("@info:tooltip", "Favourite"), QLatin1String("rating")); d->btnFavorite->setCheckable(true); connect(d->btnFavorite, &QPushButton::clicked, this, &MastodonPostWidget::toggleFavorite); updateFavStat(); } +void MastodonPostWidget::slotReply() +{ + setReadWithSignal(); + if (currentPost()->isPrivate) { + MastodonAccount *account = qobject_cast(currentAccount()); + MastodonMicroBlog *microblog = qobject_cast(account->microblog()); + microblog->showDirectMessageDialog(account, currentPost()->author.userName); + } else { + QString replyto = QStringLiteral("@%1").arg(currentPost()->author.userName); + QString postId = currentPost()->postId; + QString username = currentPost()->author.userName; + if (!currentPost()->repeatedFromUser.userName.isEmpty()) { + replyto.prepend(QStringLiteral("@%1 ").arg(currentPost()->repeatedFromUser.userName)); + postId = currentPost()->repeatedPostId; + } + Q_EMIT reply(replyto, postId, username); + } +} + +void MastodonPostWidget::slotWriteTo() +{ + Q_EMIT reply(QStringLiteral("@%1").arg(currentPost()->author.userName), QString(), currentPost()->author.userName); +} + +void MastodonPostWidget::slotReplyToAll() +{ + QString txt = QStringLiteral("@%1").arg(currentPost()->author.userName); + Q_EMIT reply(txt, currentPost()->postId, currentPost()->author.userName); +} + void MastodonPostWidget::slotResendPost() { qCDebug(CHOQOK); setReadWithSignal(); MastodonMicroBlog *microBlog = qobject_cast(currentAccount()->microblog()); microBlog->toggleReblog(currentAccount(), currentPost()); } void MastodonPostWidget::toggleFavorite() { qCDebug(CHOQOK); setReadWithSignal(); MastodonMicroBlog *microBlog = qobject_cast(currentAccount()->microblog()); connect(microBlog, &MastodonMicroBlog::favorite, this, &MastodonPostWidget::slotToggleFavorite); microBlog->toggleFavorite(currentAccount(), currentPost()); } void MastodonPostWidget::slotToggleFavorite(Choqok::Account *, Choqok::Post *) { qCDebug(CHOQOK); updateFavStat(); } void MastodonPostWidget::updateFavStat() { d->btnFavorite->setChecked(currentPost()->isFavorited); if (currentPost()->isFavorited) { d->btnFavorite->setIcon(QIcon::fromTheme(QLatin1String("rating"))); } else { d->btnFavorite->setIcon(unFavIcon); } } diff --git a/microblogs/mastodon/mastodonpostwidget.h b/microblogs/mastodon/mastodonpostwidget.h index 569d3244..3457ec58 100644 --- a/microblogs/mastodon/mastodonpostwidget.h +++ b/microblogs/mastodon/mastodonpostwidget.h @@ -1,58 +1,60 @@ /* This file is part of Choqok, the KDE micro-blogging client Copyright (C) 2017 Andrea Scarpino This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see http://www.gnu.org/licenses/ */ #ifndef MASTODONPOSTWIDGET_H #define MASTODONPOSTWIDGET_H #include "postwidget.h" class MastodonPostWidget : public Choqok::UI::PostWidget { Q_OBJECT public: explicit MastodonPostWidget(Choqok::Account *account, Choqok::Post *post, QWidget *parent = 0); virtual ~MastodonPostWidget(); virtual QString generateSign() override; virtual void initUi() override; protected Q_SLOTS: virtual void slotResendPost() override; void slotToggleFavorite(Choqok::Account *, Choqok::Post *); - + void slotReply(); + void slotWriteTo(); + void slotReplyToAll(); void toggleFavorite(); protected: virtual QString getUsernameHyperlink(const Choqok::User &user) const; static const QIcon unFavIcon; private: void updateFavStat(); class Private; Private *const d; }; #endif // MASTODONPOSTWIDGET_H