From 5884bcb142fd11989bcf643c023276a520d3dffd Mon Sep 17 00:00:00 2001 From: Takayuki Orito Date: Tue, 29 Aug 2023 00:46:48 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF?= =?UTF-8?q?=E3=82=BF=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/qml/main.qml | 19 +++++------ app/qtquick/accountlistmodel.cpp | 55 ++++++++++++++++++++------------ app/qtquick/accountlistmodel.h | 7 ++-- scripts/json_merge.py | 33 ------------------- 4 files changed, 45 insertions(+), 69 deletions(-) delete mode 100644 scripts/json_merge.py diff --git a/app/qml/main.qml b/app/qml/main.qml index 381a0664..881b086d 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -182,22 +182,19 @@ ApplicationWindow { // アカウント管理で内容が変更されたときにカラムとインデックスの関係が崩れるのでuuidで確認する AccountListModel { id: accountListModel - onAppendedAccount: (row) => { - console.log("onAppendedAccount:" + row) - } + onUpdatedSession: (row, uuid) => { + console.log("onUpdatedSession:" + row + ", " + uuid) + if(columnManageModel.rowCount() === 0 && allAccountsReady()){ + // すべてのアカウント情報の認証が終わったのでカラムを復元 + console.log("start loading columns") + columnManageModel.load() + } + } onUpdatedAccount: (row, uuid) => { console.log("onUpdatedAccount:" + row + ", " + uuid) // カラムを更新しにいく repeater.updateAccount(uuid) } - - onAllFinished: { - // すべてのアカウント情報の認証が終わったのでカラムを復元(成功しているとは限らない) - console.log("allFinished()" + accountListModel.count) - if(columnManageModel.rowCount() === 0){ - columnManageModel.load() - } - } onErrorOccured: (message) => {console.log(message)} function syncColumn(){ diff --git a/app/qtquick/accountlistmodel.cpp b/app/qtquick/accountlistmodel.cpp index 1377d786..95b4a795 100644 --- a/app/qtquick/accountlistmodel.cpp +++ b/app/qtquick/accountlistmodel.cpp @@ -233,6 +233,18 @@ void AccountListModel::setMainAccount(int row) save(); } +bool AccountListModel::allAccountsReady() const +{ + bool ready = true; + for (const AccountData &item : qAsConst(m_accountList)) { + if (item.status == AccountStatus::Unknown) { + ready = false; + break; + } + } + return ready; +} + void AccountListModel::save() const { QSettings settings; @@ -291,8 +303,7 @@ void AccountListModel::load() endInsertRows(); emit countChanged(); - updateSession(m_accountList.count() - 1, item.service, item.identifier, - item.password); + createSession(m_accountList.count() - 1); if (item.is_main) { has_main = true; @@ -336,38 +347,37 @@ QHash AccountListModel::roleNames() const return roles; } -void AccountListModel::updateSession(int row, const QString &service, const QString &identifier, - const QString &password) +void AccountListModel::createSession(int row) { + if (row < 0 || row >= m_accountList.count()) + return; + ComAtprotoServerCreateSession *session = new ComAtprotoServerCreateSession(this); - session->setService(service); connect(session, &ComAtprotoServerCreateSession::finished, [=](bool success) { // qDebug() << session << session->service() << session->did() << session->handle() // << session->email() << session->accessJwt() << session->refreshJwt(); // qDebug() << service << identifier << password; - updateAccount(service, identifier, password, session->did(), session->handle(), - session->email(), session->accessJwt(), session->refreshJwt(), success); if (success) { - emit appendedAccount(row); + qDebug() << "Create session" << session->did() << session->handle(); + m_accountList[row].did = session->did(); + m_accountList[row].handle = session->handle(); + m_accountList[row].accessJwt = session->accessJwt(); + m_accountList[row].refreshJwt = session->refreshJwt(); + m_accountList[row].status = AccountStatus::Authorized; + + emit updatedSession(row, m_accountList[row].uuid); // 詳細を取得 getProfile(row); } else { + m_accountList[row].status = AccountStatus::Unauthorized; emit errorOccured(session->errorMessage()); } - bool all_finished = true; - for (const AccountData &item : qAsConst(m_accountList)) { - if (item.status == AccountStatus::Unknown) { - all_finished = false; - break; - } - } - if (all_finished) { - emit allFinished(); - } + emit dataChanged(index(row), index(row)); session->deleteLater(); }); - session->create(identifier, password); + session->setAccount(m_accountList.at(row)); + session->create(m_accountList.at(row).identifier, m_accountList.at(row).password); } void AccountListModel::refreshSession(int row) @@ -376,7 +386,6 @@ void AccountListModel::refreshSession(int row) return; ComAtprotoServerRefreshSession *session = new ComAtprotoServerRefreshSession(this); - session->setAccount(m_accountList.at(row)); connect(session, &ComAtprotoServerRefreshSession::finished, [=](bool success) { if (success) { qDebug() << "Refresh session" << session->did() << session->handle(); @@ -386,7 +395,10 @@ void AccountListModel::refreshSession(int row) m_accountList[row].refreshJwt = session->refreshJwt(); m_accountList[row].status = AccountStatus::Authorized; - emit updatedAccount(row, m_accountList[row].uuid); + emit updatedSession(row, m_accountList[row].uuid); + + // 詳細を取得 + getProfile(row); } else { m_accountList[row].status = AccountStatus::Unauthorized; emit errorOccured(session->errorMessage()); @@ -394,6 +406,7 @@ void AccountListModel::refreshSession(int row) emit dataChanged(index(row), index(row)); session->deleteLater(); }); + session->setAccount(m_accountList.at(row)); session->refreshSession(); } diff --git a/app/qtquick/accountlistmodel.h b/app/qtquick/accountlistmodel.h index 705b1453..f7e37082 100644 --- a/app/qtquick/accountlistmodel.h +++ b/app/qtquick/accountlistmodel.h @@ -54,6 +54,7 @@ class AccountListModel : public QAbstractListModel Q_INVOKABLE int indexAt(const QString &uuid); Q_INVOKABLE int getMainAccountIndex() const; Q_INVOKABLE void setMainAccount(int row); + Q_INVOKABLE bool allAccountsReady() const; Q_INVOKABLE void save() const; Q_INVOKABLE void load(); @@ -64,9 +65,8 @@ class AccountListModel : public QAbstractListModel signals: void errorOccured(const QString &message); - void appendedAccount(int row); + void updatedSession(int row, const QString &uuid); void updatedAccount(int row, const QString &uuid); - void allFinished(); void countChanged(); protected: @@ -80,8 +80,7 @@ class AccountListModel : public QAbstractListModel QString appDataFolder() const; - void updateSession(int row, const QString &service, const QString &identifier, - const QString &password); + void createSession(int row); void refreshSession(int row); void getProfile(int row); }; diff --git a/scripts/json_merge.py b/scripts/json_merge.py deleted file mode 100644 index 9e794ab5..00000000 --- a/scripts/json_merge.py +++ /dev/null @@ -1,33 +0,0 @@ -import json - -def deep_merge(dict1, dict2): - """再帰的に辞書をマージする""" - for key in dict2: - if key in dict1: - # 両方の辞書にキーが存在し、それらの値が辞書である場合、再帰的にマージ - if isinstance(dict1[key], dict) and isinstance(dict2[key], dict): - deep_merge(dict1[key], dict2[key]) - # dict1の値を優先して保持(何もしない) - else: - # dict1にないキーを追加 - dict1[key] = dict2[key] - return dict1 - -def merge_json_files(file1, file2): - with open(file1, 'r') as f1: - data1 = json.load(f1) - - with open(file2, 'r') as f2: - data2 = json.load(f2) - - return deep_merge(data1, data2) - -# マージしたい2つのJSONファイルのパスを指定 -file1_path = './scripts/lexicons/app.bsky.feed.post_org.json' -file2_path = './scripts/lexicons/app.bsky.feed.post.json' - -# マージ処理を実行 -merged_data = merge_json_files(file1_path, file2_path) - -# 結果を表示 -print(json.dumps(merged_data)) From 92ab716e2a0750342321fd050963fac4af395f75 Mon Sep 17 00:00:00 2001 From: Takayuki Orito Date: Fri, 1 Sep 2023 22:56:42 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=E7=B6=9A=E3=81=8D=E3=81=AE=E8=AA=AD?= =?UTF-8?q?=E3=81=BF=E8=BE=BC=E3=81=BF=E5=AF=BE=E5=BF=9C=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 続きの読み込み * カスタムフィードの検索で続き読み込み対応 * フォローの続きの読み込み対応 * フォロワーの続きの読み込み対応 * 通知の続きの読み込み対応 * リポスト・いいね・ユーザーポストの続きの読み込み対応 * リファクタ --- app/qml/dialogs/DiscoverFeedsDialog.qml | 5 + app/qml/view/NotificationListView.qml | 6 + app/qml/view/ProfileListView.qml | 6 + app/qml/view/TimelineView.qml | 6 + app/qtquick/anyfeedlistmodel.cpp | 71 +- app/qtquick/anyfeedlistmodel.h | 1 + app/qtquick/atpabstractlistmodel.cpp | 48 ++ app/qtquick/atpabstractlistmodel.h | 3 + app/qtquick/authorfeedlistmodel.cpp | 35 + app/qtquick/authorfeedlistmodel.h | 1 + app/qtquick/feedgeneratorlistmodel.cpp | 34 +- app/qtquick/feedgeneratorlistmodel.h | 1 + app/qtquick/feedtypelistmodel.cpp | 5 + app/qtquick/feedtypelistmodel.h | 1 + app/qtquick/followerslistmodel.cpp | 38 + app/qtquick/followerslistmodel.h | 1 + app/qtquick/followslistmodel.cpp | 38 + app/qtquick/followslistmodel.h | 1 + app/qtquick/notificationlistmodel.cpp | 94 ++- app/qtquick/notificationlistmodel.h | 1 + app/qtquick/timelinelistmodel.cpp | 44 + app/qtquick/timelinelistmodel.h | 2 + lib/atprotocol/accessatprotocol.cpp | 10 + lib/atprotocol/accessatprotocol.h | 3 + .../bsky/feed/appbskyfeedgetauthorfeed.cpp | 3 + .../app/bsky/feed/appbskyfeedgettimeline.cpp | 7 +- .../app/bsky/feed/appbskyfeedgettimeline.h | 2 +- .../bsky/graph/appbskygraphgetfollowers.cpp | 3 + .../app/bsky/graph/appbskygraphgetfollows.cpp | 4 + .../appbskynotificationlistnotifications.cpp | 7 +- .../appbskynotificationlistnotifications.h | 2 +- ...pbskyunspeccedgetpopularfeedgenerators.cpp | 4 + .../repo/comatprotorepolistrecords.cpp | 12 +- .../atproto/repo/comatprotorepolistrecords.h | 4 +- tests/hagoromo_test/hagoromo_test.qrc | 4 + .../1st/xrpc/app.bsky.actor.getPreferences | 56 ++ .../next/1st/xrpc/app.bsky.feed.getTimeline | 382 +++++++++ .../2nd/xrpc/app.bsky.actor.getPreferences | 56 ++ .../next/2nd/xrpc/app.bsky.feed.getTimeline | 767 ++++++++++++++++++ tests/hagoromo_test/tst_hagoromo_test.cpp | 72 ++ 40 files changed, 1824 insertions(+), 16 deletions(-) create mode 100644 tests/hagoromo_test/response/timeline/next/1st/xrpc/app.bsky.actor.getPreferences create mode 100644 tests/hagoromo_test/response/timeline/next/1st/xrpc/app.bsky.feed.getTimeline create mode 100644 tests/hagoromo_test/response/timeline/next/2nd/xrpc/app.bsky.actor.getPreferences create mode 100644 tests/hagoromo_test/response/timeline/next/2nd/xrpc/app.bsky.feed.getTimeline diff --git a/app/qml/dialogs/DiscoverFeedsDialog.qml b/app/qml/dialogs/DiscoverFeedsDialog.qml index 34fe86bf..e732d734 100644 --- a/app/qml/dialogs/DiscoverFeedsDialog.qml +++ b/app/qml/dialogs/DiscoverFeedsDialog.qml @@ -100,6 +100,11 @@ Dialog { ListView { id: generatorListView clip: true + onMovementEnded: { + if(atYEnd){ + feedGeneratorListModel.getNext() + } + } model: FeedGeneratorListModel { id: feedGeneratorListModel query: searchText.text diff --git a/app/qml/view/NotificationListView.qml b/app/qml/view/NotificationListView.qml index fa33c86b..e8cd9404 100644 --- a/app/qml/view/NotificationListView.qml +++ b/app/qml/view/NotificationListView.qml @@ -40,6 +40,12 @@ ScrollView { id: systemTool } + onMovementEnded: { + if(atYEnd){ + rootListView.model.getNext() + } + } + header: ItemDelegate { width: rootListView.width height: AdjustedValues.h24 diff --git a/app/qml/view/ProfileListView.qml b/app/qml/view/ProfileListView.qml index 9c624a4f..b9a6ed22 100644 --- a/app/qml/view/ProfileListView.qml +++ b/app/qml/view/ProfileListView.qml @@ -49,6 +49,12 @@ ScrollView { } } + onMovementEnded: { + if(atYEnd){ + profileListView.model.getNext() + } + } + header: ItemDelegate { width: rootListView.width height: AdjustedValues.h24 diff --git a/app/qml/view/TimelineView.qml b/app/qml/view/TimelineView.qml index b08621e0..95dfe0f1 100644 --- a/app/qml/view/TimelineView.qml +++ b/app/qml/view/TimelineView.qml @@ -39,6 +39,12 @@ ScrollView { anchors.rightMargin: parent.ScrollBar.vertical.width spacing: 5 + onMovementEnded: { + if(atYEnd){ + rootListView.model.getNext() + } + } + SystemTool { id: systemTool } diff --git a/app/qtquick/anyfeedlistmodel.cpp b/app/qtquick/anyfeedlistmodel.cpp index d90445a1..9c37b038 100644 --- a/app/qtquick/anyfeedlistmodel.cpp +++ b/app/qtquick/anyfeedlistmodel.cpp @@ -28,6 +28,9 @@ void AnyFeedListModel::getLatest() if (success) { QDateTime reference_time = QDateTime::currentDateTimeUtc(); + if (m_cidList.isEmpty() && m_cursor.isEmpty()) { + m_cursor = records->cursor(); + } for (const auto &record : *records->recordList()) { m_recordHash[record.cid] = record; @@ -65,10 +68,74 @@ void AnyFeedListModel::getLatest() records->setAccount(account()); switch (feedType()) { case AnyFeedListModelFeedType::LikeFeedType: - records->listLikes(targetDid()); + records->listLikes(targetDid(), QString()); + break; + case AnyFeedListModelFeedType::RepostFeedType: + records->listReposts(targetDid(), QString()); + break; + default: + setRunning(false); + delete records; + break; + } + }); +} + +void AnyFeedListModel::getNext() +{ + if (running() || m_cursor.isEmpty()) + return; + setRunning(true); + + updateContentFilterLabels([=]() { + ComAtprotoRepoListRecords *records = new ComAtprotoRepoListRecords(this); + connect(records, &ComAtprotoRepoListRecords::finished, [=](bool success) { + if (success) { + QDateTime reference_time = QDateTime::currentDateTimeUtc(); + + m_cursor = records->cursor(); + + for (const auto &record : *records->recordList()) { + m_recordHash[record.cid] = record; + + QString cid; + QString indexed_at; + switch (feedType()) { + case AnyFeedListModelFeedType::LikeFeedType: + cid = appendGetPostCue(record.value); + indexed_at = + getIndexedAt(record.value); + break; + case AnyFeedListModelFeedType::RepostFeedType: + cid = appendGetPostCue( + record.value); + indexed_at = + getIndexedAt(record.value); + break; + default: + break; + } + if (!cid.isEmpty() && !m_cidList.contains(cid)) { + PostCueItem post; + post.cid = cid; + post.indexed_at = indexed_at; + post.reference_time = reference_time; + m_cuePost.insert(0, post); + } + } + } else { + emit errorOccured(records->errorMessage()); + } + QTimer::singleShot(100, this, &AnyFeedListModel::displayQueuedPostsNext); + records->deleteLater(); + }); + records->setAccount(account()); + switch (feedType()) { + case AnyFeedListModelFeedType::LikeFeedType: + records->listLikes(targetDid(), m_cursor); break; case AnyFeedListModelFeedType::RepostFeedType: - records->listReposts(targetDid()); + records->listReposts(targetDid(), m_cursor); break; default: setRunning(false); diff --git a/app/qtquick/anyfeedlistmodel.h b/app/qtquick/anyfeedlistmodel.h index 06ee4a2b..ad2dced4 100644 --- a/app/qtquick/anyfeedlistmodel.h +++ b/app/qtquick/anyfeedlistmodel.h @@ -18,6 +18,7 @@ class AnyFeedListModel : public TimelineListModel Q_ENUM(AnyFeedListModelFeedType) Q_INVOKABLE void getLatest(); + Q_INVOKABLE void getNext(); QString targetDid() const; void setTargetDid(const QString &newTargetDid); diff --git a/app/qtquick/atpabstractlistmodel.cpp b/app/qtquick/atpabstractlistmodel.cpp index 50dfaf60..3b4140d3 100644 --- a/app/qtquick/atpabstractlistmodel.cpp +++ b/app/qtquick/atpabstractlistmodel.cpp @@ -257,6 +257,54 @@ void AtpAbstractListModel::displayQueuedPosts() } } +void AtpAbstractListModel::displayQueuedPostsNext() +{ + while (!m_cuePost.isEmpty()) { + const PostCueItem &post = m_cuePost.back(); + bool visible = checkVisibility(post.cid); + + if (m_originalCidList.contains(post.cid)) { + if (post.reason_type == AppBskyFeedDefs::FeedViewPostReasonType::reason_ReasonRepost) { + // 通常、repostのときはいったん消して上へ移動だけど + // 続きの読み込みの時は下へ入れることになるので無視 + } else { + // リストは更新しないでデータのみ入れ替える + // 更新をUIに通知 + // (取得できた範囲でしか更新できないのだけど・・・) + int r = m_cidList.indexOf(post.cid); + if (r >= 0) { + if (visible) { + emit dataChanged(index(r), index(r)); + } else { + beginRemoveRows(QModelIndex(), r, r); + m_cidList.removeAt(r); + endRemoveRows(); + } + } else { + int r = searchInsertPosition(post.cid); + if (visible && r >= 0) { + // 復活させる + beginInsertRows(QModelIndex(), r, r); + m_cidList.insert(r, post.cid); + endInsertRows(); + } + } + } + } else { + if (visible) { + beginInsertRows(QModelIndex(), m_cidList.count(), m_cidList.count()); + m_cidList.append(post.cid); + endInsertRows(); + } + m_originalCidList.append(post.cid); + } + + m_cuePost.pop_back(); + } + + finishedDisplayingQueuedPosts(); +} + void AtpAbstractListModel::updateContentFilterLabels(std::function callback) { ConfigurableLabels *labels = new ConfigurableLabels(this); diff --git a/app/qtquick/atpabstractlistmodel.h b/app/qtquick/atpabstractlistmodel.h index 0ef98712..979eea2b 100644 --- a/app/qtquick/atpabstractlistmodel.h +++ b/app/qtquick/atpabstractlistmodel.h @@ -75,11 +75,13 @@ class AtpAbstractListModel : public QAbstractListModel public slots: virtual Q_INVOKABLE void getLatest() = 0; + virtual Q_INVOKABLE void getNext() = 0; protected: QString formatDateTime(const QString &value, const bool is_long = false) const; QString copyRecordText(const QVariant &value) const; void displayQueuedPosts(); + void displayQueuedPostsNext(); virtual void finishedDisplayingQueuedPosts() = 0; virtual bool checkVisibility(const QString &cid) = 0; void updateContentFilterLabels(std::function callback); @@ -102,6 +104,7 @@ public slots: // displayQueuedPosts()を使ってcidのリストを構成しないと使わない QList m_originalCidList; QList m_cuePost; + QString m_cursor; QHash m_translations; // QHash diff --git a/app/qtquick/authorfeedlistmodel.cpp b/app/qtquick/authorfeedlistmodel.cpp index 717bbd2d..bb8e949e 100644 --- a/app/qtquick/authorfeedlistmodel.cpp +++ b/app/qtquick/authorfeedlistmodel.cpp @@ -19,6 +19,9 @@ void AuthorFeedListModel::getLatest() AppBskyFeedGetAuthorFeed *timeline = new AppBskyFeedGetAuthorFeed(this); connect(timeline, &AppBskyFeedGetAuthorFeed::finished, [=](bool success) { if (success) { + if (m_cidList.isEmpty() && m_cursor.isEmpty()) { + m_cursor = timeline->cursor(); + } copyFrom(timeline); } else { emit errorOccured(timeline->errorMessage()); @@ -40,6 +43,38 @@ void AuthorFeedListModel::getLatest() }); } +void AuthorFeedListModel::getNext() +{ + if (running() || m_cursor.isEmpty()) + return; + setRunning(true); + + updateContentFilterLabels([=]() { + AppBskyFeedGetAuthorFeed *timeline = new AppBskyFeedGetAuthorFeed(this); + connect(timeline, &AppBskyFeedGetAuthorFeed::finished, [=](bool success) { + if (success) { + m_cursor = timeline->cursor(); + copyFromNext(timeline); + } else { + emit errorOccured(timeline->errorMessage()); + } + QTimer::singleShot(10, this, &AuthorFeedListModel::displayQueuedPostsNext); + timeline->deleteLater(); + }); + + AppBskyFeedGetAuthorFeed::FilterType filter_type; + if (filter() == AuthorFeedListModelFilterType::PostsNoReplies) { + filter_type = AppBskyFeedGetAuthorFeed::FilterType::PostsNoReplies; + } else if (filter() == AuthorFeedListModelFilterType::PostsWithMedia) { + filter_type = AppBskyFeedGetAuthorFeed::FilterType::PostsWithMedia; + } else { + filter_type = AppBskyFeedGetAuthorFeed::FilterType::PostsWithReplies; + } + timeline->setAccount(account()); + timeline->getAuthorFeed(authorDid(), -1, m_cursor, filter_type); + }); +} + QString AuthorFeedListModel::authorDid() const { return m_authorDid; diff --git a/app/qtquick/authorfeedlistmodel.h b/app/qtquick/authorfeedlistmodel.h index 3b510f77..1b30a17a 100644 --- a/app/qtquick/authorfeedlistmodel.h +++ b/app/qtquick/authorfeedlistmodel.h @@ -22,6 +22,7 @@ class AuthorFeedListModel : public TimelineListModel Q_ENUM(AuthorFeedListModelFilterType) Q_INVOKABLE void getLatest(); + Q_INVOKABLE void getNext(); QString authorDid() const; void setAuthorDid(const QString &newAuthorDid); diff --git a/app/qtquick/feedgeneratorlistmodel.cpp b/app/qtquick/feedgeneratorlistmodel.cpp index 4f34b45f..f02393c8 100644 --- a/app/qtquick/feedgeneratorlistmodel.cpp +++ b/app/qtquick/feedgeneratorlistmodel.cpp @@ -93,7 +93,7 @@ void FeedGeneratorListModel::getLatest() AppBskyUnspeccedGetPopularFeedGenerators *generators = new AppBskyUnspeccedGetPopularFeedGenerators(this); connect(generators, &AppBskyUnspeccedGetPopularFeedGenerators::finished, [=](bool success) { - if (success) { + if (success && !generators->generatorViewList()->isEmpty()) { beginInsertRows(QModelIndex(), 0, generators->generatorViewList()->count() - 1); for (const auto &generator : *generators->generatorViewList()) { m_cidList.append(generator.cid); @@ -101,6 +101,7 @@ void FeedGeneratorListModel::getLatest() } endInsertRows(); + m_cursor = generators->cursor(); getSavedGenerators(); } else { emit errorOccured(generators->errorMessage()); @@ -112,6 +113,37 @@ void FeedGeneratorListModel::getLatest() generators->getPopularFeedGenerators(50, QString(), query()); } +void FeedGeneratorListModel::getNext() +{ + if (running() || m_cursor.isEmpty()) + return; + setRunning(true); + + AppBskyUnspeccedGetPopularFeedGenerators *generators = + new AppBskyUnspeccedGetPopularFeedGenerators(this); + connect(generators, &AppBskyUnspeccedGetPopularFeedGenerators::finished, [=](bool success) { + if (success && !generators->generatorViewList()->isEmpty()) { + beginInsertRows(QModelIndex(), m_cidList.count(), + m_cidList.count() + generators->generatorViewList()->count() - 1); + for (const auto &generator : *generators->generatorViewList()) { + m_cidList.append(generator.cid); + m_generatorViewHash[generator.cid] = generator; + } + endInsertRows(); + + m_cursor = generators->cursor(); + // getSavedGenerators(); + } else { + m_cursor.clear(); + emit errorOccured(generators->errorMessage()); + } + setRunning(false); + generators->deleteLater(); + }); + generators->setAccount(account()); + generators->getPopularFeedGenerators(50, m_cursor, query()); +} + void FeedGeneratorListModel::saveGenerator(const QString &uri) { if (running()) diff --git a/app/qtquick/feedgeneratorlistmodel.h b/app/qtquick/feedgeneratorlistmodel.h index 8fbb6f24..e128dae5 100644 --- a/app/qtquick/feedgeneratorlistmodel.h +++ b/app/qtquick/feedgeneratorlistmodel.h @@ -37,6 +37,7 @@ class FeedGeneratorListModel : public AtpAbstractListModel Q_INVOKABLE void clear(); Q_INVOKABLE void getLatest(); + Q_INVOKABLE void getNext(); Q_INVOKABLE void saveGenerator(const QString &uri); Q_INVOKABLE void removeGenerator(const QString &uri); diff --git a/app/qtquick/feedtypelistmodel.cpp b/app/qtquick/feedtypelistmodel.cpp index 1451005d..b7a3bb1a 100644 --- a/app/qtquick/feedtypelistmodel.cpp +++ b/app/qtquick/feedtypelistmodel.cpp @@ -102,6 +102,11 @@ void FeedTypeListModel::getLatest() pref->getPreferences(); } +void FeedTypeListModel::getNext() +{ + // +} + QHash FeedTypeListModel::roleNames() const { QHash roles; diff --git a/app/qtquick/feedtypelistmodel.h b/app/qtquick/feedtypelistmodel.h index 6d59b54a..ff5836ed 100644 --- a/app/qtquick/feedtypelistmodel.h +++ b/app/qtquick/feedtypelistmodel.h @@ -38,6 +38,7 @@ class FeedTypeListModel : public AtpAbstractListModel Q_INVOKABLE void clear(); Q_INVOKABLE void getLatest(); + Q_INVOKABLE void getNext(); protected: QHash roleNames() const; diff --git a/app/qtquick/followerslistmodel.cpp b/app/qtquick/followerslistmodel.cpp index c091fed0..898004f9 100644 --- a/app/qtquick/followerslistmodel.cpp +++ b/app/qtquick/followerslistmodel.cpp @@ -16,6 +16,9 @@ void FollowersListModel::getLatest() AppBskyGraphGetFollowers *followers = new AppBskyGraphGetFollowers(this); connect(followers, &AppBskyGraphGetFollowers::finished, [=](bool success) { if (success) { + if (m_didList.isEmpty()) { + m_cursor = followers->cursor(); + } for (const auto &profile : *followers->profileList()) { m_profileHash[profile.did] = profile; m_formattedDescriptionHash[profile.did] = @@ -39,3 +42,38 @@ void FollowersListModel::getLatest() followers->getFollowers(targetDid(), 50, QString()); }); } + +void FollowersListModel::getNext() +{ + if (running() || m_cursor.isEmpty()) + return; + setRunning(true); + + updateContentFilterLabels([=]() { + AppBskyGraphGetFollowers *followers = new AppBskyGraphGetFollowers(this); + connect(followers, &AppBskyGraphGetFollowers::finished, [=](bool success) { + if (success) { + m_cursor = followers->cursor(); + for (const auto &profile : *followers->profileList()) { + m_profileHash[profile.did] = profile; + m_formattedDescriptionHash[profile.did] = + m_systemTool.markupText(profile.description); + if (m_didList.contains(profile.did)) { + int row = m_didList.indexOf(profile.did); + emit dataChanged(index(row), index(row)); + } else { + beginInsertRows(QModelIndex(), m_didList.count(), m_didList.count()); + m_didList.append(profile.did); + endInsertRows(); + } + } + } else { + emit errorOccured(followers->errorMessage()); + } + setRunning(false); + followers->deleteLater(); + }); + followers->setAccount(account()); + followers->getFollowers(targetDid(), 50, m_cursor); + }); +} diff --git a/app/qtquick/followerslistmodel.h b/app/qtquick/followerslistmodel.h index 34278571..49b7417e 100644 --- a/app/qtquick/followerslistmodel.h +++ b/app/qtquick/followerslistmodel.h @@ -11,6 +11,7 @@ class FollowersListModel : public FollowsListModel public slots: Q_INVOKABLE void getLatest(); + Q_INVOKABLE void getNext(); }; #endif // FOLLOWERSLISTMODEL_H diff --git a/app/qtquick/followslistmodel.cpp b/app/qtquick/followslistmodel.cpp index f8dce061..8ba94243 100644 --- a/app/qtquick/followslistmodel.cpp +++ b/app/qtquick/followslistmodel.cpp @@ -101,6 +101,9 @@ void FollowsListModel::getLatest() AppBskyGraphGetFollows *follows = new AppBskyGraphGetFollows(this); connect(follows, &AppBskyGraphGetFollows::finished, [=](bool success) { if (success) { + if (m_didList.isEmpty()) { + m_cursor = follows->cursor(); + } for (const auto &profile : *follows->profileList()) { m_profileHash[profile.did] = profile; m_formattedDescriptionHash[profile.did] = @@ -125,6 +128,41 @@ void FollowsListModel::getLatest() }); } +void FollowsListModel::getNext() +{ + if (running() || m_cursor.isEmpty()) + return; + setRunning(true); + + updateContentFilterLabels([=]() { + AppBskyGraphGetFollows *follows = new AppBskyGraphGetFollows(this); + connect(follows, &AppBskyGraphGetFollows::finished, [=](bool success) { + if (success) { + m_cursor = follows->cursor(); + for (const auto &profile : *follows->profileList()) { + m_profileHash[profile.did] = profile; + m_formattedDescriptionHash[profile.did] = + m_systemTool.markupText(profile.description); + if (m_didList.contains(profile.did)) { + int row = m_didList.indexOf(profile.did); + emit dataChanged(index(row), index(row)); + } else { + beginInsertRows(QModelIndex(), m_didList.count(), m_didList.count()); + m_didList.append(profile.did); + endInsertRows(); + } + } + } else { + emit errorOccured(follows->errorMessage()); + } + setRunning(false); + follows->deleteLater(); + }); + follows->setAccount(account()); + follows->getFollows(targetDid(), 50, m_cursor); + }); +} + QHash FollowsListModel::roleNames() const { QHash roles; diff --git a/app/qtquick/followslistmodel.h b/app/qtquick/followslistmodel.h index 78f81a17..3d6dc3d7 100644 --- a/app/qtquick/followslistmodel.h +++ b/app/qtquick/followslistmodel.h @@ -45,6 +45,7 @@ class FollowsListModel : public AtpAbstractListModel public slots: Q_INVOKABLE void getLatest(); + Q_INVOKABLE void getNext(); signals: void profileTypeChanged(); diff --git a/app/qtquick/notificationlistmodel.cpp b/app/qtquick/notificationlistmodel.cpp index d5661dab..4dfac897 100644 --- a/app/qtquick/notificationlistmodel.cpp +++ b/app/qtquick/notificationlistmodel.cpp @@ -373,6 +373,9 @@ void NotificationListModel::getLatest() if (success) { QDateTime reference_time = QDateTime::currentDateTimeUtc(); + if (m_cidList.isEmpty() && m_cursor.isEmpty()) { + m_cursor = notification->cursor(); + } for (auto item = notification->notificationList()->crbegin(); item != notification->notificationList()->crend(); item++) { m_notificationHash[item->cid] = *item; @@ -462,7 +465,96 @@ void NotificationListModel::getLatest() notification->deleteLater(); }); notification->setAccount(account()); - notification->listNotifications(); + notification->listNotifications(QString()); + }); +} + +void NotificationListModel::getNext() +{ + if (running() || m_cursor.isEmpty()) + return; + setRunning(true); + + updateContentFilterLabels([=]() { + AppBskyNotificationListNotifications *notification = + new AppBskyNotificationListNotifications(this); + connect(notification, &AppBskyNotificationListNotifications::finished, [=](bool success) { + if (success) { + QDateTime reference_time = QDateTime::currentDateTimeUtc(); + + m_cursor = notification->cursor(); + + for (auto item = notification->notificationList()->crbegin(); + item != notification->notificationList()->crend(); item++) { + m_notificationHash[item->cid] = *item; + + PostCueItem post; + post.cid = item->cid; + post.indexed_at = item->indexedAt; + post.reference_time = reference_time; + m_cuePost.append(post); + + if (item->reason == "like") { + appendGetPostCue(item->record); + } else if (item->reason == "repost") { + appendGetPostCue(item->record); + } else if (item->reason == "quote") { + AtProtocolType::AppBskyFeedPost::Main post = + AtProtocolType::LexiconsTypeUnknown::fromQVariant< + AtProtocolType::AppBskyFeedPost::Main>(item->record); + switch (post.embed_type) { + case AtProtocolType::AppBskyFeedPost::MainEmbedType:: + embed_AppBskyEmbedImages_Main: + break; + case AtProtocolType::AppBskyFeedPost::MainEmbedType:: + embed_AppBskyEmbedExternal_Main: + break; + case AtProtocolType::AppBskyFeedPost::MainEmbedType:: + embed_AppBskyEmbedRecord_Main: + if (!post.embed_AppBskyEmbedRecord_Main.record.cid.isEmpty() + && !m_cueGetPost.contains( + post.embed_AppBskyEmbedRecord_Main.record.uri)) { + m_cueGetPost.append(post.embed_AppBskyEmbedRecord_Main.record.uri); + } + // quoteしてくれたユーザーのPostの情報も取得できるようにするためキューに入れる + if (!m_cueGetPost.contains(item->uri)) { + m_cueGetPost.append(item->uri); + } + break; + case AtProtocolType::AppBskyFeedPost::MainEmbedType:: + embed_AppBskyEmbedRecordWithMedia_Main: + if (!post.embed_AppBskyEmbedRecordWithMedia_Main.record.isNull() + && !post.embed_AppBskyEmbedRecordWithMedia_Main.record->record.uri + .isEmpty() + && !m_cueGetPost.contains( + post.embed_AppBskyEmbedRecordWithMedia_Main.record->record + .uri)) { + m_cueGetPost.append(post.embed_AppBskyEmbedRecordWithMedia_Main + .record->record.uri); + } + // quoteしてくれたユーザーのPostの情報も取得できるようにするためキューに入れる + if (!m_cueGetPost.contains(item->uri)) { + m_cueGetPost.append(item->uri); + } + break; + default: + break; + } + } else if (item->reason == "reply" || item->reason == "mention") { + // quoteしてくれたユーザーのPostの情報も取得できるようにするためキューに入れる + if (!m_cueGetPost.contains(item->uri)) { + m_cueGetPost.append(item->uri); + } + } + } + } else { + emit errorOccured(notification->errorMessage()); + } + QTimer::singleShot(10, this, &NotificationListModel::displayQueuedPostsNext); + notification->deleteLater(); + }); + notification->setAccount(account()); + notification->listNotifications(m_cursor); }); } diff --git a/app/qtquick/notificationlistmodel.h b/app/qtquick/notificationlistmodel.h index 4dfb5955..e32b5028 100644 --- a/app/qtquick/notificationlistmodel.h +++ b/app/qtquick/notificationlistmodel.h @@ -103,6 +103,7 @@ class NotificationListModel : public AtpAbstractListModel virtual Q_INVOKABLE QString getRecordText(const QString &cid); Q_INVOKABLE void getLatest(); + Q_INVOKABLE void getNext(); Q_INVOKABLE void repost(int row); Q_INVOKABLE void like(int row); diff --git a/app/qtquick/timelinelistmodel.cpp b/app/qtquick/timelinelistmodel.cpp index 7ac17ded..6d4b319f 100644 --- a/app/qtquick/timelinelistmodel.cpp +++ b/app/qtquick/timelinelistmodel.cpp @@ -248,6 +248,9 @@ void TimelineListModel::getLatest() AppBskyFeedGetTimeline *timeline = new AppBskyFeedGetTimeline(this); connect(timeline, &AppBskyFeedGetTimeline::finished, [=](bool success) { if (success) { + if (m_cidList.isEmpty() && m_cursor.isEmpty()) { + m_cursor = timeline->cursor(); + } copyFrom(timeline); } else { emit errorOccured(timeline->errorMessage()); @@ -260,6 +263,30 @@ void TimelineListModel::getLatest() }); } +void TimelineListModel::getNext() +{ + if (running() || m_cursor.isEmpty()) + return; + setRunning(true); + + updateContentFilterLabels([=]() { + AppBskyFeedGetTimeline *timeline = new AppBskyFeedGetTimeline(this); + connect(timeline, &AppBskyFeedGetTimeline::finished, [=](bool success) { + if (success) { + m_cursor = timeline->cursor(); // 続きの読み込みの時は必ず上書き + + copyFromNext(timeline); + } else { + emit errorOccured(timeline->errorMessage()); + } + QTimer::singleShot(10, this, &TimelineListModel::displayQueuedPostsNext); + timeline->deleteLater(); + }); + timeline->setAccount(account()); + timeline->getTimeline(m_cursor); + }); +} + void TimelineListModel::deletePost(int row) { if (row < 0 || row >= m_cidList.count()) @@ -477,6 +504,23 @@ void TimelineListModel::copyFrom(AppBskyFeedGetTimeline *timeline) } } +void TimelineListModel::copyFromNext(AtProtocolInterface::AppBskyFeedGetTimeline *timeline) +{ + QDateTime reference_time = QDateTime::currentDateTimeUtc(); + + for (auto item = timeline->feedList()->crbegin(); item != timeline->feedList()->crend(); + item++) { + m_viewPostHash[item->post.cid] = *item; + + PostCueItem post; + post.cid = item->post.cid; + post.indexed_at = getReferenceTime(*item); + post.reference_time = reference_time; + post.reason_type = item->reason_type; + m_cuePost.append(post); + } +} + QString TimelineListModel::getReferenceTime(const AtProtocolType::AppBskyFeedDefs::FeedViewPost &view_post) { diff --git a/app/qtquick/timelinelistmodel.h b/app/qtquick/timelinelistmodel.h index f026bbb5..97c63a9f 100644 --- a/app/qtquick/timelinelistmodel.h +++ b/app/qtquick/timelinelistmodel.h @@ -99,6 +99,7 @@ class TimelineListModel : public AtpAbstractListModel virtual Q_INVOKABLE QString getRecordText(const QString &cid); Q_INVOKABLE void getLatest(); + Q_INVOKABLE void getNext(); Q_INVOKABLE void deletePost(int row); Q_INVOKABLE void repost(int row); Q_INVOKABLE void like(int row); @@ -108,6 +109,7 @@ class TimelineListModel : public AtpAbstractListModel virtual void finishedDisplayingQueuedPosts(); virtual bool checkVisibility(const QString &cid); void copyFrom(AtProtocolInterface::AppBskyFeedGetTimeline *timeline); + void copyFromNext(AtProtocolInterface::AppBskyFeedGetTimeline *timeline); QString getReferenceTime(const AtProtocolType::AppBskyFeedDefs::FeedViewPost &view_post); QVariant getQuoteItem(const AtProtocolType::AppBskyFeedDefs::PostView &post, const TimelineListModel::TimelineListModelRoles role) const; diff --git a/lib/atprotocol/accessatprotocol.cpp b/lib/atprotocol/accessatprotocol.cpp index 124f89c5..43408670 100644 --- a/lib/atprotocol/accessatprotocol.cpp +++ b/lib/atprotocol/accessatprotocol.cpp @@ -175,6 +175,16 @@ void AccessAtProtocol::postWithImage(const QString &endpoint, const QString &pat file->setParent(reply); } +QString AccessAtProtocol::cursor() const +{ + return m_cursor; +} + +void AccessAtProtocol::setCursor(const QString &newCursor) +{ + m_cursor = newCursor; +} + QString AccessAtProtocol::errorMessage() const { return m_errorMessage; diff --git a/lib/atprotocol/accessatprotocol.h b/lib/atprotocol/accessatprotocol.h index 5dc9a730..aaf4caf2 100644 --- a/lib/atprotocol/accessatprotocol.h +++ b/lib/atprotocol/accessatprotocol.h @@ -59,6 +59,8 @@ class AccessAtProtocol : public QObject QString replyJson() const; QString errorMessage() const; + QString cursor() const; + void setCursor(const QString &newCursor); signals: void finished(bool success); @@ -78,6 +80,7 @@ public slots: AccountData m_account; QString m_replyJson; QString m_errorMessage; + QString m_cursor; }; } diff --git a/lib/atprotocol/app/bsky/feed/appbskyfeedgetauthorfeed.cpp b/lib/atprotocol/app/bsky/feed/appbskyfeedgetauthorfeed.cpp index 6ec8e3d1..28ba282d 100644 --- a/lib/atprotocol/app/bsky/feed/appbskyfeedgetauthorfeed.cpp +++ b/lib/atprotocol/app/bsky/feed/appbskyfeedgetauthorfeed.cpp @@ -17,6 +17,9 @@ void AppBskyFeedGetAuthorFeed::getAuthorFeed(const QString &actor, const int lim { QUrlQuery query; query.addQueryItem(QStringLiteral("actor"), actor); + if (!cursor.isEmpty()) { + query.addQueryItem(QStringLiteral("cursor"), cursor); + } if (filter == FilterType::PostsNoReplies) { query.addQueryItem(QStringLiteral("filter"), "posts_no_replies"); } else if (filter == FilterType::PostsWithMedia) { diff --git a/lib/atprotocol/app/bsky/feed/appbskyfeedgettimeline.cpp b/lib/atprotocol/app/bsky/feed/appbskyfeedgettimeline.cpp index 483090f7..759a69e5 100644 --- a/lib/atprotocol/app/bsky/feed/appbskyfeedgettimeline.cpp +++ b/lib/atprotocol/app/bsky/feed/appbskyfeedgettimeline.cpp @@ -12,11 +12,13 @@ namespace AtProtocolInterface { AppBskyFeedGetTimeline::AppBskyFeedGetTimeline(QObject *parent) : AccessAtProtocol { parent } { } -void AppBskyFeedGetTimeline::getTimeline() +void AppBskyFeedGetTimeline::getTimeline(const QString &cursor) { QUrlQuery query; query.addQueryItem(QStringLiteral("actor"), handle()); - // query.addQueryItem(QStringLiteral("actor"), cursor); + if (!cursor.isEmpty()) { + query.addQueryItem(QStringLiteral("cursor"), cursor); + } get(QStringLiteral("xrpc/app.bsky.feed.getTimeline"), query); } @@ -34,6 +36,7 @@ void AppBskyFeedGetTimeline::parseJson(bool success, const QString reply_json) if (json_doc.isEmpty() || !json_doc.object().contains("feed")) { success = false; } else { + setCursor(json_doc.object().value("cursor").toString()); for (const auto &obj : json_doc.object().value("feed").toArray()) { AppBskyFeedDefs::FeedViewPost feed_item; diff --git a/lib/atprotocol/app/bsky/feed/appbskyfeedgettimeline.h b/lib/atprotocol/app/bsky/feed/appbskyfeedgettimeline.h index 0ff8bd5a..7cd61aec 100644 --- a/lib/atprotocol/app/bsky/feed/appbskyfeedgettimeline.h +++ b/lib/atprotocol/app/bsky/feed/appbskyfeedgettimeline.h @@ -11,7 +11,7 @@ class AppBskyFeedGetTimeline : public AccessAtProtocol public: explicit AppBskyFeedGetTimeline(QObject *parent = nullptr); - void getTimeline(); + void getTimeline(const QString &cursor = QString()); const QList *feedList() const; diff --git a/lib/atprotocol/app/bsky/graph/appbskygraphgetfollowers.cpp b/lib/atprotocol/app/bsky/graph/appbskygraphgetfollowers.cpp index 2e9d063c..7a8d9dcc 100644 --- a/lib/atprotocol/app/bsky/graph/appbskygraphgetfollowers.cpp +++ b/lib/atprotocol/app/bsky/graph/appbskygraphgetfollowers.cpp @@ -18,6 +18,9 @@ void AppBskyGraphGetFollowers::getFollowers(const QString &actor, const int limi { QUrlQuery query; query.addQueryItem(QStringLiteral("actor"), actor); + if (!cursor.isEmpty()) { + query.addQueryItem(QStringLiteral("cursor"), cursor); + } get(QStringLiteral("xrpc/app.bsky.graph.getFollowers"), query); } diff --git a/lib/atprotocol/app/bsky/graph/appbskygraphgetfollows.cpp b/lib/atprotocol/app/bsky/graph/appbskygraphgetfollows.cpp index f664d14f..1d9dd7b7 100644 --- a/lib/atprotocol/app/bsky/graph/appbskygraphgetfollows.cpp +++ b/lib/atprotocol/app/bsky/graph/appbskygraphgetfollows.cpp @@ -17,6 +17,9 @@ void AppBskyGraphGetFollows::getFollows(const QString &actor, const int limit, { QUrlQuery query; query.addQueryItem(QStringLiteral("actor"), actor); + if (!cursor.isEmpty()) { + query.addQueryItem(QStringLiteral("cursor"), cursor); + } get(QStringLiteral("xrpc/app.bsky.graph.getFollows"), query); } @@ -33,6 +36,7 @@ void AppBskyGraphGetFollows::parseJson(bool success, const QString reply_json) if (json_doc.isEmpty() || !json_doc.object().contains(m_listKey)) { success = false; } else { + setCursor(json_doc.object().value("cursor").toString()); for (const auto &obj : json_doc.object().value(m_listKey).toArray()) { AtProtocolType::AppBskyActorDefs::ProfileView profile; AtProtocolType::AppBskyActorDefs::copyProfileView(obj.toObject(), profile); diff --git a/lib/atprotocol/app/bsky/notification/appbskynotificationlistnotifications.cpp b/lib/atprotocol/app/bsky/notification/appbskynotificationlistnotifications.cpp index 67f06fef..2fb58de0 100644 --- a/lib/atprotocol/app/bsky/notification/appbskynotificationlistnotifications.cpp +++ b/lib/atprotocol/app/bsky/notification/appbskynotificationlistnotifications.cpp @@ -13,11 +13,13 @@ AppBskyNotificationListNotifications::AppBskyNotificationListNotifications(QObje { } -void AppBskyNotificationListNotifications::listNotifications() +void AppBskyNotificationListNotifications::listNotifications(const QString &cursor) { QUrlQuery query; // query.addQueryItem(QStringLiteral("actor"), handle()); - // query.addQueryItem(QStringLiteral("cursor"), cursor); + if (!cursor.isEmpty()) { + query.addQueryItem(QStringLiteral("cursor"), cursor); + } get(QStringLiteral("xrpc/app.bsky.notification.listNotifications"), query); } @@ -34,6 +36,7 @@ void AppBskyNotificationListNotifications::parseJson(bool success, const QString if (json_doc.isEmpty() || !json_doc.object().contains("notifications")) { success = false; } else { + setCursor(json_doc.object().value("cursor").toString()); for (const auto &obj : json_doc.object().value("notifications").toArray()) { AtProtocolType::AppBskyNotificationListNotifications::Notification notification; diff --git a/lib/atprotocol/app/bsky/notification/appbskynotificationlistnotifications.h b/lib/atprotocol/app/bsky/notification/appbskynotificationlistnotifications.h index 0727f5e1..45bd987c 100644 --- a/lib/atprotocol/app/bsky/notification/appbskynotificationlistnotifications.h +++ b/lib/atprotocol/app/bsky/notification/appbskynotificationlistnotifications.h @@ -11,7 +11,7 @@ class AppBskyNotificationListNotifications : public AccessAtProtocol public: explicit AppBskyNotificationListNotifications(QObject *parent = nullptr); - void listNotifications(); + void listNotifications(const QString &cursor); const QList * notificationList() const; diff --git a/lib/atprotocol/app/bsky/unspecced/appbskyunspeccedgetpopularfeedgenerators.cpp b/lib/atprotocol/app/bsky/unspecced/appbskyunspeccedgetpopularfeedgenerators.cpp index 79503b07..99dab485 100644 --- a/lib/atprotocol/app/bsky/unspecced/appbskyunspeccedgetpopularfeedgenerators.cpp +++ b/lib/atprotocol/app/bsky/unspecced/appbskyunspeccedgetpopularfeedgenerators.cpp @@ -23,6 +23,9 @@ void AppBskyUnspeccedGetPopularFeedGenerators::getPopularFeedGenerators(const in if (!query.isEmpty()) { url_query.addQueryItem(QStringLiteral("query"), query); } + if (!cursor.isEmpty()) { + url_query.addQueryItem(QStringLiteral("cursor"), cursor); + } get(QStringLiteral("xrpc/app.bsky.unspecced.getPopularFeedGenerators"), url_query); } @@ -40,6 +43,7 @@ void AppBskyUnspeccedGetPopularFeedGenerators::parseJson(bool success, const QSt if (json_doc.isEmpty() || !json_doc.object().contains("feeds")) { success = false; } else { + setCursor(json_doc.object().value("cursor").toString()); for (const auto &value : json_doc.object().value("feeds").toArray()) { GeneratorView generator; copyGeneratorView(value.toObject(), generator); diff --git a/lib/atprotocol/com/atproto/repo/comatprotorepolistrecords.cpp b/lib/atprotocol/com/atproto/repo/comatprotorepolistrecords.cpp index 223984d6..2e95cc6f 100644 --- a/lib/atprotocol/com/atproto/repo/comatprotorepolistrecords.cpp +++ b/lib/atprotocol/com/atproto/repo/comatprotorepolistrecords.cpp @@ -21,18 +21,21 @@ void ComAtprotoRepoListRecords::listRecords(const QString &repo, const QString & QUrlQuery query; query.addQueryItem(QStringLiteral("repo"), repo); query.addQueryItem(QStringLiteral("collection"), collection); + if (!cursor.isEmpty()) { + query.addQueryItem(QStringLiteral("cursor"), cursor); + } get(QStringLiteral("xrpc/com.atproto.repo.listRecords"), query); } -void ComAtprotoRepoListRecords::listLikes(const QString &repo) +void ComAtprotoRepoListRecords::listLikes(const QString &repo, const QString &cursor) { - listRecords(repo, "app.bsky.feed.like", 50, QString(), QString(), QString()); + listRecords(repo, "app.bsky.feed.like", 50, cursor, QString(), QString()); } -void ComAtprotoRepoListRecords::listReposts(const QString &repo) +void ComAtprotoRepoListRecords::listReposts(const QString &repo, const QString &cursor) { - listRecords(repo, "app.bsky.feed.repost", 50, QString(), QString(), QString()); + listRecords(repo, "app.bsky.feed.repost", 50, cursor, QString(), QString()); } const QList * @@ -47,6 +50,7 @@ void ComAtprotoRepoListRecords::parseJson(bool success, const QString reply_json if (json_doc.isEmpty()) { success = false; } else { + setCursor(json_doc.object().value("cursor").toString()); for (const auto &obj : json_doc.object().value("records").toArray()) { AtProtocolType::ComAtprotoRepoListRecords::Record record; diff --git a/lib/atprotocol/com/atproto/repo/comatprotorepolistrecords.h b/lib/atprotocol/com/atproto/repo/comatprotorepolistrecords.h index afc0003a..3f76f6ea 100644 --- a/lib/atprotocol/com/atproto/repo/comatprotorepolistrecords.h +++ b/lib/atprotocol/com/atproto/repo/comatprotorepolistrecords.h @@ -13,8 +13,8 @@ class ComAtprotoRepoListRecords : public AccessAtProtocol void listRecords(const QString &repo, const QString &collection, const int limit, const QString &cursor, const QString &rkeyStart, const QString &rkeyEnd); - void listLikes(const QString &repo); - void listReposts(const QString &repo); + void listLikes(const QString &repo, const QString &cursor); + void listReposts(const QString &repo, const QString &cursor); const QList *recordList() const; diff --git a/tests/hagoromo_test/hagoromo_test.qrc b/tests/hagoromo_test/hagoromo_test.qrc index 96cf15fb..e8404289 100644 --- a/tests/hagoromo_test/hagoromo_test.qrc +++ b/tests/hagoromo_test/hagoromo_test.qrc @@ -22,5 +22,9 @@ response/notifications/warn/xrpc/app.bsky.notification.listNotifications response/timeline/labels/xrpc/app.bsky.actor.getPreferences response/timeline/labels/xrpc/app.bsky.feed.getTimeline + response/timeline/next/1st/xrpc/app.bsky.actor.getPreferences + response/timeline/next/1st/xrpc/app.bsky.feed.getTimeline + response/timeline/next/2nd/xrpc/app.bsky.actor.getPreferences + response/timeline/next/2nd/xrpc/app.bsky.feed.getTimeline diff --git a/tests/hagoromo_test/response/timeline/next/1st/xrpc/app.bsky.actor.getPreferences b/tests/hagoromo_test/response/timeline/next/1st/xrpc/app.bsky.actor.getPreferences new file mode 100644 index 00000000..7774dd17 --- /dev/null +++ b/tests/hagoromo_test/response/timeline/next/1st/xrpc/app.bsky.actor.getPreferences @@ -0,0 +1,56 @@ +{ + "preferences": [ + { + "$type": "app.bsky.actor.defs#savedFeedsPref", + "saved": [ + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/bsky-team", + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends", + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic" + ], + "pinned": [ + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends" + ] + }, + { + "$type": "app.bsky.actor.defs#adultContentPref", + "enabled": true + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "nsfw", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "nudity", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "suggestive", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "gore", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "hate", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "spam", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "impersonation", + "visibility": "warn" + } + ] +} \ No newline at end of file diff --git a/tests/hagoromo_test/response/timeline/next/1st/xrpc/app.bsky.feed.getTimeline b/tests/hagoromo_test/response/timeline/next/1st/xrpc/app.bsky.feed.getTimeline new file mode 100644 index 00000000..af65fa9e --- /dev/null +++ b/tests/hagoromo_test/response/timeline/next/1st/xrpc/app.bsky.feed.getTimeline @@ -0,0 +1,382 @@ +{ + "feed": [ + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3jwsftukqpf2z_1", + "cid": "bafyreig7i2uyva4rpgxv3slogiwf5fvlwy2wx4bjvwuoywy6e7ojjvcrky_1", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "record": { + "text": "link\nhttps://leme.style/", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.external", + "external": { + "uri": "https://leme.style/", + "title": "", + "description": "" + } + }, + "facets": [ + { + "index": { + "byteEnd": 24, + "byteStart": 5 + }, + "features": [ + { + "uri": "https://leme.style/", + "$type": "app.bsky.richtext.facet#link" + } + ] + } + ], + "createdAt": "2023-05-28T15:52:04.434Z" + }, + "embed": { + "$type": "app.bsky.embed.external#view", + "external": { + "uri": "https://leme.style/", + "title": "", + "description": "" + } + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-05-28T15:52:05.320Z", + "viewer": {}, + "labels": [] + } + }, + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3jwrxbyzale2a_2", + "cid": "bafyreiajxwbfoa5cbnphxcwvunisgjiqkjjqkqxpnr4mgfu3vqqupr6wca_2", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "record": { + "text": "リンクありの引用", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.record", + "record": { + "cid": "bafyreif2ux6qo4b2ez266iewichrjvqgeeehui5c7lo3gcglrzdi54pjja", + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3jwrx7cdzg22m" + } + }, + "createdAt": "2023-05-28T11:31:32.478Z" + }, + "embed": { + "$type": "app.bsky.embed.record#view", + "record": { + "$type": "app.bsky.embed.record#viewRecord", + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3jwrx7cdzg22m", + "cid": "bafyreif2ux6qo4b2ez266iewichrjvqgeeehui5c7lo3gcglrzdi54pjja", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "value": { + "text": "リンクテスト\nhttps://leme.style/\n2つめ\nhttps://blueskyweb.xyz/\n終わり", + "$type": "app.bsky.feed.post", + "facets": [ + { + "index": { + "byteEnd": 38, + "byteStart": 19 + }, + "features": [ + { + "uri": "https://leme.style/", + "$type": "app.bsky.richtext.facet#link" + } + ] + }, + { + "index": { + "byteEnd": 70, + "byteStart": 47 + }, + "features": [ + { + "uri": "https://blueskyweb.xyz/", + "$type": "app.bsky.richtext.facet#link" + } + ] + } + ], + "createdAt": "2023-05-28T11:30:01.978Z" + }, + "labels": [], + "indexedAt": "2023-05-28T11:30:02.749Z", + "embeds": [] + } + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-05-28T11:31:33.627Z", + "viewer": {}, + "labels": [] + } + }, + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3jwrx7cdzg22m_3", + "cid": "bafyreif2ux6qo4b2ez266iewichrjvqgeeehui5c7lo3gcglrzdi54pjja_3", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "record": { + "text": "リンクテスト\nhttps://leme.style/\n2つめ\nhttps://blueskyweb.xyz/\n終わり", + "$type": "app.bsky.feed.post", + "facets": [ + { + "index": { + "byteEnd": 38, + "byteStart": 19 + }, + "features": [ + { + "uri": "https://leme.style/", + "$type": "app.bsky.richtext.facet#link" + } + ] + }, + { + "index": { + "byteEnd": 70, + "byteStart": 47 + }, + "features": [ + { + "uri": "https://blueskyweb.xyz/", + "$type": "app.bsky.richtext.facet#link" + } + ] + } + ], + "createdAt": "2023-05-28T11:30:01.978Z" + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-05-28T11:30:02.749Z", + "viewer": {}, + "labels": [] + }, + "reason": { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": { + "did": "did:plc:ipj5qejfoqu6eukvt72uhyit", + "handle": "ioriayane.relog.tech", + "displayName": "IoriAYANE", + "avatar": "https://av-cdn.bsky.app/img/avatar/plain/did:plc:ipj5qejfoqu6eukvt72uhyit/bafkreifjldy2fbgjfli7dson343u2bepzwypt7vlffb45ipsll6bjklphy@jpeg", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "indexedAt": "2023-08-29T15:31:43.389Z" + } + }, + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3jwrukmfyu22m_4", + "cid": "bafyreidmpxa2bdcohuxa4p62q3d6oja75mohihxsliuasvmsxds3utokqa_4", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "record": { + "text": "リンクテスト\nhttps://leme.style/\nもうひとつ\nhttps://blueskyweb.xyz/", + "$type": "app.bsky.feed.post", + "facets": [ + { + "index": { + "byteEnd": 38, + "byteStart": 19 + }, + "features": [ + { + "uri": "https://leme.style/", + "$type": "app.bsky.richtext.facet#link" + } + ] + }, + { + "index": { + "byteEnd": 78, + "byteStart": 55 + }, + "features": [ + { + "uri": "https://blueskyweb.xyz/", + "$type": "app.bsky.richtext.facet#link" + } + ] + } + ], + "createdAt": "2023-05-28T10:42:40.419Z" + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-05-28T10:42:41.196Z", + "viewer": {}, + "labels": [] + } + }, + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3jwrx7cdzg22m_5", + "cid": "bafyreif2ux6qo4b2ez266iewichrjvqgeeehui5c7lo3gcglrzdi54pjjb_5", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "record": { + "text": "https://leme.style/\n2つめ\nhttps://blueskyweb.xyz/\n終わり", + "$type": "app.bsky.feed.post", + "facets": [ + { + "index": { + "byteEnd": 19, + "byteStart": 0 + }, + "features": [ + { + "uri": "https://leme.style/", + "$type": "app.bsky.richtext.facet#link" + } + ] + }, + { + "index": { + "byteEnd": 51, + "byteStart": 28 + }, + "features": [ + { + "uri": "https://blueskyweb.xyz/", + "$type": "app.bsky.richtext.facet#link" + } + ] + } + ], + "createdAt": "2023-05-28T11:30:01.978Z" + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-05-27T11:30:02.749Z", + "viewer": {}, + "labels": [] + } + }, + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3jxj56cxfkq2b_6", + "cid": "bafyreihr2hrmavhzdpmnc65udreph5vfmd3xceqtw2jm3b4clbfbacgsqe_6", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.graph.follow/3jwielvxo622v", + "followedBy": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.follow/3jwn752grz32g" + }, + "labels": [] + }, + "record": { + "text": "@ioriayane.relog.tech @ioriayane.bsky.social @ioriayane.relog.tech てすてす", + "$type": "app.bsky.feed.post", + "facets": [ + { + "index": { + "byteEnd": 44, + "byteStart": 22 + }, + "features": [ + { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "$type": "app.bsky.richtext.facet#mention" + } + ] + }, + { + "index": { + "byteEnd": 66, + "byteStart": 45 + }, + "features": [ + { + "did": "did:plc:ipj5qejfoqu6eukvt72uhyit", + "$type": "app.bsky.richtext.facet#mention" + } + ] + }, + { + "index": { + "byteEnd": 21, + "byteStart": 0 + }, + "features": [ + { + "did": "did:plc:ipj5qejfoqu6eukvt72uhyit", + "$type": "app.bsky.richtext.facet#mention" + } + ] + } + ], + "createdAt": "2023-05-26T16:48:05.353Z" + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-05-26T16:48:06.260Z", + "viewer": {}, + "labels": [] + } + } + ], + "cursor": "1685211366672::bafyreieyo2diuzsozpny4mkfn35qz6cqf7lsqsfdjl3x5el73nk4rmpsjy" +} \ No newline at end of file diff --git a/tests/hagoromo_test/response/timeline/next/2nd/xrpc/app.bsky.actor.getPreferences b/tests/hagoromo_test/response/timeline/next/2nd/xrpc/app.bsky.actor.getPreferences new file mode 100644 index 00000000..7774dd17 --- /dev/null +++ b/tests/hagoromo_test/response/timeline/next/2nd/xrpc/app.bsky.actor.getPreferences @@ -0,0 +1,56 @@ +{ + "preferences": [ + { + "$type": "app.bsky.actor.defs#savedFeedsPref", + "saved": [ + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/bsky-team", + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends", + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic" + ], + "pinned": [ + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends" + ] + }, + { + "$type": "app.bsky.actor.defs#adultContentPref", + "enabled": true + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "nsfw", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "nudity", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "suggestive", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "gore", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "hate", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "spam", + "visibility": "warn" + }, + { + "$type": "app.bsky.actor.defs#contentLabelPref", + "label": "impersonation", + "visibility": "warn" + } + ] +} \ No newline at end of file diff --git a/tests/hagoromo_test/response/timeline/next/2nd/xrpc/app.bsky.feed.getTimeline b/tests/hagoromo_test/response/timeline/next/2nd/xrpc/app.bsky.feed.getTimeline new file mode 100644 index 00000000..f1b8a99a --- /dev/null +++ b/tests/hagoromo_test/response/timeline/next/2nd/xrpc/app.bsky.feed.getTimeline @@ -0,0 +1,767 @@ +{ + "cursor": "1691921273842::bafyreicajuw6fektl5my5wuy757qmxmghyvpe3bapuya3l7we5qjinnbba", + "feed": [ + { + "post": { + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4vp3jcppg2g_7", + "cid": "bafyreiejog3yvjc2tdg4muknodbplaib2yqftukwurd4qjcnal3zdxu4ni_7", + "author": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle": "ioriayane2.bsky.social", + "avatar": "https://cdn.bsky.social/imgproxy/QdJdHqlX6_FaYBBrLNg6OTbw-MdCjwxRj73W41RqLOI/rs:fill:1000:1000:1:0/plain/bafkreiayv34bulrnm5gsnx73b46s2plh76k7fvwcewqrdur7eelf7u6c3u@jpeg", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.follow/3k2fx3qmuob2g", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.follow/3k2fwocixdw2b" + }, + "labels": [] + }, + "record": { + "text": "quoted mute user's post", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.record", + "record": { + "cid": "bafyreihyz7ydlnjtn7f3cvobsxf242vchhr3cjx5dwvk4t5r4knm2nxony", + "uri": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.post/3k4vndstk7t2g" + } + }, + "langs": [ + "ja" + ], + "createdAt": "2023-08-14T07:46:29.974Z" + }, + "embed": { + "$type": "app.bsky.embed.record#view", + "record": { + "$type": "app.bsky.embed.record#viewRecord", + "uri": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.post/3k4vndstk7t2g", + "cid": "bafyreihyz7ydlnjtn7f3cvobsxf242vchhr3cjx5dwvk4t5r4knm2nxony", + "author": { + "did": "did:plc:ipj5qejfoqu6eukvt72uhyit", + "handle": "ioriayane.relog.tech", + "displayName": "IoriAYANE", + "avatar": "https://cdn.bsky.social/imgproxy/KgEwaLIGxtw6NXBsgrmHbgegjorIwcpG4xgNLZjkOm8/rs:fill:1000:1000:1:0/plain/bafkreifjldy2fbgjfli7dson343u2bepzwypt7vlffb45ipsll6bjklphy@jpeg", + "viewer": { + "muted": true, + "blockedBy": false, + "following": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.follow/3jxtb56ycoc2w", + "followedBy": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.graph.follow/3jwielvxo622v" + }, + "labels": [] + }, + "value": { + "via": "Hagoromo", + "text": "はらへったー", + "$type": "app.bsky.feed.post", + "langs": [ + "ja" + ], + "createdAt": "2023-08-14T07:15:20.576Z" + }, + "labels": [], + "indexedAt": "2023-08-14T07:15:25.553Z", + "embeds": [] + } + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-08-14T07:46:34.614Z", + "viewer": {}, + "labels": [] + } + }, + { + "post": { + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4w2pmitsz2j_8", + "cid": "bafyreib67ewj54g6maljtbclhno7mrkquf3w7wbex2woedj5m23mjwyite_8", + "author": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle": "ioriayane2.bsky.social", + "avatar": "https://cdn.bsky.social/imgproxy/QdJdHqlX6_FaYBBrLNg6OTbw-MdCjwxRj73W41RqLOI/rs:fill:1000:1000:1:0/plain/bafkreiayv34bulrnm5gsnx73b46s2plh76k7fvwcewqrdur7eelf7u6c3u@jpeg", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.follow/3k2fx3qmuob2g", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.follow/3k2fwocixdw2b" + }, + "labels": [] + }, + "record": { + "text": "quote mute user test with image", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.recordWithMedia", + "media": { + "$type": "app.bsky.embed.images", + "images": [ + { + "alt": "", + "image": { + "$type": "blob", + "ref": { + "$link": "bafkreicntacak4uf4kirtl4rsnxrfzaang3ijueuqku5hbljde467di2sa" + }, + "mimeType": "image/jpeg", + "size": 29143 + } + } + ] + }, + "record": { + "$type": "app.bsky.embed.record", + "record": { + "cid": "bafyreihyz7ydlnjtn7f3cvobsxf242vchhr3cjx5dwvk4t5r4knm2nxony", + "uri": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.post/3k4vndstk7t2g" + } + } + }, + "langs": [ + "ja" + ], + "createdAt": "2023-08-14T11:14:35.490Z" + }, + "embed": { + "$type": "app.bsky.embed.recordWithMedia#view", + "record": { + "record": { + "$type": "app.bsky.embed.record#viewRecord", + "uri": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.post/3k4vndstk7t2g", + "cid": "bafyreihyz7ydlnjtn7f3cvobsxf242vchhr3cjx5dwvk4t5r4knm2nxony", + "author": { + "did": "did:plc:ipj5qejfoqu6eukvt72uhyit", + "handle": "ioriayane.relog.tech", + "displayName": "IoriAYANE", + "avatar": "https://cdn.bsky.social/imgproxy/KgEwaLIGxtw6NXBsgrmHbgegjorIwcpG4xgNLZjkOm8/rs:fill:1000:1000:1:0/plain/bafkreifjldy2fbgjfli7dson343u2bepzwypt7vlffb45ipsll6bjklphy@jpeg", + "viewer": { + "muted": true, + "blockedBy": false, + "following": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.follow/3jxtb56ycoc2w", + "followedBy": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.graph.follow/3jwielvxo622v" + }, + "labels": [] + }, + "value": { + "via": "Hagoromo", + "text": "はらへったー", + "$type": "app.bsky.feed.post", + "langs": [ + "ja" + ], + "createdAt": "2023-08-14T07:15:20.576Z" + }, + "labels": [], + "indexedAt": "2023-08-14T07:15:25.553Z", + "embeds": [] + } + }, + "media": { + "$type": "app.bsky.embed.images#view", + "images": [ + { + "thumb": "https://cdn.bsky.social/imgproxy/wCVae3ye6cBCuNZyNpC-dixwF25wmP0Z9_YJVh1JZoc/rs:fit:1000:1000:1:0/plain/bafkreicntacak4uf4kirtl4rsnxrfzaang3ijueuqku5hbljde467di2sa@jpeg", + "fullsize": "https://cdn.bsky.social/imgproxy/hKxJL_w7SRQaeOIZpWEQnfNpz5lCisdyZ3-bnBwhJb4/rs:fit:2000:2000:1:0/plain/bafkreicntacak4uf4kirtl4rsnxrfzaang3ijueuqku5hbljde467di2sa@jpeg", + "alt": "" + } + ] + } + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-08-14T11:14:40.213Z", + "viewer": {}, + "labels": [] + } + }, + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3jwrx7cdzg22m_3", + "cid": "bafyreif2ux6qo4b2ez266iewichrjvqgeeehui5c7lo3gcglrzdi54pjja_3", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "record": { + "text": "リンクテスト\nhttps://leme.style/\n2つめ\nhttps://blueskyweb.xyz/\n終わり", + "$type": "app.bsky.feed.post", + "facets": [ + { + "index": { + "byteEnd": 38, + "byteStart": 19 + }, + "features": [ + { + "uri": "https://leme.style/", + "$type": "app.bsky.richtext.facet#link" + } + ] + }, + { + "index": { + "byteEnd": 70, + "byteStart": 47 + }, + "features": [ + { + "uri": "https://blueskyweb.xyz/", + "$type": "app.bsky.richtext.facet#link" + } + ] + } + ], + "createdAt": "2023-05-28T11:30:01.978Z" + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-05-28T11:30:02.749Z", + "viewer": {}, + "labels": [] + }, + "reason": { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": { + "did": "did:plc:ipj5qejfoqu6eukvt72uhyit", + "handle": "ioriayane.relog.tech", + "displayName": "IoriAYANE", + "avatar": "https://av-cdn.bsky.app/img/avatar/plain/did:plc:ipj5qejfoqu6eukvt72uhyit/bafkreifjldy2fbgjfli7dson343u2bepzwypt7vlffb45ipsll6bjklphy@jpeg", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "indexedAt": "2023-08-29T15:31:43.389Z" + } + }, + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3k4w3h24hwh2u_9", + "cid": "bafyreigj7v4cnmqpu5jiaqk2e4z7lele7toehjjbzbgmnaydufkayrsrly_9", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "record": { + "text": "quote a post with warn label added", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.record", + "record": { + "cid": "bafyreigwrjbutytae2lgle3t3wt52gkkrpogerioon73w3vsu7vfk6zqee", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rh3spkuw2n" + } + }, + "langs": [ + "ja" + ], + "createdAt": "2023-08-14T11:27:41.506Z" + }, + "embed": { + "$type": "app.bsky.embed.record#view", + "record": { + "$type": "app.bsky.embed.record#viewRecord", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rh3spkuw2n", + "cid": "bafyreigwrjbutytae2lgle3t3wt52gkkrpogerioon73w3vsu7vfk6zqee", + "author": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle": "ioriayane2.bsky.social", + "avatar": "https://cdn.bsky.social/imgproxy/QdJdHqlX6_FaYBBrLNg6OTbw-MdCjwxRj73W41RqLOI/rs:fill:1000:1000:1:0/plain/bafkreiayv34bulrnm5gsnx73b46s2plh76k7fvwcewqrdur7eelf7u6c3u@jpeg", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.follow/3k2fx3qmuob2g", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.follow/3k2fwocixdw2b" + }, + "labels": [] + }, + "value": { + "via": "Hagoromo", + "text": "label test (warn)", + "$type": "app.bsky.feed.post", + "labels": { + "$type": "com.atproto.label.defs#selfLabels", + "values": [ + { + "val": "!warn" + } + ] + }, + "createdAt": "2023-08-12T15:12:49.272Z" + }, + "labels": [ + { + "src": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rh3spkuw2n", + "cid": "bafyreigwrjbutytae2lgle3t3wt52gkkrpogerioon73w3vsu7vfk6zqee", + "val": "!warn", + "cts": "2023-08-12T15:12:49.272Z", + "neg": false + } + ], + "indexedAt": "2023-08-12T15:12:55.585Z", + "embeds": [] + } + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-08-14T11:27:46.252Z", + "viewer": {}, + "labels": [] + } + }, + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3k4w3quvamn2i_10", + "cid": "bafyreiemuasu6a6snzjjhke5tr3f462bfz62t7yqlidkxrpnedbzbrnnou_10", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "record": { + "text": "quote a post with labeling image", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.record", + "record": { + "cid": "bafyreiebuxprnkypyylqmacpp7sirqoqpspuehtityxxtbtre7lodtmhvm", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rhbgiidp2s" + } + }, + "langs": [ + "ja" + ], + "createdAt": "2023-08-14T11:33:11.573Z" + }, + "embed": { + "$type": "app.bsky.embed.record#view", + "record": { + "$type": "app.bsky.embed.record#viewRecord", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rhbgiidp2s", + "cid": "bafyreiebuxprnkypyylqmacpp7sirqoqpspuehtityxxtbtre7lodtmhvm", + "author": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle": "ioriayane2.bsky.social", + "avatar": "https://cdn.bsky.social/imgproxy/QdJdHqlX6_FaYBBrLNg6OTbw-MdCjwxRj73W41RqLOI/rs:fill:1000:1000:1:0/plain/bafkreiayv34bulrnm5gsnx73b46s2plh76k7fvwcewqrdur7eelf7u6c3u@jpeg", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.follow/3k2fx3qmuob2g", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.follow/3k2fwocixdw2b" + }, + "labels": [] + }, + "value": { + "via": "Hagoromo", + "text": "label test (sexual)", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.images", + "images": [ + { + "alt": "", + "image": { + "$type": "blob", + "ref": { + "$link": "bafkreibmmux3wklplvddwjqszdzx3vnvfllhjrbqsnlgtt6fax7ajdjy5y" + }, + "mimeType": "image/jpeg", + "size": 35488 + } + } + ] + }, + "labels": { + "$type": "com.atproto.label.defs#selfLabels", + "values": [ + { + "val": "sexual" + } + ] + }, + "createdAt": "2023-08-12T15:15:57.827Z" + }, + "labels": [ + { + "src": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rhbgiidp2s", + "cid": "bafyreiebuxprnkypyylqmacpp7sirqoqpspuehtityxxtbtre7lodtmhvm", + "val": "sexual", + "cts": "2023-08-12T15:15:57.827Z", + "neg": false + } + ], + "indexedAt": "2023-08-12T15:16:04.088Z", + "embeds": [ + { + "$type": "app.bsky.embed.images#view", + "images": [ + { + "thumb": "https://cdn.bsky.social/imgproxy/5Yw3gWICYYm-gCp6LP206jY_NGm3iPn2iH9BD4pw1ZU/rs:fit:1000:1000:1:0/plain/bafkreibmmux3wklplvddwjqszdzx3vnvfllhjrbqsnlgtt6fax7ajdjy5y@jpeg", + "fullsize": "https://cdn.bsky.social/imgproxy/k46B3Cqu4IiOyilM2gKVFXUWl_6epvzX6d_v6OnyuE0/rs:fit:2000:2000:1:0/plain/bafkreibmmux3wklplvddwjqszdzx3vnvfllhjrbqsnlgtt6fax7ajdjy5y@jpeg", + "alt": "" + } + ] + } + ] + } + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-08-14T11:33:16.301Z", + "viewer": {}, + "labels": [ + { + "src": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rhbgiidp2s", + "cid": "bafyreiebuxprnkypyylqmacpp7sirqoqpspuehtityxxtbtre7lodtmhvm", + "val": "sexual", + "cts": "2023-08-12T15:15:57.827Z", + "neg": false + } + ] + } + }, + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3k4w52nxwdm2j_11", + "cid": "bafyreiegferq3itlq4qapotqm3udyi3eobdigoptoyxubvudv5ttrjf5ka_11", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "record": { + "text": "quote a post with warn label added with image", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.recordWithMedia", + "media": { + "$type": "app.bsky.embed.images", + "images": [ + { + "alt": "", + "image": { + "$type": "blob", + "ref": { + "$link": "bafkreicntacak4uf4kirtl4rsnxrfzaang3ijueuqku5hbljde467di2sa" + }, + "mimeType": "image/jpeg", + "size": 29143 + } + } + ] + }, + "record": { + "$type": "app.bsky.embed.record", + "record": { + "cid": "bafyreigwrjbutytae2lgle3t3wt52gkkrpogerioon73w3vsu7vfk6zqee", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rh3spkuw2n" + } + } + }, + "langs": [], + "createdAt": "2023-08-14T11:56:33.564Z" + }, + "embed": { + "$type": "app.bsky.embed.recordWithMedia#view", + "record": { + "record": { + "$type": "app.bsky.embed.record#viewRecord", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rh3spkuw2n", + "cid": "bafyreigwrjbutytae2lgle3t3wt52gkkrpogerioon73w3vsu7vfk6zqee", + "author": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle": "ioriayane2.bsky.social", + "avatar": "https://cdn.bsky.social/imgproxy/QdJdHqlX6_FaYBBrLNg6OTbw-MdCjwxRj73W41RqLOI/rs:fill:1000:1000:1:0/plain/bafkreiayv34bulrnm5gsnx73b46s2plh76k7fvwcewqrdur7eelf7u6c3u@jpeg", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.follow/3k2fx3qmuob2g", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.follow/3k2fwocixdw2b" + }, + "labels": [] + }, + "value": { + "via": "Hagoromo", + "text": "label test (warn)", + "$type": "app.bsky.feed.post", + "labels": { + "$type": "com.atproto.label.defs#selfLabels", + "values": [ + { + "val": "!warn" + } + ] + }, + "createdAt": "2023-08-12T15:12:49.272Z" + }, + "labels": [ + { + "src": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rh3spkuw2n", + "cid": "bafyreigwrjbutytae2lgle3t3wt52gkkrpogerioon73w3vsu7vfk6zqee", + "val": "!warn", + "cts": "2023-08-12T15:12:49.272Z", + "neg": false + } + ], + "indexedAt": "2023-08-12T15:12:55.585Z", + "embeds": [] + } + }, + "media": { + "$type": "app.bsky.embed.images#view", + "images": [ + { + "thumb": "https://cdn.bsky.social/imgproxy/wCVae3ye6cBCuNZyNpC-dixwF25wmP0Z9_YJVh1JZoc/rs:fit:1000:1000:1:0/plain/bafkreicntacak4uf4kirtl4rsnxrfzaang3ijueuqku5hbljde467di2sa@jpeg", + "fullsize": "https://cdn.bsky.social/imgproxy/hKxJL_w7SRQaeOIZpWEQnfNpz5lCisdyZ3-bnBwhJb4/rs:fit:2000:2000:1:0/plain/bafkreicntacak4uf4kirtl4rsnxrfzaang3ijueuqku5hbljde467di2sa@jpeg", + "alt": "" + } + ] + } + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-08-14T11:56:38.397Z", + "viewer": {}, + "labels": [ + { + "src": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rhbgiidp2s", + "cid": "bafyreiebuxprnkypyylqmacpp7sirqoqpspuehtityxxtbtre7lodtmhvm", + "val": "sexual", + "cts": "2023-08-12T15:15:57.827Z", + "neg": false + }, + { + "src": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rh3spkuw2n", + "cid": "bafyreigwrjbutytae2lgle3t3wt52gkkrpogerioon73w3vsu7vfk6zqee", + "val": "!warn", + "cts": "2023-08-12T15:12:49.272Z", + "neg": false + } + ] + } + }, + { + "post": { + "uri": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3k4w5fuw72y2d_12", + "cid": "bafyreia3gadukf62bq3pq46kr3ewjpsao2ltbrlaios332oxqfggbaqha4_12", + "author": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "record": { + "text": "quote a post with labeling image with image", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.recordWithMedia", + "media": { + "$type": "app.bsky.embed.images", + "images": [ + { + "alt": "", + "image": { + "$type": "blob", + "ref": { + "$link": "bafkreicntacak4uf4kirtl4rsnxrfzaang3ijueuqku5hbljde467di2sa" + }, + "mimeType": "image/jpeg", + "size": 29143 + } + } + ] + }, + "record": { + "$type": "app.bsky.embed.record", + "record": { + "cid": "bafyreiebuxprnkypyylqmacpp7sirqoqpspuehtityxxtbtre7lodtmhvm", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rhbgiidp2s" + } + } + }, + "langs": [], + "createdAt": "2023-08-14T12:02:49.949Z" + }, + "embed": { + "$type": "app.bsky.embed.recordWithMedia#view", + "record": { + "record": { + "$type": "app.bsky.embed.record#viewRecord", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rhbgiidp2s", + "cid": "bafyreiebuxprnkypyylqmacpp7sirqoqpspuehtityxxtbtre7lodtmhvm", + "author": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle": "ioriayane2.bsky.social", + "avatar": "https://cdn.bsky.social/imgproxy/QdJdHqlX6_FaYBBrLNg6OTbw-MdCjwxRj73W41RqLOI/rs:fill:1000:1000:1:0/plain/bafkreiayv34bulrnm5gsnx73b46s2plh76k7fvwcewqrdur7eelf7u6c3u@jpeg", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.follow/3k2fx3qmuob2g", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.follow/3k2fwocixdw2b" + }, + "labels": [] + }, + "value": { + "via": "Hagoromo", + "text": "label test (sexual)", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.images", + "images": [ + { + "alt": "", + "image": { + "$type": "blob", + "ref": { + "$link": "bafkreibmmux3wklplvddwjqszdzx3vnvfllhjrbqsnlgtt6fax7ajdjy5y" + }, + "mimeType": "image/jpeg", + "size": 35488 + } + } + ] + }, + "labels": { + "$type": "com.atproto.label.defs#selfLabels", + "values": [ + { + "val": "sexual" + } + ] + }, + "createdAt": "2023-08-12T15:15:57.827Z" + }, + "labels": [ + { + "src": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4rhbgiidp2s", + "cid": "bafyreiebuxprnkypyylqmacpp7sirqoqpspuehtityxxtbtre7lodtmhvm", + "val": "sexual", + "cts": "2023-08-12T15:15:57.827Z", + "neg": false + } + ], + "indexedAt": "2023-08-12T15:16:04.088Z", + "embeds": [ + { + "$type": "app.bsky.embed.images#view", + "images": [ + { + "thumb": "https://cdn.bsky.social/imgproxy/5Yw3gWICYYm-gCp6LP206jY_NGm3iPn2iH9BD4pw1ZU/rs:fit:1000:1000:1:0/plain/bafkreibmmux3wklplvddwjqszdzx3vnvfllhjrbqsnlgtt6fax7ajdjy5y@jpeg", + "fullsize": "https://cdn.bsky.social/imgproxy/k46B3Cqu4IiOyilM2gKVFXUWl_6epvzX6d_v6OnyuE0/rs:fit:2000:2000:1:0/plain/bafkreibmmux3wklplvddwjqszdzx3vnvfllhjrbqsnlgtt6fax7ajdjy5y@jpeg", + "alt": "" + } + ] + } + ] + } + }, + "media": { + "$type": "app.bsky.embed.images#view", + "images": [ + { + "thumb": "https://cdn.bsky.social/imgproxy/wCVae3ye6cBCuNZyNpC-dixwF25wmP0Z9_YJVh1JZoc/rs:fit:1000:1000:1:0/plain/bafkreicntacak4uf4kirtl4rsnxrfzaang3ijueuqku5hbljde467di2sa@jpeg", + "fullsize": "https://cdn.bsky.social/imgproxy/hKxJL_w7SRQaeOIZpWEQnfNpz5lCisdyZ3-bnBwhJb4/rs:fit:2000:2000:1:0/plain/bafkreicntacak4uf4kirtl4rsnxrfzaang3ijueuqku5hbljde467di2sa@jpeg", + "alt": "" + } + ] + } + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-08-14T12:02:54.725Z", + "viewer": {}, + "labels": [] + } + }, + { + "post": { + "uri": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3k4vp3jcppg2g11_13", + "cid": "bafyreiejog3yvjc2tdg4muknodbplaib2yqftukwurd4qjcnal3zdxu4ni11_13", + "author": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle": "ioriayane2.bsky.social", + "avatar": "https://cdn.bsky.social/imgproxy/QdJdHqlX6_FaYBBrLNg6OTbw-MdCjwxRj73W41RqLOI/rs:fill:1000:1000:1:0/plain/bafkreiayv34bulrnm5gsnx73b46s2plh76k7fvwcewqrdur7eelf7u6c3u@jpeg", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.follow/3k2fx3qmuob2g", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.follow/3k2fwocixdw2b" + }, + "labels": [] + }, + "record": { + "text": "quote blocked user's post", + "$type": "app.bsky.feed.post", + "embed": { + "$type": "app.bsky.embed.record", + "record": { + "cid": "bafyreihyz7ydlnjtn7f3cvobsxf242vchhr3cjx5dwvk4t5r4knm2nxony", + "uri": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.post/3k4vndstk7t2g" + } + }, + "langs": [ + "ja" + ], + "createdAt": "2023-08-14T07:46:29.974Z" + }, + "embed": { + "$type": "app.bsky.embed.record#view", + "record": { + "$type": "app.bsky.embed.record#viewBlocked", + "uri": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.post/3k4vndstk7t2g", + "blocked": true, + "author": { + "did": "did:plc:ipj5qejfoqu6eukvt72uhyit", + "viewer": { + "blockedBy": false, + "blocking": "at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.graph.block/3k4w7jojpwx2v" + } + } + } + }, + "replyCount": 0, + "repostCount": 0, + "likeCount": 0, + "indexedAt": "2023-08-14T07:46:34.614Z", + "viewer": {}, + "labels": [] + } + } + ] +} \ No newline at end of file diff --git a/tests/hagoromo_test/tst_hagoromo_test.cpp b/tests/hagoromo_test/tst_hagoromo_test.cpp index b7ddf835..16372148 100644 --- a/tests/hagoromo_test/tst_hagoromo_test.cpp +++ b/tests/hagoromo_test/tst_hagoromo_test.cpp @@ -36,6 +36,7 @@ private slots: void test_TimelineListModel_quote_hide2(); void test_TimelineListModel_quote_label(); void test_NotificationListModel_warn(); + void test_TimelineListModel_next(); private: WebServer m_mockServer; @@ -1153,6 +1154,77 @@ void hagoromo_test::test_NotificationListModel_warn() == false); } +void hagoromo_test::test_TimelineListModel_next() +{ + int row = 0; + TimelineListModel model; + model.setAccount(m_service + "/timeline/next/1st", QString(), QString(), QString(), "dummy", + QString()); + model.setDisplayInterval(0); + { + QSignalSpy spy(&model, SIGNAL(runningChanged())); + model.getLatest(); + spy.wait(); + QVERIFY2(spy.count() == 2, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + } + QVERIFY(model.rowCount() == 6); + + row = 0; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreig7i2uyva4rpgxv3slogiwf5fvlwy2wx4bjvwuoywy6e7ojjvcrky_1"); + row = 1; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreiajxwbfoa5cbnphxcwvunisgjiqkjjqkqxpnr4mgfu3vqqupr6wca_2"); + row = 2; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreif2ux6qo4b2ez266iewichrjvqgeeehui5c7lo3gcglrzdi54pjja_3"); + QVERIFY(model.item(row, TimelineListModel::IsRepostedRole).toBool() == true); + + row = 3; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreidmpxa2bdcohuxa4p62q3d6oja75mohihxsliuasvmsxds3utokqa_4"); + row = 4; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreif2ux6qo4b2ez266iewichrjvqgeeehui5c7lo3gcglrzdi54pjjb_5"); + row = 5; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreihr2hrmavhzdpmnc65udreph5vfmd3xceqtw2jm3b4clbfbacgsqe_6"); + + model.setAccount(m_service + "/timeline/next/2nd", QString(), QString(), QString(), "dummy", + QString()); + model.setDisplayInterval(0); + { + QSignalSpy spy(&model, SIGNAL(runningChanged())); + model.getNext(); + spy.wait(); + QVERIFY2(spy.count() == 2, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + } + QVERIFY(model.rowCount() == 13); + + row = 6; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreiejog3yvjc2tdg4muknodbplaib2yqftukwurd4qjcnal3zdxu4ni_7"); + row = 7; + QVERIFY2(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreib67ewj54g6maljtbclhno7mrkquf3w7wbex2woedj5m23mjwyite_8", + model.item(row, TimelineListModel::CidRole).toString().toLocal8Bit()); + row = 8; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreigj7v4cnmqpu5jiaqk2e4z7lele7toehjjbzbgmnaydufkayrsrly_9"); + row = 9; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreiemuasu6a6snzjjhke5tr3f462bfz62t7yqlidkxrpnedbzbrnnou_10"); + row = 10; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreiegferq3itlq4qapotqm3udyi3eobdigoptoyxubvudv5ttrjf5ka_11"); + row = 11; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreia3gadukf62bq3pq46kr3ewjpsao2ltbrlaios332oxqfggbaqha4_12"); + row = 12; + QVERIFY(model.item(row, TimelineListModel::CidRole).toString() + == "bafyreiejog3yvjc2tdg4muknodbplaib2yqftukwurd4qjcnal3zdxu4ni11_13"); +} + void hagoromo_test::test_RecordOperatorCreateRecord(const QByteArray &body) { QJsonDocument json_doc = QJsonDocument::fromJson(body); From c4de53db9eddc07d91d08fb1ee95ee93b512a646 Mon Sep 17 00:00:00 2001 From: Takayuki Orito Date: Sun, 3 Sep 2023 15:01:09 +0900 Subject: [PATCH 03/10] =?UTF-8?q?ALT=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC?= =?UTF-8?q?=E3=82=B8=E5=AF=BE=E5=BF=9C=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 最大表示中の画像をカーソルで移動 * 画像のAltメッセージの表示対応 * Altメッセージ入力用のレイアウト追加 * altメッセージ付きの投稿 * レイアウト修正 * 実装漏れ修正 --- app/app.pro | 1 + app/qml/dialogs/AltEditDialog.qml | 72 +++++++++++++++++++ app/qml/dialogs/PostDialog.qml | 64 ++++++++++++++--- app/qml/main.qml | 2 +- app/qml/parts/ImagePreview.qml | 12 ++++ app/qml/view/ColumnView.qml | 16 ++--- app/qml/view/ImageFullView.qml | 60 +++++++++++++--- app/qml/view/NotificationListView.qml | 8 ++- app/qml/view/PostThreadView.qml | 8 ++- app/qml/view/ProfileView.qml | 10 +-- app/qml/view/TimelineView.qml | 8 ++- app/qtquick/notificationlistmodel.cpp | 32 +++++++-- app/qtquick/notificationlistmodel.h | 2 + app/qtquick/recordoperator.cpp | 19 +++-- app/qtquick/recordoperator.h | 10 ++- app/qtquick/timelinelistmodel.cpp | 37 ++++++++-- app/qtquick/timelinelistmodel.h | 2 + .../repo/comatprotorepocreaterecord.cpp | 2 +- lib/atprotocol/lexicons_func_unknown.cpp | 29 +++++--- lib/atprotocol/lexicons_func_unknown.h | 12 +++- 20 files changed, 330 insertions(+), 76 deletions(-) create mode 100644 app/qml/dialogs/AltEditDialog.qml diff --git a/app/app.pro b/app/app.pro index 992a6a86..cc75a832 100644 --- a/app/app.pro +++ b/app/app.pro @@ -18,6 +18,7 @@ QML_FILES = \ qml/data/ColumnSettings.qml \ qml/dialogs/AccountDialog.qml \ qml/dialogs/AddColumnDialog.qml \ + qml/dialogs/AltEditDialog.qml \ qml/dialogs/ColumnSettingDialog.qml \ qml/dialogs/ContentFilterSettingDialog.qml \ qml/dialogs/DiscoverFeedsDialog.qml \ diff --git a/app/qml/dialogs/AltEditDialog.qml b/app/qml/dialogs/AltEditDialog.qml new file mode 100644 index 00000000..a8b1b313 --- /dev/null +++ b/app/qml/dialogs/AltEditDialog.qml @@ -0,0 +1,72 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls.Material 2.15 + +import tech.relog.hagoromo.singleton 1.0 + +Dialog { + id: altEditDialog + modal: true + x: (parent.width - width) * 0.5 + y: (parent.height - height) * 0.5 + closePolicy: Popup.NoAutoClose + + property string embedImage: "" + property string embedAlt: "" + + onOpened: { + altTextArea.text = altEditDialog.embedAlt + altTextArea.forceActiveFocus() + } + + ColumnLayout { + Image { + id: image + Layout.preferredWidth: 300 * AdjustedValues.ratio + Layout.preferredHeight: image.paintedWidth > image.paintedHeight ? image.paintedHeight : 300 * AdjustedValues.ratio + Layout.maximumWidth: 300 * AdjustedValues.ratio + Layout.maximumHeight: 300 * AdjustedValues.ratio + fillMode: Image.PreserveAspectFit + source: altEditDialog.embedImage + } + ScrollView { + Layout.preferredWidth: 300 * AdjustedValues.ratio + Layout.preferredHeight: 75 * AdjustedValues.ratio + TextArea { + id: altTextArea + verticalAlignment: TextInput.AlignTop + wrapMode: TextInput.WordWrap + selectByMouse: true + font.pointSize: AdjustedValues.f10 + } + } + RowLayout { + Button { + flat: true + font.pointSize: AdjustedValues.f10 + text: qsTr("Cancel") + onClicked: { + altEditDialog.embedImage = "" + altEditDialog.embedAlt = "" + altTextArea.text = "" + altEditDialog.close() + } + } + Item { + Layout.fillWidth: true + Layout.preferredHeight: 1 + } + Button { + font.pointSize: AdjustedValues.f10 + text: qsTr("Add") + onClicked: { + altEditDialog.embedImage = "" + altEditDialog.embedAlt = altTextArea.text + altTextArea.text = "" + altEditDialog.accept() + } + } + } + } +} diff --git a/app/qml/dialogs/PostDialog.qml b/app/qml/dialogs/PostDialog.qml index 12968ead..1ec0c31a 100644 --- a/app/qml/dialogs/PostDialog.qml +++ b/app/qml/dialogs/PostDialog.qml @@ -67,6 +67,7 @@ Dialog { postText.clear() embedImagePreview.embedImages = [] + embedImagePreview.embedAlts = [] externalLink.clear() addingExternalLinkUrlText.text = "" } @@ -276,12 +277,33 @@ Dialog { Repeater { id: embedImagePreview property var embedImages: [] + property var embedAlts: [] model: embedImagePreview.embedImages delegate: ImageWithIndicator { Layout.preferredWidth: 97 * AdjustedValues.ratio Layout.preferredHeight: 97 * AdjustedValues.ratio fillMode: Image.PreserveAspectCrop source: modelData + TagLabel { + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: 3 + visible: model.index < embedImagePreview.embedAlts.length ? embedImagePreview.embedAlts[model.index].length > 0 : false + source: "" + fontPointSize: AdjustedValues.f8 + text: "Alt" + } + MouseArea { + anchors.fill: parent + onClicked: { + altEditDialog.editingIndex = model.index + altEditDialog.embedImage = modelData + if(model.index < embedImagePreview.embedAlts.length){ + altEditDialog.embedAlt = embedImagePreview.embedAlts[model.index] + } + altEditDialog.open() + } + } IconButton { enabled: !createRecord.running width: AdjustedValues.b24 @@ -290,18 +312,23 @@ Dialog { anchors.right: parent.right anchors.margins: 5 iconSource: "../images/delete.png" - onClicked: { - var images = embedImagePreview.embedImages - var new_images = [] - for(var i=0; i 0){ - createRecord.setImages(embedImagePreview.embedImages) + createRecord.setImages(embedImagePreview.embedImages, embedImagePreview.embedAlts) createRecord.postWithImages() }else{ createRecord.post() @@ -461,6 +488,7 @@ Dialog { return } var new_images = embedImagePreview.embedImages + var new_alts = embedImagePreview.embedAlts for(var i=0; i= 4){ break @@ -469,8 +497,10 @@ Dialog { continue; } new_images.push(files[i]) + new_alts.push("") } embedImagePreview.embedImages = new_images + embedImagePreview.embedAlts = new_alts } property string prevFolder } @@ -482,4 +512,16 @@ Dialog { postLanguagesButton.setLanguageText(selectedLanguages) } } + + AltEditDialog { + id: altEditDialog + property int editingIndex: -1 + onAccepted: { + if(editingIndex >= 0 && editingIndex < embedImagePreview.embedAlts.length){ + var alts = embedImagePreview.embedAlts + alts[editingIndex] = altEditDialog.embedAlt + embedImagePreview.embedAlts = alts + } + } + } } diff --git a/app/qml/main.qml b/app/qml/main.qml index 881b086d..ea1b1830 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -260,7 +260,7 @@ ApplicationWindow { columnManageModel.append(account_uuid, 5, false, 300000, 350, handle, did) scrollView.showRightMost() } - onRequestViewImages: (index, paths) => imageFullView.open(index, paths) + onRequestViewImages: (index, paths, alts) => imageFullView.open(index, paths, alts) onRequestViewGeneratorFeed: (account_uuid, name, uri) => { columnManageModel.append(account.uuid, 4, false, 300000, 400, name, uri) scrollView.showRightMost() diff --git a/app/qml/parts/ImagePreview.qml b/app/qml/parts/ImagePreview.qml index 2056792c..fb0c2b43 100644 --- a/app/qml/parts/ImagePreview.qml +++ b/app/qml/parts/ImagePreview.qml @@ -2,6 +2,8 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import tech.relog.hagoromo.singleton 1.0 + import "../controls" GridLayout { @@ -13,6 +15,7 @@ GridLayout { property int layoutWidth: 100 property var embedImages: [] + property var embedAlts: [] property int cellWidth: imagePreviewLayout.layoutWidth * 0.5 - 3 @@ -28,6 +31,15 @@ GridLayout { Layout.columnSpan: isWide ? 2 : 1 fillMode: Image.PreserveAspectCrop source: modelData + TagLabel { + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: 3 + visible: model.index < embedAlts.length ? embedAlts[model.index].length > 0 : false + source: "" + fontPointSize: AdjustedValues.f8 + text: "Alt" + } MouseArea { anchors.fill: parent onClicked: imagePreviewLayout.requestViewImages(model.index) diff --git a/app/qml/view/ColumnView.qml b/app/qml/view/ColumnView.qml index 0a3a0a4b..43f86b28 100644 --- a/app/qml/view/ColumnView.qml +++ b/app/qml/view/ColumnView.qml @@ -37,7 +37,7 @@ ColumnLayout { string avatar, string display_name, string handle, string indexed_at, string text) signal requestMention(string account_uuid, string handle) signal requestViewAuthorFeed(string account_uuid, string did, string handle) - signal requestViewImages(int index, var paths) + signal requestViewImages(int index, var paths, var alts) signal requestViewGeneratorFeed(string account_uuid, string name, string uri) signal requestReportPost(string account_uuid, string uri, string cid) signal requestReportAccount(string account_uuid, string did) @@ -76,7 +76,7 @@ ColumnLayout { columnStackView.push(postThreadComponent, { "postThreadUri": uri }) } - onRequestViewImages: (index, paths) => columnView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) @@ -110,7 +110,7 @@ ColumnLayout { // これはPostThreadViewのプロパティにダイレクトに設定する columnStackView.push(postThreadComponent, { "postThreadUri": uri }) } - onRequestViewImages: (index, paths) => columnView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) @@ -132,7 +132,7 @@ ColumnLayout { // これはPostThreadViewのプロパティにダイレクトに設定する columnStackView.push(postThreadComponent, { "postThreadUri": uri }) } - onRequestViewImages: (index, paths) => columnView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) @@ -163,7 +163,7 @@ ColumnLayout { onRequestViewAuthorFeed: (did, handle) => columnView.requestViewAuthorFeed(account.uuid, did, handle) - onRequestViewImages: (index, paths) => columnView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) @@ -201,7 +201,7 @@ ColumnLayout { columnStackView.push(postThreadComponent, { "postThreadUri": uri }) } - onRequestViewImages: (index, paths) => columnView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) @@ -248,7 +248,7 @@ ColumnLayout { columnStackView.push(postThreadComponent, { "postThreadUri": uri }) } - onRequestViewImages: (index, paths) => columnView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) @@ -280,7 +280,7 @@ ColumnLayout { columnStackView.push(postThreadComponent, { "postThreadUri": uri }) } - onRequestViewImages: (index, paths) => columnView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) diff --git a/app/qml/view/ImageFullView.qml b/app/qml/view/ImageFullView.qml index e1efa9a1..7464f9de 100644 --- a/app/qml/view/ImageFullView.qml +++ b/app/qml/view/ImageFullView.qml @@ -1,6 +1,9 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import QtQuick.Controls.Material 2.15 + +import tech.relog.hagoromo.singleton 1.0 import "../controls" @@ -9,9 +12,11 @@ Rectangle { color: "#aa000000" property var sources: [] + property var alts: [] - function open(index, sources){ + function open(index, sources, alts){ imageFullView.sources = sources + imageFullView.alts = alts imageFullView.visible = true imageFullListView.currentIndex = index } @@ -21,6 +26,16 @@ Rectangle { sequence: "Esc" onActivated: imageFullView.visible = false } + Shortcut { + enabled: imageFullView.visible && leftMoveButton.enabled + sequence: "left" + onActivated: leftMoveButton.clicked() + } + Shortcut { + enabled: imageFullView.visible && righttMoveButton.enabled + sequence: "right" + onActivated: righttMoveButton.clicked() + } MouseArea { anchors.fill: parent @@ -35,17 +50,42 @@ Rectangle { interactive: false model: imageFullView.sources - delegate: ImageWithIndicator { + delegate: ColumnLayout { width: imageFullListView.width height: imageFullListView.height - fillMode: Image.PreserveAspectFit - source: modelData - MouseArea { - x: parent.width/2 - width/2 - y: parent.height/2 - height/2 - width: parent.paintedWidth - height: parent.paintedHeight - onClicked: (mouse) => mouse.accepted = false + spacing: 0 + ImageWithIndicator { + id: image + Layout.preferredWidth: imageFullListView.width +// Layout.preferredHeight: imageFullListView.height - altMessage.height - 10 + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: modelData + MouseArea { + x: parent.width/2 - width/2 + y: parent.height/2 - height/2 + width: parent.paintedWidth + height: parent.paintedHeight + onClicked: (mouse) => mouse.accepted = false + } + } + Label { + id: altMessage + Layout.preferredWidth: image.paintedWidth + Layout.alignment: Qt.AlignHCenter + topPadding: 5 + leftPadding: 5 + rightPadding: 5 + bottomPadding: 5 + visible: text.length > 0 + wrapMode: Text.Wrap + font.pointSize: AdjustedValues.f10 + text: model.index < imageFullView.alts.length ? imageFullView.alts[model.index] : "" + background: Rectangle { + width: altMessage.width + height: altMessage.height + color: Material.backgroundColor + } } } } diff --git a/app/qml/view/NotificationListView.qml b/app/qml/view/NotificationListView.qml index e8cd9404..afa29f67 100644 --- a/app/qml/view/NotificationListView.qml +++ b/app/qml/view/NotificationListView.qml @@ -26,7 +26,7 @@ ScrollView { string avatar, string display_name, string handle, string indexed_at, string text) signal requestQuote(string cid, string uri, string avatar, string display_name, string handle, string indexed_at, string text) signal requestViewThread(string uri) - signal requestViewImages(int index, var paths) + signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) signal requestViewGeneratorFeed(string name, string uri) signal requestReportPost(string uri, string cid) @@ -97,7 +97,8 @@ ScrollView { contentMediaFilterFrame.visible: model.contentMediaFilterMatched contentMediaFilterFrame.labelText: model.contentMediaFilterMessage postImagePreview.embedImages: model.embedImages - postImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.embedImagesFull) + postImagePreview.embedAlts: model.embedImagesAlt + postImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.embedImagesFull, model.embedImagesAlt) quoteRecordDisplayName: model.quoteRecordDisplayName quoteRecordHandle: model.quoteRecordHandle @@ -105,7 +106,8 @@ ScrollView { quoteRecordIndexedAt: model.quoteRecordIndexedAt quoteRecordRecordText: model.quoteRecordRecordText quoteRecordImagePreview.embedImages: model.quoteRecordEmbedImages - quoteRecordImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.quoteRecordEmbedImagesFull) + quoteRecordImagePreview.embedAlts: model.quoteRecordEmbedImagesAlt + quoteRecordImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.quoteRecordEmbedImagesFull, model.quoteRecordEmbedImagesAlt) // generatorViewFrame.visible: model.hasGeneratorFeed // generatorViewFrame.onClicked: notificationListView.requestViewGeneratorFeed(model.generatorFeedDisplayName, model.generatorFeedUri) diff --git a/app/qml/view/PostThreadView.qml b/app/qml/view/PostThreadView.qml index 97fefa51..d22501ad 100644 --- a/app/qml/view/PostThreadView.qml +++ b/app/qml/view/PostThreadView.qml @@ -25,7 +25,7 @@ ColumnLayout { string avatar, string display_name, string handle, string indexed_at, string text) signal requestQuote(string cid, string uri, string avatar, string display_name, string handle, string indexed_at, string text) signal requestViewThread(string uri) - signal requestViewImages(int index, var paths) + signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) signal requestViewGeneratorFeed(string name, string uri) signal requestReportPost(string uri, string cid) @@ -136,7 +136,8 @@ ColumnLayout { contentMediaFilterFrame.visible: model.contentMediaFilterMatched contentMediaFilterFrame.labelText: model.contentMediaFilterMessage postImagePreview.embedImages: model.embedImages - postImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.embedImagesFull) + postImagePreview.embedAlts: model.embedImagesAlt + postImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.embedImagesFull, model.embedImagesAlt) quoteFilterFrame.visible: model.quoteFilterMatched && !model.quoteRecordBlocked quoteFilterFrame.labelText: qsTr("Quoted content warning") @@ -153,7 +154,8 @@ ColumnLayout { quoteRecordAuthor.indexedAt: model.quoteRecordIndexedAt quoteRecordRecordText.text: model.quoteRecordRecordText quoteRecordImagePreview.embedImages: model.quoteRecordEmbedImages - quoteRecordImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.quoteRecordEmbedImagesFull) + quoteRecordImagePreview.embedAlts: model.quoteRecordEmbedImagesAlt + quoteRecordImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.quoteRecordEmbedImagesFull, model.quoteRecordEmbedImagesAlt) externalLinkFrame.visible: model.hasExternalLink externalLinkFrame.onClicked: Qt.openUrlExternally(model.externalLinkUri) diff --git a/app/qml/view/ProfileView.qml b/app/qml/view/ProfileView.qml index 9ee2ec12..90367a87 100644 --- a/app/qml/view/ProfileView.qml +++ b/app/qml/view/ProfileView.qml @@ -35,7 +35,7 @@ ColumnLayout { signal requestQuote(string cid, string uri, string avatar, string display_name, string handle, string indexed_at, string text) signal requestMention(string handle) signal requestViewThread(string uri) - signal requestViewImages(int index, var paths) + signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) signal requestViewGeneratorFeed(string name, string uri) signal requestViewAuthorFeed(string did, string handle) @@ -420,7 +420,7 @@ ColumnLayout { profileView.requestQuote(cid, uri, avatar, display_name, handle, indexed_at, text) onRequestViewThread: (uri) => profileView.requestViewThread(uri) - onRequestViewImages: (index, paths) => profileView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => profileView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => { if(did !== profileView.userDid){ profileView.requestViewProfile(did) @@ -449,7 +449,7 @@ ColumnLayout { profileView.requestQuote(cid, uri, avatar, display_name, handle, indexed_at, text) onRequestViewThread: (uri) => profileView.requestViewThread(uri) - onRequestViewImages: (index, paths) => profileView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => profileView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => { if(did !== profileView.userDid){ profileView.requestViewProfile(did) @@ -478,7 +478,7 @@ ColumnLayout { profileView.requestQuote(cid, uri, avatar, display_name, handle, indexed_at, text) onRequestViewThread: (uri) => profileView.requestViewThread(uri) - onRequestViewImages: (index, paths) => profileView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => profileView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => { if(did !== profileView.userDid){ profileView.requestViewProfile(did) @@ -507,7 +507,7 @@ ColumnLayout { profileView.requestQuote(cid, uri, avatar, display_name, handle, indexed_at, text) onRequestViewThread: (uri) => profileView.requestViewThread(uri) - onRequestViewImages: (index, paths) => profileView.requestViewImages(index, paths) + onRequestViewImages: (index, paths, alts) => profileView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => { if(did !== profileView.userDid){ profileView.requestViewProfile(did) diff --git a/app/qml/view/TimelineView.qml b/app/qml/view/TimelineView.qml index 95dfe0f1..7d4434f2 100644 --- a/app/qml/view/TimelineView.qml +++ b/app/qml/view/TimelineView.qml @@ -27,7 +27,7 @@ ScrollView { string avatar, string display_name, string handle, string indexed_at, string text) signal requestQuote(string cid, string uri, string avatar, string display_name, string handle, string indexed_at, string text) signal requestViewThread(string uri) - signal requestViewImages(int index, var paths) + signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) signal requestViewGeneratorFeed(string name, string uri) signal requestReportPost(string uri, string cid) @@ -107,7 +107,8 @@ ScrollView { contentMediaFilterFrame.visible: model.contentMediaFilterMatched contentMediaFilterFrame.labelText: model.contentMediaFilterMessage postImagePreview.embedImages: model.embedImages - postImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.embedImagesFull) + postImagePreview.embedAlts: model.embedImagesAlt + postImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.embedImagesFull, model.embedImagesAlt) quoteFilterFrame.visible: model.quoteFilterMatched && !model.quoteRecordBlocked quoteFilterFrame.labelText: qsTr("Quoted content warning") @@ -124,7 +125,8 @@ ScrollView { quoteRecordAuthor.indexedAt: model.quoteRecordIndexedAt quoteRecordRecordText.text: model.quoteRecordRecordText quoteRecordImagePreview.embedImages: model.quoteRecordEmbedImages - quoteRecordImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.quoteRecordEmbedImagesFull) + quoteRecordImagePreview.embedAlts: model.quoteRecordEmbedImagesAlt + quoteRecordImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.quoteRecordEmbedImagesFull, model.quoteRecordEmbedImagesAlt) externalLinkFrame.visible: model.hasExternalLink externalLinkFrame.onClicked: Qt.openUrlExternally(model.externalLinkUri) diff --git a/app/qtquick/notificationlistmodel.cpp b/app/qtquick/notificationlistmodel.cpp index 4dfac897..5dd56b6e 100644 --- a/app/qtquick/notificationlistmodel.cpp +++ b/app/qtquick/notificationlistmodel.cpp @@ -66,13 +66,22 @@ QVariant NotificationListModel::item(int row, NotificationListModelRoles role) c else if (role == EmbedImagesRole) { if (m_postHash.contains(current.cid)) return AtProtocolType::LexiconsTypeUnknown::copyImagesFromPostView( - m_postHash[current.cid], true); + m_postHash[current.cid], + AtProtocolType::LexiconsTypeUnknown::CopyImageType::Thumb); else return QStringList(); } else if (role == EmbedImagesFullRole) { if (m_postHash.contains(current.cid)) return AtProtocolType::LexiconsTypeUnknown::copyImagesFromPostView( - m_postHash[current.cid], false); + m_postHash[current.cid], + AtProtocolType::LexiconsTypeUnknown::CopyImageType::FullSize); + else + return QStringList(); + } else if (role == EmbedImagesAltRole) { + if (m_postHash.contains(current.cid)) + return AtProtocolType::LexiconsTypeUnknown::copyImagesFromPostView( + m_postHash[current.cid], + AtProtocolType::LexiconsTypeUnknown::CopyImageType::Alt); else return QStringList(); @@ -223,15 +232,24 @@ QVariant NotificationListModel::item(int row, NotificationListModelRoles role) c } else if (role == QuoteRecordEmbedImagesRole) { if (m_postHash.contains(record_cid)) return AtProtocolType::LexiconsTypeUnknown::copyImagesFromPostView( - m_postHash[record_cid], true); + m_postHash[record_cid], + AtProtocolType::LexiconsTypeUnknown::CopyImageType::Thumb); else - return QString(); + return QStringList(); } else if (role == QuoteRecordEmbedImagesFullRole) { if (m_postHash.contains(record_cid)) return AtProtocolType::LexiconsTypeUnknown::copyImagesFromPostView( - m_postHash[record_cid], false); + m_postHash[record_cid], + AtProtocolType::LexiconsTypeUnknown::CopyImageType::FullSize); else - return QString(); + return QStringList(); + } else if (role == QuoteRecordEmbedImagesAltRole) { + if (m_postHash.contains(record_cid)) + return AtProtocolType::LexiconsTypeUnknown::copyImagesFromPostView( + m_postHash[record_cid], + AtProtocolType::LexiconsTypeUnknown::CopyImageType::Alt); + else + return QStringList(); } else if (role == QuoteRecordIsRepostedRole) { if (m_postHash.contains(record_cid)) return m_postHash[record_cid].viewer.repost.contains(account().did); @@ -639,6 +657,7 @@ QHash NotificationListModel::roleNames() const roles[IndexedAtRole] = "indexedAt"; roles[EmbedImagesRole] = "embedImages"; roles[EmbedImagesFullRole] = "embedImagesFull"; + roles[EmbedImagesAltRole] = "embedImagesAlt"; roles[IsRepostedRole] = "isReposted"; roles[IsLikedRole] = "isLiked"; @@ -656,6 +675,7 @@ QHash NotificationListModel::roleNames() const roles[QuoteRecordRecordTextRole] = "quoteRecordRecordText"; roles[QuoteRecordEmbedImagesRole] = "quoteRecordEmbedImages"; roles[QuoteRecordEmbedImagesFullRole] = "quoteRecordEmbedImagesFull"; + roles[QuoteRecordEmbedImagesAltRole] = "quoteRecordEmbedImagesAlt"; roles[QuoteRecordIsRepostedRole] = "quoteRecordIsReposted"; roles[QuoteRecordIsLikedRole] = "quoteRecordIsLiked"; diff --git a/app/qtquick/notificationlistmodel.h b/app/qtquick/notificationlistmodel.h index e32b5028..637d8aab 100644 --- a/app/qtquick/notificationlistmodel.h +++ b/app/qtquick/notificationlistmodel.h @@ -44,6 +44,7 @@ class NotificationListModel : public AtpAbstractListModel IndexedAtRole, EmbedImagesRole, EmbedImagesFullRole, + EmbedImagesAltRole, IsRepostedRole, IsLikedRole, RepostedUriRole, @@ -60,6 +61,7 @@ class NotificationListModel : public AtpAbstractListModel QuoteRecordRecordTextRole, QuoteRecordEmbedImagesRole, QuoteRecordEmbedImagesFullRole, + QuoteRecordEmbedImagesAltRole, QuoteRecordIsRepostedRole, QuoteRecordIsLikedRole, diff --git a/app/qtquick/recordoperator.cpp b/app/qtquick/recordoperator.cpp index 4c82b311..1dd6fa6d 100644 --- a/app/qtquick/recordoperator.cpp +++ b/app/qtquick/recordoperator.cpp @@ -65,9 +65,16 @@ void RecordOperator::setQuote(const QString &cid, const QString &uri) m_embedQuote.uri = uri; } -void RecordOperator::setImages(const QStringList &images) +void RecordOperator::setImages(const QStringList &images, const QStringList &alts) { - m_embedImages = images; + for (int i = 0; i < images.length(); i++) { + EmbedImage e; + e.path = images.at(i); + if (i < alts.length()) { + e.alt = alts.at(i); + } + m_embedImages.append(e); + } } void RecordOperator::setPostLanguages(const QStringList &langs) @@ -82,7 +89,9 @@ void RecordOperator::setExternalLink(const QString &uri, const QString &title, m_externalLinkTitle = title; m_externalLinkDescription = description; m_embedImages.clear(); - m_embedImages.append(image_path); + EmbedImage e; + e.path = image_path; + m_embedImages.append(e); } void RecordOperator::setSelfLabels(const QStringList &labels) @@ -142,7 +151,8 @@ void RecordOperator::postWithImages() setRunning(true); - QString path = QUrl(m_embedImages.first()).toLocalFile(); + QString path = QUrl(m_embedImages.first().path).toLocalFile(); + QString alt = m_embedImages.first().alt; m_embedImages.removeFirst(); ComAtprotoRepoUploadBlob *upload_blob = new ComAtprotoRepoUploadBlob(this); @@ -155,6 +165,7 @@ void RecordOperator::postWithImages() blob.cid = upload_blob->cid(); blob.mimeType = upload_blob->mimeType(); blob.size = upload_blob->size(); + blob.alt = alt; m_embedImageBlogs.append(blob); if (m_embedImages.isEmpty()) { diff --git a/app/qtquick/recordoperator.h b/app/qtquick/recordoperator.h index d0ab01e6..690beac6 100644 --- a/app/qtquick/recordoperator.h +++ b/app/qtquick/recordoperator.h @@ -6,6 +6,12 @@ #include "atprotocol/accessatprotocol.h" #include +struct EmbedImage +{ + QString path; + QString alt; +}; + class RecordOperator : public QObject { Q_OBJECT @@ -22,7 +28,7 @@ class RecordOperator : public QObject Q_INVOKABLE void setReply(const QString &parent_cid, const QString &parent_uri, const QString &root_cid, const QString &root_uri); Q_INVOKABLE void setQuote(const QString &cid, const QString &uri); - Q_INVOKABLE void setImages(const QStringList &images); + Q_INVOKABLE void setImages(const QStringList &images, const QStringList &alts); Q_INVOKABLE void setPostLanguages(const QStringList &langs); Q_INVOKABLE void setExternalLink(const QString &uri, const QString &title, const QString &description, const QString &image_path); @@ -64,7 +70,7 @@ class RecordOperator : public QObject AtProtocolType::ComAtprotoRepoStrongRef::Main m_replyParent; AtProtocolType::ComAtprotoRepoStrongRef::Main m_replyRoot; AtProtocolType::ComAtprotoRepoStrongRef::Main m_embedQuote; - QStringList m_embedImages; + QList m_embedImages; QList m_embedImageBlogs; QList m_facets; QStringList m_postLanguages; diff --git a/app/qtquick/timelinelistmodel.cpp b/app/qtquick/timelinelistmodel.cpp index 6d4b319f..05df75c3 100644 --- a/app/qtquick/timelinelistmodel.cpp +++ b/app/qtquick/timelinelistmodel.cpp @@ -60,9 +60,14 @@ QVariant TimelineListModel::item(int row, TimelineListModelRoles role) const else if (role == IndexedAtLongRole) return formatDateTime(current.post.indexedAt, true); else if (role == EmbedImagesRole) - return LexiconsTypeUnknown::copyImagesFromPostView(current.post, true); + return LexiconsTypeUnknown::copyImagesFromPostView( + current.post, LexiconsTypeUnknown::CopyImageType::Thumb); else if (role == EmbedImagesFullRole) - return LexiconsTypeUnknown::copyImagesFromPostView(current.post, false); + return LexiconsTypeUnknown::copyImagesFromPostView( + current.post, LexiconsTypeUnknown::CopyImageType::FullSize); + else if (role == EmbedImagesAltRole) + return LexiconsTypeUnknown::copyImagesFromPostView(current.post, + LexiconsTypeUnknown::CopyImageType::Alt); else if (role == IsRepostedRole) return current.post.viewer.repost.contains(account().did); @@ -77,7 +82,8 @@ QVariant TimelineListModel::item(int row, TimelineListModelRoles role) const || role == QuoteRecordDisplayNameRole || role == QuoteRecordHandleRole || role == QuoteRecordAvatarRole || role == QuoteRecordRecordTextRole || role == QuoteRecordIndexedAtRole || role == QuoteRecordEmbedImagesRole - || role == QuoteRecordEmbedImagesFullRole || role == QuoteRecordBlockedRole) + || role == QuoteRecordEmbedImagesFullRole || role == QuoteRecordEmbedImagesAltRole + || role == QuoteRecordBlockedRole) return getQuoteItem(current.post, role); else if (role == HasExternalLinkRole) @@ -397,6 +403,7 @@ QHash TimelineListModel::roleNames() const roles[IndexedAtLongRole] = "indexedAtLong"; roles[EmbedImagesRole] = "embedImages"; roles[EmbedImagesFullRole] = "embedImagesFull"; + roles[EmbedImagesAltRole] = "embedImagesAlt"; roles[IsRepostedRole] = "isReposted"; roles[IsLikedRole] = "isLiked"; @@ -413,6 +420,7 @@ QHash TimelineListModel::roleNames() const roles[QuoteRecordIndexedAtRole] = "quoteRecordIndexedAt"; roles[QuoteRecordEmbedImagesRole] = "quoteRecordEmbedImages"; roles[QuoteRecordEmbedImagesFullRole] = "quoteRecordEmbedImagesFull"; + roles[QuoteRecordEmbedImagesAltRole] = "quoteRecordEmbedImagesAlt"; roles[QuoteRecordBlockedRole] = "quoteRecordBlocked"; roles[HasExternalLinkRole] = "hasExternalLink"; @@ -608,20 +616,35 @@ QVariant TimelineListModel::getQuoteItem(const AtProtocolType::AppBskyFeedDefs:: // unionの配列で読み込んでない if (has_record) return LexiconsTypeUnknown::copyImagesFromRecord( - post.embed_AppBskyEmbedRecord_View->record_ViewRecord, true); + post.embed_AppBskyEmbedRecord_View->record_ViewRecord, + LexiconsTypeUnknown::CopyImageType::Thumb); else if (has_with_image) return LexiconsTypeUnknown::copyImagesFromRecord( - post.embed_AppBskyEmbedRecordWithMedia_View.record->record_ViewRecord, true); + post.embed_AppBskyEmbedRecordWithMedia_View.record->record_ViewRecord, + LexiconsTypeUnknown::CopyImageType::Thumb); else return QStringList(); } else if (role == QuoteRecordEmbedImagesFullRole) { // unionの配列で読み込んでない if (has_record) return LexiconsTypeUnknown::copyImagesFromRecord( - post.embed_AppBskyEmbedRecord_View->record_ViewRecord, false); + post.embed_AppBskyEmbedRecord_View->record_ViewRecord, + LexiconsTypeUnknown::CopyImageType::FullSize); else if (has_with_image) return LexiconsTypeUnknown::copyImagesFromRecord( - post.embed_AppBskyEmbedRecordWithMedia_View.record->record_ViewRecord, false); + post.embed_AppBskyEmbedRecordWithMedia_View.record->record_ViewRecord, + LexiconsTypeUnknown::CopyImageType::FullSize); + else + return QStringList(); + } else if (role == QuoteRecordEmbedImagesAltRole) { + if (has_record) + return LexiconsTypeUnknown::copyImagesFromRecord( + post.embed_AppBskyEmbedRecord_View->record_ViewRecord, + LexiconsTypeUnknown::CopyImageType::Alt); + else if (has_with_image) + return LexiconsTypeUnknown::copyImagesFromRecord( + post.embed_AppBskyEmbedRecordWithMedia_View.record->record_ViewRecord, + LexiconsTypeUnknown::CopyImageType::Alt); else return QStringList(); } else if (role == QuoteRecordBlockedRole) { diff --git a/app/qtquick/timelinelistmodel.h b/app/qtquick/timelinelistmodel.h index 97c63a9f..44f31c67 100644 --- a/app/qtquick/timelinelistmodel.h +++ b/app/qtquick/timelinelistmodel.h @@ -35,6 +35,7 @@ class TimelineListModel : public AtpAbstractListModel IndexedAtLongRole, EmbedImagesRole, EmbedImagesFullRole, + EmbedImagesAltRole, IsRepostedRole, IsLikedRole, @@ -51,6 +52,7 @@ class TimelineListModel : public AtpAbstractListModel QuoteRecordIndexedAtRole, QuoteRecordEmbedImagesRole, QuoteRecordEmbedImagesFullRole, + QuoteRecordEmbedImagesAltRole, QuoteRecordBlockedRole, HasExternalLinkRole, diff --git a/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.cpp b/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.cpp index e27359f3..550e8668 100644 --- a/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.cpp +++ b/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.cpp @@ -87,7 +87,7 @@ void ComAtprotoRepoCreateRecord::post(const QString &text) json_image.insert("mimeType", blob.mimeType); json_image.insert("size", blob.size); json_blob.insert("image", json_image); - json_blob.insert("alt", ""); + json_blob.insert("alt", blob.alt); json_blobs.append(json_blob); } diff --git a/lib/atprotocol/lexicons_func_unknown.cpp b/lib/atprotocol/lexicons_func_unknown.cpp index 56ba07f3..52e50ad5 100644 --- a/lib/atprotocol/lexicons_func_unknown.cpp +++ b/lib/atprotocol/lexicons_func_unknown.cpp @@ -41,15 +41,17 @@ void copyUnknown(const QJsonObject &src, QVariant &dest) } } -QStringList copyImagesFromPostView(const AppBskyFeedDefs::PostView &post, const bool thumb) +QStringList copyImagesFromPostView(const AppBskyFeedDefs::PostView &post, const CopyImageType type) { if (post.embed_type == AppBskyFeedDefs::PostViewEmbedType::embed_AppBskyEmbedImages_View) { QStringList images; for (const auto &image : post.embed_AppBskyEmbedImages_View.images) { - if (thumb) + if (type == CopyImageType::Thumb) images.append(image.thumb); - else + else if (type == CopyImageType::FullSize) images.append(image.fullsize); + else if (type == CopyImageType::Alt) + images.append(image.alt); } return images; } else if (post.embed_type @@ -60,10 +62,12 @@ QStringList copyImagesFromPostView(const AppBskyFeedDefs::PostView &post, const QStringList images; for (const auto &image : post.embed_AppBskyEmbedRecordWithMedia_View.media_AppBskyEmbedImages_View.images) { - if (thumb) + if (type == CopyImageType::Thumb) images.append(image.thumb); - else + else if (type == CopyImageType::FullSize) images.append(image.fullsize); + else if (type == CopyImageType::Alt) + images.append(image.alt); } return images; } else { @@ -71,26 +75,31 @@ QStringList copyImagesFromPostView(const AppBskyFeedDefs::PostView &post, const } } -QStringList copyImagesFromRecord(const AppBskyEmbedRecord::ViewRecord &record, const bool thumb) +QStringList copyImagesFromRecord(const AppBskyEmbedRecord::ViewRecord &record, + const CopyImageType type) { // unionの配列で複数種類を混ぜられる QStringList images; for (const auto &view : record.embeds_AppBskyEmbedImages_View) { for (const auto &image : view.images) { - if (thumb) + if (type == CopyImageType::Thumb) images.append(image.thumb); - else + else if (type == CopyImageType::FullSize) images.append(image.fullsize); + else if (type == CopyImageType::Alt) + images.append(image.alt); } } for (const auto &view : record.embeds_AppBskyEmbedRecordWithMedia_View) { if (view.media_type == AppBskyEmbedRecordWithMedia::ViewMediaType::media_AppBskyEmbedImages_View) { for (const auto &image : view.media_AppBskyEmbedImages_View.images) { - if (thumb) + if (type == CopyImageType::Thumb) images.append(image.thumb); - else + else if (type == CopyImageType::FullSize) images.append(image.fullsize); + else if (type == CopyImageType::Alt) + images.append(image.alt); } } } diff --git a/lib/atprotocol/lexicons_func_unknown.h b/lib/atprotocol/lexicons_func_unknown.h index 9c82ff7f..6ce0749f 100644 --- a/lib/atprotocol/lexicons_func_unknown.h +++ b/lib/atprotocol/lexicons_func_unknown.h @@ -17,13 +17,21 @@ struct Blob { QString cid; QString mimeType; + QString alt; int size = 0; }; void copyUnknown(const QJsonObject &src, QVariant &dest); -QStringList copyImagesFromPostView(const AppBskyFeedDefs::PostView &post, const bool thumb); -QStringList copyImagesFromRecord(const AppBskyEmbedRecord::ViewRecord &record, const bool thumb); +enum class CopyImageType : int { + Thumb, + FullSize, + Alt, +}; + +QStringList copyImagesFromPostView(const AppBskyFeedDefs::PostView &post, const CopyImageType type); +QStringList copyImagesFromRecord(const AppBskyEmbedRecord::ViewRecord &record, + const CopyImageType type); template T fromQVariant(const QVariant &variant) From 77d2f35870e8e187d740e3900d8cf929a70a2993 Mon Sep 17 00:00:00 2001 From: Takayuki Orito Date: Mon, 4 Sep 2023 01:08:56 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=E3=82=BB=E3=83=83=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E3=81=AE=E6=9B=B4=E6=96=B0=E3=81=8C=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=81=AA=E3=81=8F=E3=81=AA=E3=81=A3=E3=81=A6=E3=81=84=E3=81=9F?= =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/qtquick/accountlistmodel.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/qtquick/accountlistmodel.cpp b/app/qtquick/accountlistmodel.cpp index 95b4a795..cb10281d 100644 --- a/app/qtquick/accountlistmodel.cpp +++ b/app/qtquick/accountlistmodel.cpp @@ -361,6 +361,7 @@ void AccountListModel::createSession(int row) qDebug() << "Create session" << session->did() << session->handle(); m_accountList[row].did = session->did(); m_accountList[row].handle = session->handle(); + m_accountList[row].email = session->email(); m_accountList[row].accessJwt = session->accessJwt(); m_accountList[row].refreshJwt = session->refreshJwt(); m_accountList[row].status = AccountStatus::Authorized; @@ -391,6 +392,7 @@ void AccountListModel::refreshSession(int row) qDebug() << "Refresh session" << session->did() << session->handle(); m_accountList[row].did = session->did(); m_accountList[row].handle = session->handle(); + m_accountList[row].email = session->email(); m_accountList[row].accessJwt = session->accessJwt(); m_accountList[row].refreshJwt = session->refreshJwt(); m_accountList[row].status = AccountStatus::Authorized; From ad01fe2623100946ae736cdc7ccb885c9321573f Mon Sep 17 00:00:00 2001 From: Takayuki Orito Date: Tue, 5 Sep 2023 02:01:38 +0900 Subject: [PATCH 05/10] =?UTF-8?q?OGP=E3=81=AE=E8=A7=A3=E6=9E=90=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tools/opengraphprotocol.cpp | 173 ++++++++++-------- lib/tools/opengraphprotocol.h | 1 + tests/atprotocol_test/atprotocol_test.qrc | 1 + tests/atprotocol_test/response/ogp/file6.html | 29 +++ tests/atprotocol_test/tst_atprotocol_test.cpp | 16 ++ 5 files changed, 142 insertions(+), 78 deletions(-) create mode 100644 tests/atprotocol_test/response/ogp/file6.html diff --git a/lib/tools/opengraphprotocol.cpp b/lib/tools/opengraphprotocol.cpp index c7c2aa49..854fd1d2 100644 --- a/lib/tools/opengraphprotocol.cpp +++ b/lib/tools/opengraphprotocol.cpp @@ -115,7 +115,7 @@ void OpenGraphProtocol::setThumb(const QString &newThumb) bool OpenGraphProtocol::parse(const QByteArray &data, const QString &src_uri) { bool ret = false; - QString charset = extractCharset(rebuildHtml(QString::fromUtf8(data))); + QString charset = extractCharset(QString::fromUtf8(data)); qDebug() << "charset" << charset; QTextStream ts(data); @@ -130,6 +130,9 @@ bool OpenGraphProtocol::parse(const QByteArray &data, const QString &src_uri) QDomDocument doc; if (!doc.setContent(rebuild_text, false, &errorMsg, &errorLine, &errorColumn)) { qDebug() << "parse" << errorMsg << ", Line=" << errorLine << ", Column=" << errorColumn; + qDebug().noquote().nospace() << "--- rebuild_text -----------------"; + qDebug().noquote().nospace() << rebuild_text; + qDebug().noquote().nospace() << "----------------------------------"; } else { QDomElement root = doc.documentElement(); QDomElement head = root.firstChildElement("head"); @@ -169,48 +172,52 @@ QString OpenGraphProtocol::extractCharset(const QString &data) const { QString charset = "utf-8"; + QRegularExpressionMatch match = m_rxMeta.match(data); + if (match.capturedTexts().isEmpty()) + return charset; + QString errorMsg; int errorLine; int errorColumn; - QDomDocument doc; - if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorColumn)) { - qDebug() << "extractCharset" << errorMsg << ", Line=" << errorLine - << ", Column=" << errorColumn; - qDebug().noquote().nospace() << "--------------------"; - qDebug().noquote().nospace() << data; - qDebug().noquote().nospace() << "--------------------"; - } else { - QDomElement root = doc.documentElement(); - QDomElement head = root.firstChildElement("head"); + QDomDocument doc; + QString result; + int pos; + while ((pos = match.capturedStart()) != -1) { + result = rebuildTag(match.captured()); - QDomElement element = head.firstChildElement("meta"); - while (!element.isNull()) { - if (element.attribute("http-equiv") == "content-type") { - QString content = element.attribute("content"); - QStringList items = content.split(";"); - bool exist = false; - for (const QString &item : qAsConst(items)) { - QStringList parts = item.trimmed().split("="); - if (parts.length() != 2) - continue; - if (parts[0] == "charset") { - charset = parts[1].trimmed(); - exist = true; + if (!doc.setContent(result, false, &errorMsg, &errorLine, &errorColumn)) { + qDebug() << "parse" << errorMsg << ", Line=" << errorLine << ", Column=" << errorColumn; + } else { + QDomElement element = doc.documentElement(); + if (element.tagName().toLower() == "meta") { + if (element.attribute("http-equiv") == "content-type") { + QString content = element.attribute("content"); + QStringList items = content.split(";"); + bool exist = false; + for (const QString &item : qAsConst(items)) { + QStringList parts = item.trimmed().split("="); + if (parts.length() != 2) + continue; + if (parts[0] == "charset") { + charset = parts[1].trimmed(); + exist = true; + break; + } + } + if (exist) + break; + } else { + QString temp = element.attribute("charset"); + if (!temp.isEmpty()) { + charset = temp; break; } } - if (exist) - break; - } else { - QString temp = element.attribute("charset"); - if (!temp.isEmpty()) { - charset = temp; - break; - } } - element = element.nextSiblingElement("meta"); } + + match = m_rxMeta.match(data, pos + match.capturedLength()); } return charset; @@ -222,66 +229,76 @@ QString OpenGraphProtocol::rebuildHtml(const QString &text) const if (match.capturedTexts().isEmpty()) return text; + QString errorMsg; + int errorLine; + int errorColumn; + QDomDocument doc; QString result; - QString temp; int pos; + while ((pos = match.capturedStart()) != -1) { + QString temp = rebuildTag(match.captured()); + if (doc.setContent(temp, false, &errorMsg, &errorLine, &errorColumn)) { + result += temp + "\n"; + } + match = m_rxMeta.match(text, pos + match.capturedLength()); + } + + return QString("%1").arg(result); +} + +QString OpenGraphProtocol::rebuildTag(QString text) const +{ QChar c; int state = 0; + bool in_quote = false; // 0:属性より前 // 1:属性名 // 2:スペース(=より後ろ) // 3:属性値 - bool in_quote = false; - while ((pos = match.capturedStart()) != -1) { - temp = match.captured(); - if (!temp.endsWith("/>") && !temp.toLower().startsWith("", "/>"); - } + QString result; - for (int i = 0; i < temp.length(); i++) { - c = temp.at(i); - if (state == 0) { - if (c == ' ') { + if (!text.endsWith("/>") && !text.toLower().startsWith("", "/>"); + } + + for (int i = 0; i < text.length(); i++) { + c = text.at(i); + if (state == 0) { + if (c == ' ') { + state = 1; + } + result += c; + } else if (state == 1) { + if (c == '=') { + state = 2; + in_quote = false; + } + result += c; + } else if (state == 2) { + if (c == '\"') { + in_quote = true; + state = 3; + } else if (c != ' ') { + result += '\"'; + in_quote = false; + state = 3; + } + result += c; + } else if (state == 3) { + if (in_quote) { + if (c == '\"') { state = 1; - } - result += c; - } else if (state == 1) { - if (c == '=') { - state = 2; in_quote = false; } - result += c; - } else if (state == 2) { - if (c == '\"') { - in_quote = true; - state = 3; - } else if (c != ' ') { + } else { + if (c == ' ') { result += '\"'; - in_quote = false; - state = 3; - } - result += c; - } else if (state == 3) { - if (in_quote) { - if (c == '\"') { - state = 1; - in_quote = false; - } - } else { - if (c == ' ') { - result += '\"'; - state = 1; - } + state = 1; } - result += c; - } else { } + result += c; + } else { } - - result += "\n"; - - match = m_rxMeta.match(text, pos + match.capturedLength()); } - - return QString("%1").arg(result); + return result; } diff --git a/lib/tools/opengraphprotocol.h b/lib/tools/opengraphprotocol.h index c51b2b95..9b19efa9 100644 --- a/lib/tools/opengraphprotocol.h +++ b/lib/tools/opengraphprotocol.h @@ -31,6 +31,7 @@ class OpenGraphProtocol : public QObject bool parse(const QByteArray &data, const QString &src_uri); QString extractCharset(const QString &data) const; QString rebuildHtml(const QString &text) const; + QString rebuildTag(QString text) const; QRegularExpression m_rxMeta; diff --git a/tests/atprotocol_test/atprotocol_test.qrc b/tests/atprotocol_test/atprotocol_test.qrc index 21ca5ac7..cacbc0fa 100644 --- a/tests/atprotocol_test/atprotocol_test.qrc +++ b/tests/atprotocol_test/atprotocol_test.qrc @@ -20,5 +20,6 @@ response/labels/save/2/xrpc/app.bsky.actor.getPreferences response/labels/save/3/xrpc/app.bsky.actor.getPreferences data/labels/save/3/app.bsky.actor.putPreferences + response/ogp/file6.html diff --git a/tests/atprotocol_test/response/ogp/file6.html b/tests/atprotocol_test/response/ogp/file6.html new file mode 100644 index 00000000..097544ec --- /dev/null +++ b/tests/atprotocol_test/response/ogp/file6.html @@ -0,0 +1,29 @@ + + + + + + + + + +^Cg^O + + + + + + + + + + + + + + + + +file1 + + \ No newline at end of file diff --git a/tests/atprotocol_test/tst_atprotocol_test.cpp b/tests/atprotocol_test/tst_atprotocol_test.cpp index 7a0148fc..27eb6aa0 100644 --- a/tests/atprotocol_test/tst_atprotocol_test.cpp +++ b/tests/atprotocol_test/tst_atprotocol_test.cpp @@ -292,6 +292,22 @@ void atprotocol_test::test_OpenGraphProtocol() ogp.description().toLocal8Bit()); QVERIFY(ogp.thumb() == ""); } + + { + QSignalSpy spy(&ogp, SIGNAL(finished(bool))); + ogp.getData(m_service + "/ogp/file6.html"); + spy.wait(); + QVERIFY(spy.count() == 1); + + QList arguments = spy.takeFirst(); + QVERIFY(arguments.at(0).toBool()); + + QVERIFY2(ogp.uri() == "http://localhost/response/ogp/file6.html", ogp.uri().toLocal8Bit()); + QVERIFY2(ogp.title() == QString("file6 TITLE"), ogp.title().toLocal8Bit()); + QVERIFY2(ogp.description() == QString("file6 ").append(QChar(0x8a73)).append(QChar(0x7d30)), + ogp.description().toLocal8Bit()); + QVERIFY(ogp.thumb() == "http://localhost:%1/response/ogp/images/file6.png"); + } } void atprotocol_test::test_getTimeline() From a63211996ea164e8a8f419c7e4e30b14d4dc15e0 Mon Sep 17 00:00:00 2001 From: Takayuki Orito Date: Wed, 6 Sep 2023 00:23:53 +0900 Subject: [PATCH 06/10] =?UTF-8?q?OGP=E3=81=AE=E8=A7=A3=E6=9E=90=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 396e858b9d5a8564019e564cfe90486dfb88c36d Mon Sep 17 00:00:00 2001 From: Takayuki Orito Date: Thu, 7 Sep 2023 22:35:29 +0900 Subject: [PATCH 07/10] =?UTF-8?q?=E3=83=95=E3=82=A3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=82=AB=E3=83=BC=E3=83=89=E3=81=AE=E6=8A=95=E7=A8=BF=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * フィードの取得機能追加 * リファクタ * ポストダイアログでカスタムフィードの情報を取得して表示 * カスタムフィードカードの送信 * 翻訳データ更新 --- app/app.pro | 1 + app/i18n/qt_ja_JP.qm | Bin 26334 -> 26689 bytes app/i18n/qt_ja_JP.ts | 64 ++++-- app/main.cpp | 3 + app/qml/dialogs/PostDialog.qml | 55 ++++- app/qml/main.qml | 2 +- app/qml/parts/FeedGeneratorLinkCard.qml | 59 ++++++ app/qml/parts/NotificationDelegate.qml | 20 +- app/qml/parts/PostDelegate.qml | 53 +---- app/qml/view/ColumnView.qml | 14 +- app/qml/view/NotificationListView.qml | 14 +- app/qml/view/PostThreadView.qml | 14 +- app/qml/view/ProfileView.qml | 10 +- app/qml/view/TimelineView.qml | 14 +- app/qtquick/feedgeneratorlink.cpp | 192 ++++++++++++++++++ app/qtquick/feedgeneratorlink.h | 73 +++++++ app/qtquick/notificationlistmodel.cpp | 24 +-- app/qtquick/notificationlistmodel.h | 12 +- app/qtquick/qtquick.pri | 2 + app/qtquick/recordoperator.cpp | 9 + app/qtquick/recordoperator.h | 3 + app/qtquick/timelinelistmodel.cpp | 24 +-- app/qtquick/timelinelistmodel.h | 12 +- .../bsky/feed/appbskyfeedgetfeedgenerator.cpp | 42 ++++ .../bsky/feed/appbskyfeedgetfeedgenerator.h | 26 +++ .../repo/comatprotorepocreaterecord.cpp | 14 ++ .../atproto/repo/comatprotorepocreaterecord.h | 3 + lib/lib.pri | 2 + tests/atprotocol_test/atprotocol_test.qrc | 1 + .../xrpc/app.bsky.feed.getFeedGenerator | 26 +++ tests/atprotocol_test/tst_atprotocol_test.cpp | 27 +++ tests/hagoromo_test/hagoromo_test.qrc | 1 + .../xrpc/app.bsky.feed.getFeedGenerator | 26 +++ tests/hagoromo_test/tst_hagoromo_test.cpp | 30 +++ 34 files changed, 718 insertions(+), 154 deletions(-) create mode 100644 app/qml/parts/FeedGeneratorLinkCard.qml create mode 100644 app/qtquick/feedgeneratorlink.cpp create mode 100644 app/qtquick/feedgeneratorlink.h create mode 100644 lib/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.cpp create mode 100644 lib/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.h create mode 100644 tests/atprotocol_test/response/xrpc/app.bsky.feed.getFeedGenerator create mode 100644 tests/hagoromo_test/response/generator/xrpc/app.bsky.feed.getFeedGenerator diff --git a/app/app.pro b/app/app.pro index cc75a832..4bcff4c1 100644 --- a/app/app.pro +++ b/app/app.pro @@ -37,6 +37,7 @@ QML_FILES = \ qml/parts/CoverFrame.qml \ qml/parts/ImagePreview.qml \ qml/parts/ExternalLinkCard.qml \ + qml/parts/FeedGeneratorLinkCard.qml \ qml/parts/NotificationDelegate.qml \ qml/parts/PostControls.qml \ qml/parts/PostDelegate.qml \ diff --git a/app/i18n/qt_ja_JP.qm b/app/i18n/qt_ja_JP.qm index 75c685b49befcecf9519c4c3484756899849c7d9..d7d055c78c87bd613b75aa2c6bdd326ceaf54c5e 100644 GIT binary patch delta 3511 zcmYM0d0bTG8pofRIp@rog&7INC4~_eFkHrkO>kHQMRtaLkwPFrP*hNnlu-dgM9gJH`= z3F1~167|&+w`UEJDe5pTjt~vk^BL1H%o{j z3W)n{7tvd;f3t_UC(cA-f8t)P!$!fxk8me)m`1$5FB~r=-gQ6T+e^G#9Fda?@fjnD z#G2#))ziLaRjd-E8rx%iakNoHnk8%T2i3PMr6^Kf>c!qWFKQxDh0J;Jvf*#s)s^~w-ed@ zKq05{!E6d$a~!GC<(n^vVmv9dyn<**6@_toiR_&zd_4>dyG;@IqY$|d8Pl#H;>Bbv zdyNz?CF6;uM4E+UeDwz$N~6dbxNnH2$d#*!-l?F-RiIM_MU}(BQNa|m{&!@=jbb)= zfpaM4fC&+X&7#=ZjYQ$cDef#%E+4|^J)GjsA;oed#TR4a@$)FY1&)pTj^dv{GZQ|g zgyk=gvHp}$^b!oBgfi6FwU83J@tn(KO1O`VCFm$A=UW6)Pf1%VQKGjfsRfys)JQW- zA|jtuNHb0&QjZEsPK_b*R8aC0WWv)xGf(;x^@*g#W(Nbh=%cc9qWJ5y>4g+2&!IhI zA;9577-O30NEXDUoJ>EyS4|YRh#nNQ5vgZM(ryevDON}_A94uPMN*vlCsF7WiK%27 zQZoFMWZS$VqQH+NO)ucNTbbmPJx?^+PI4#UAyiu|xzmVDMDtSdN&-sIN2;lXsQiXV zEw7A+2K=PslAaT3G6zlU zBB4{-+R%yzZKRjlQOf{#>7}nH68YbicAx!{Xg~*JWT%Ylo=-G#h|HzZfk@iTXl=zf zWfY@-fz0I)90|ykxq54eWT$1J?F(T~DT_6)xec=T^?pQCOh3sMzqxT%RN#3GTE zmVFLxk!|fkDjjdij?E}Vs&2{7G~h*{S=M_MJ#O&@=iqIC;Xk>M>$@-){^Ay!*W5eY zlC60}mKv`3swFaTk^8W13Q)lom^-JWcrhfrL zXBnlnjQm7K%V~^OGZ-gN{hLo2eF_+-6Jv;hF}#d1@ QJ{g&;=62u2KvEy$_Si(D z|D8LyJ>d}FpdQAkGOpqL9f-w-J512V$PHYl7MZX;!(Ezy0V3Ek`aWUw58y6cJdIK~ zGg@ury8rzp+IJUYOe)vi0{eC@jL~0lm(ShCRITTJsw_13zn}R6{cp|P{J<7{YQ^2F zI)^!Mp1WU;NoOzN?l+Bs7`u64Xg(ae#9Nm@BVr!kKWR0@`6b9Z+3@94p&_G+FE@`|`G<_&AMzF2 zP_$(MU*!+S#(VPBPT054d497=IR+aP@>@z_$m$wn_$_{0AWAUNg5Qz-0hrGiZ{Ta) z@Z!XK{61&&zrlgu?{NbIr;*=ZjY%j(@y+JQ+`i$PyD^~LIsU6F7;x@C2*MU<%(G6= z%vuZa77E&9)3ELlOpX^Y!945*x4?&(Z0|D09TVK|!m+8Zh3Q+q!C|wMF=nk0`J@e= zuM%dp;eRg`qxY}E2fT+Sv;Apg4Dx-hBaA3?v zJl|mw&ghrmcpNIUPDgD9-xk_xU|8=fT<9B$NE;aA-xvOP>WssuNqAZJJtiN?M<4kW zZMjqKx!^SdXq9is!Vx{NK)$haAsnxkZ_c*H#-rp7r?HW?htXIdZxk@Ff}hII$bZB> zrU&w_!w^$Y6l2&$`DHs-^zk$KO|CEc)l2@U4w1P0B7atd2(?v=MmPDhYuLaySM2lq zC-A3N93B;m8sA`y@D?3@uEwN`7RPQm0QJ|1u4-hiUxjEWZNyyIDkhBCfc_u9LrlmW zPUL-*F(E|Em=2GJ$1%oS7qcQ(W5gDUSxFbb-Qsd9q;!%6W5O=6sPrTZe=n|n?2Eyo z606Mr_sRbXTRdKK4YhX^Te>sx zVw2eV#({G2LY)DIbz;Yg2pH-TJFW&JwQ=Ir&FB_iXN6=O&J63{6-vkX7%WZ-=k4F) zV`{3x(>(ia@)cf(S`b(mW7xkG!AA>;rXN(q?dyRy<||T5TG9XGsui=tu+e~-isja* z-7qJ`itQMgL2DFcD{+6Im!eAF52v0=(Yg-Wn2@Dt^X$TXrJ}6^?Ph3Fw3{_EUd|Zz zi{f&bE#}3?il4I-IJDdq_da=z=YLZ?GJmSQove7-+6`7Hx!WdqKG8=xxhV~5KCH|L z$06ig#TZ|q%nUa_*sfe8{Rn;CtXx(khZgoJtK9k`!o$j~b=YX^Hs#TvFEJs9GDc@B zzma@`v%!+lI#l_?BZ%BZP+m44axU{2LOZC@}(Z?cHU_#A_$ z*&@{i849SiD00CD9!VB0t)8gqEzS5{h}`Lt#`WuPL^eqirpA4ry_)Pvdr|vjP42lU zh|Nv2{Gb5=rfQ1k*g*rIY1VmrAmdJ&k5}%%959t?>i6Zrz-vwYQ_wF>bF37i(LL0h zGk*a&+i9NP#wdN;Q7bw8oG5Cv*1mHBJinkVY_meCvb9^xT@Wm48+JROG^@0YrXCzp zag4DQ+Q!Y95N((CSZfKs1HRRMCzD~|ozvd32EAlO>_TD5m>8hFLufa*Q0>EChC)RC zjAA!qVqfiFW)XSjz-r>M%B||$O3h$fYt59Lxq0bx^K|cLX6L0ZfK17vG9|_?+8DSg zO~i@>{4~b&IXR2cbt$Q-ISc3Jy?M%qUHfIGWarG1z$a4cujzN{d-YjmqZ2p(?_E`7 zdY&#NS2rgmb8h1^b%vz!sI84?;gg-`pO%?dd8p4j|6i=P8{1f{sW$`;)8B=mgZh2? zm-;q+kiJX5FZ-WV6M HYQTR1J-yIJ delta 3295 zcmXYzd0bTG8pofRIq#WsW`-GMaR(_yaKk}XWmQmOk$o4l1leCzWp(u@0^))-uHQa&+mEObGlt|^Ri-_ zgJ~TR%^-?BOvHTg>E!gw=JdbFne;hP=*PGh9}BIA5Jg@ivY$m1eTe8~Migfva&{-0 zdxOYl3kk6*B26j@>o*bE>>(k4Ba!Ve66!V+#jgdA5t&9hlW_4Uk$8=SJ4Hk>fh64D zLo|5wUp!C3V^1PAk??#gHu4}gZW__JDa4F+2<{DH-t~BH88M#}q6rSfGRG6So+7pe zfd*Y6R=Wi2rToSF#P)6=GH71VL{XQB9n2;2Y9rQyh^85dna>bOCB!wKlOnX0L?g>N<6S7C71yIYIO9K}Ifc8498S`lGf>+h zfuc4y5shA8AtXywP*mA=BF8+67Ct3<$(~}iVSzF26ni%TnUANqj4ovSD#eu!K;jIF z`+7Z*)jW!O{s;k$r}%mJ-QS<$Hrh<_xr=lnanpoKC5Q*w`(FQkxN&@k>g546jTrrL?@KP>d+8 z;2G#nX{D&K*J4WRzz;s2pDGF${YErLQWS+kiLv_>AG}dO6qc+o9es+3 zy;msCSR6b_t>}5>9^ATA(bEV8;#-wcR~nJi6QzDH%rx_ka&XsFI9*n{rS}u*XDFBU z_CXO*SsZ~9*oG@Nw;@31e{d#|a;w{Ol%!r+PHWLytvkIkpEEIMRh5}8(JLX`_ z1|Ok#9Rjj_C=^@zeCQ;htQgmJ-wNA&@`;873zcDCq5l;!|00ay95jW~dKRaj>tF2R z3|hz;dX6(v#TgyV8Fxykyf_~U<_n+P!2r^JAk+;>B=Y!NsEdKuMxN)4j}{s(_K=B2 z4hTmGrf^v=v>Tv+{Q=>+I|c?5IcGF+hFA&LubxHb>o~192py+Rpk3ePOmq=Cnz4a{ z17|{^(0Sn}Osxvx-{n|$^d!!N8^WEp9H979;g`w_=zqsX;cgiwn`4h~_vj>;@Dvlr ztV2L2m~9cfp-yMR(o5jlAK7qe7|a;W94Eu9P9<#IW-L5z2WMgjdr=})D0`*FfWBYC zA~LRk)hyC75hq?@aU-sxlznV&14^#fgC=~5sbCBGapUA_mRWlU1EQB@=fmw`GRr>g zj>s$68Y42B@)~DSCo7u`^Tf2XGRue+mvaU#XWI=?Xvc-Dauxz}8_RZ0#Kwk3R;6*p z`ioh0G1jp*b4J&*4@}`bXrJHMM@!!#a$mriqGEe}@Syuuw%=2U$!1{nzPB-8s#*Om zOg`qvzO-cK{TchR0|UwXE<4$UiRN=s6sut_-yNcU$rfa~STr0D#r1j7=d#-1Yn>(TMxvhcoBnENBz!>|wTH&guvIkcnrj`bhyY)aP-=JXb&Y5gW|#k(?5eQQOO$F-&s)VHXC7zci)#AYA{B zlYR+Y zD&^TgN}u02Q?sRl;?r38E2*S+CfFxcTK*rpK&qXE0G#Zk+Fop+c_!_<1*QD6rG_C; z%FQetD>;VtOpuyte?-j(NzENuc&C(Oc^#2e^YRiwsLSIX5-I0*gUh1ww zn*{wKE8K8M*xZmcE^lI>*vOu{zr*jYvY%zv+bxg-4>uz-Z_cQ6Ir6iOM6axpQ}*A) zph=Y%6=B93^X2U506ZTq=h?2tX=o$o@5Y1-kC96^1)%?jJIj^Ep*X$T<(925#ngD& z>~{?h^vdQow3&a6+-fn4@jhqrCvs=01Lj1L{6mh6Lux?&<^2Jy^S%7Q@=7*hw*0K6 z1I*P3KOu1U@f!c58AJh9n#>pk<$1W&Z+CeFqBW8!os->!`~B)WA`Mkq0=Ru`AF1@C^Z>N4_hyNeCJ>)YSrOqgpGoB0Li&4(tdMP867G{>sI3mZ%iv1)GdBTA^(Pu+uzC!ElGUyeaW zf9jV`{}eT!tzUT|0rx-G=N$?_W)b?rzl}!iYxP@$d?B@s{-2vZLf_BPAK1SNik#IS z_#F%$uRmT4uZ+H~zhHUM@lfmgf5PM(VH#vm9O*~f^%`DocSpoW4I9lis8NEU+T#B3 z%Z7$e@ZvFOnW52i6Ngd|XHt@(u__B@{n~K6r3mN!dxmdSDzxDt!~bkSWe%shpEJ$M z&_n1lZ!g2W|BS)^lR1Z~Pw_9Q|1$hxF^_LDR!y5(-mF{M_{b_&QQqk|wz1i8Ai&wy r__MLkc*wZl_{?ZFE~7+>qe#gzBSn0yBD0p|I4*i^MPpOc+r$444v3PR diff --git a/app/i18n/qt_ja_JP.ts b/app/i18n/qt_ja_JP.ts index 674618b9..888d97d9 100644 --- a/app/i18n/qt_ja_JP.ts +++ b/app/i18n/qt_ja_JP.ts @@ -9,7 +9,22 @@ アカウント管理 - + + Set as main + メインに設定 + + + + Content filter + コンテンツフィルター + + + + Remove account + アカウントを削除 + + + Close 閉じる @@ -47,6 +62,19 @@ 追加 + + AltEditDialog + + + Cancel + キャンセル + + + + Add + 追加 + + ColumnListModel @@ -419,12 +447,12 @@ 検索 - + Cancel キャンセル - + Add 追加 @@ -1477,12 +1505,12 @@ PostDelegate - + Post from an account you muted. ミュートしているアカウントのポスト - + blocked ブロック済み @@ -1502,22 +1530,26 @@ リンクカード - Link card URL - リンクカードのURL + リンクカードのURL + + + + Link card URL or custom feed URL + リンクカードかフィードカードのURL - + Cancel キャンセル - + Post ポスト - + Select contents コンテンツの選択 @@ -1530,7 +1562,7 @@ ポストスレッド - + Quoted content warning 閲覧注意な引用 @@ -1538,22 +1570,22 @@ ProfileListView - + Following フォロー中 - + Follow フォローする - + Follows you あなたをフォロー中 - + Muted user ミュート中 @@ -2114,7 +2146,7 @@ TimelineView - + Quoted content warning 閲覧注意な引用 diff --git a/app/main.cpp b/app/main.cpp index 1fdcd752..39877255 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -28,6 +28,7 @@ #include "qtquick/systemtool.h" #include "qtquick/externallink.h" #include "qtquick/reporter.h" +#include "qtquick/feedgeneratorlink.h" int main(int argc, char *argv[]) { @@ -87,6 +88,8 @@ int main(int argc, char *argv[]) qmlRegisterType("tech.relog.hagoromo.systemtool", 1, 0, "SystemTool"); qmlRegisterType("tech.relog.hagoromo.externallink", 1, 0, "ExternalLink"); qmlRegisterType("tech.relog.hagoromo.reporter", 1, 0, "Reporter"); + qmlRegisterType("tech.relog.hagoromo.feedgeneratorlink", 1, 0, + "FeedGeneratorLink"); qmlRegisterSingletonType(QUrl("qrc:/Hagoromo/qml/data/AdjustedValues.qml"), "tech.relog.hagoromo.singleton", 1, 0, "AdjustedValues"); diff --git a/app/qml/dialogs/PostDialog.qml b/app/qml/dialogs/PostDialog.qml index 1ec0c31a..2a418e05 100644 --- a/app/qml/dialogs/PostDialog.qml +++ b/app/qml/dialogs/PostDialog.qml @@ -8,6 +8,7 @@ import tech.relog.hagoromo.recordoperator 1.0 import tech.relog.hagoromo.accountlistmodel 1.0 import tech.relog.hagoromo.languagelistmodel 1.0 import tech.relog.hagoromo.externallink 1.0 +import tech.relog.hagoromo.feedgeneratorlink 1.0 import tech.relog.hagoromo.systemtool 1.0 import tech.relog.hagoromo.singleton 1.0 @@ -69,6 +70,7 @@ Dialog { embedImagePreview.embedImages = [] embedImagePreview.embedAlts = [] externalLink.clear() + feedGeneratorLink.clear() addingExternalLinkUrlText.text = "" } @@ -103,6 +105,9 @@ Dialog { ExternalLink { id: externalLink } + FeedGeneratorLink { + id: feedGeneratorLink + } ColumnLayout { id: mainLayout @@ -227,22 +232,37 @@ Dialog { id: addingExternalLinkUrlText selectByMouse: true font.pointSize: AdjustedValues.f10 - placeholderText: qsTr("Link card URL") + placeholderText: qsTr("Link card URL or custom feed URL") } } IconButton { id: externalLinkButton iconSource: "../images/add.png" enabled: addingExternalLinkUrlText.text.length > 0 - onClicked: externalLink.getExternalLink(addingExternalLinkUrlText.text) + onClicked: { + var uri = addingExternalLinkUrlText.text + var at_uri = feedGeneratorLink.convertToAtUri(uri) + if(at_uri.length > 0){ + var row = accountCombo.currentIndex; + feedGeneratorLink.setAccount(postDialog.accountModel.item(row, AccountListModel.ServiceRole), + postDialog.accountModel.item(row, AccountListModel.DidRole), + postDialog.accountModel.item(row, AccountListModel.HandleRole), + postDialog.accountModel.item(row, AccountListModel.EmailRole), + postDialog.accountModel.item(row, AccountListModel.AccessJwtRole), + postDialog.accountModel.item(row, AccountListModel.RefreshJwtRole)) + feedGeneratorLink.getFeedGenerator(at_uri) + }else{ + externalLink.getExternalLink(uri) + } + } BusyIndicator { anchors.fill: parent anchors.margins: 3 - visible: externalLink.running + visible: externalLink.running || feedGeneratorLink.running } states: [ State { - when: externalLink.running || createRecord.running + when: externalLink.running || feedGeneratorLink.running || createRecord.running PropertyChanges { target: externalLinkButton enabled: false @@ -250,11 +270,14 @@ Dialog { } }, State { - when: externalLink.valid + when: externalLink.valid || feedGeneratorLink.valid PropertyChanges { target: externalLinkButton iconSource: "../images/delete.png" - onClicked: externalLink.clear() + onClicked: { + externalLink.clear() + feedGeneratorLink.clear() + } } } ] @@ -270,6 +293,15 @@ Dialog { titleLabel.text: externalLink.title descriptionLabel.text: externalLink.description } + FeedGeneratorLinkCard { + Layout.preferredWidth: 400 * AdjustedValues.ratio + visible: feedGeneratorLink.valid + + avatarImage.source: feedGeneratorLink.avatar + displayNameLabel.text: feedGeneratorLink.displayName + creatorHandleLabel.text: feedGeneratorLink.creatorHandle + likeCountLabel.text: feedGeneratorLink.likeCount + } RowLayout { visible: embedImagePreview.embedImages.length > 0 @@ -401,7 +433,7 @@ Dialog { } } IconButton { - enabled: !createRecord.running && !externalLink.valid + enabled: !createRecord.running && !externalLink.valid && !feedGeneratorLink.valid iconSource: "../images/add_image.png" iconSize: AdjustedValues.i16 flat: true @@ -431,7 +463,11 @@ Dialog { Button { id: postButton Layout.alignment: Qt.AlignRight - enabled: postText.text.length > 0 && postText.realTextLength <= 300 && !createRecord.running && !externalLink.running + enabled: postText.text.length > 0 && + postText.realTextLength <= 300 && + !createRecord.running && + !externalLink.running && + !feedGeneratorLink.running font.pointSize: AdjustedValues.f10 text: qsTr("Post") onClicked: { @@ -456,6 +492,9 @@ Dialog { if(externalLink.valid){ createRecord.setExternalLink(externalLink.uri, externalLink.title, externalLink.description, externalLink.thumbLocal) createRecord.postWithImages() + }else if(feedGeneratorLink.valid){ + createRecord.setFeedGeneratorLink(feedGeneratorLink.uri, feedGeneratorLink.cid) + createRecord.post() }else if(embedImagePreview.embedImages.length > 0){ createRecord.setImages(embedImagePreview.embedImages, embedImagePreview.embedAlts) createRecord.postWithImages() diff --git a/app/qml/main.qml b/app/qml/main.qml index ea1b1830..5a3f4a10 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -261,7 +261,7 @@ ApplicationWindow { scrollView.showRightMost() } onRequestViewImages: (index, paths, alts) => imageFullView.open(index, paths, alts) - onRequestViewGeneratorFeed: (account_uuid, name, uri) => { + onRequestViewFeedGenerator: (account_uuid, name, uri) => { columnManageModel.append(account.uuid, 4, false, 300000, 400, name, uri) scrollView.showRightMost() } diff --git a/app/qml/parts/FeedGeneratorLinkCard.qml b/app/qml/parts/FeedGeneratorLinkCard.qml new file mode 100644 index 00000000..54a67af4 --- /dev/null +++ b/app/qml/parts/FeedGeneratorLinkCard.qml @@ -0,0 +1,59 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls.Material 2.15 +import QtGraphicalEffects 1.15 + +import tech.relog.hagoromo.singleton 1.0 + +import "../controls" + +ClickableFrame { + property alias avatarImage: feedGeneratorAvatarImage + property alias displayNameLabel: feedGeneratorDisplayNameLabel + property alias creatorHandleLabel: feedGeneratorCreatorHandleLabel + property alias likeCountLabel: feedGeneratorLikeCountLabel + + ColumnLayout { + GridLayout { + columns: 2 + rowSpacing: 3 + AvatarImage { + id: feedGeneratorAvatarImage + Layout.preferredWidth: AdjustedValues.i24 + Layout.preferredHeight: AdjustedValues.i24 + Layout.rowSpan: 2 + altSource: "../images/account_icon.png" + } + Label { + id: feedGeneratorDisplayNameLabel + Layout.fillWidth: true + font.pointSize: AdjustedValues.f10 + } + Label { + id: feedGeneratorCreatorHandleLabel + color: Material.color(Material.Grey) + font.pointSize: AdjustedValues.f8 + } + } + RowLayout { + Layout.leftMargin: 3 + spacing: 3 + Image { + Layout.preferredWidth: AdjustedValues.i16 + Layout.preferredHeight: AdjustedValues.i16 + source: "../images/like.png" + layer.enabled: true + layer.effect: ColorOverlay { + color: Material.color(Material.Pink) + } + } + Label { + id: feedGeneratorLikeCountLabel + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + font.pointSize: AdjustedValues.f8 + } + } + } +} diff --git a/app/qml/parts/NotificationDelegate.qml b/app/qml/parts/NotificationDelegate.qml index b94030cc..d050dd8b 100644 --- a/app/qml/parts/NotificationDelegate.qml +++ b/app/qml/parts/NotificationDelegate.qml @@ -42,11 +42,11 @@ ClickableFrame { property alias postImagePreview: postImagePreview property alias quoteRecordFrame: quoteRecordFrame property alias quoteRecordImagePreview: quoteRecordImagePreview - // property alias generatorViewFrame: generatorFeedFrame - // property alias generatorAvatarImage: generatorFeedAvatarImage - // property alias generatorDisplayNameLabel: generatorFeedDisplayNameLabel - // property alias generatorCreatorHandleLabel: generatorFeedCreatorHandleLabel - // property alias generatorLikeCountLabel: generatorFeedLikeCountLabel + // property alias generatorViewFrame: feedGeneratorFrame + // property alias generatorAvatarImage: feedGeneratorAvatarImage + // property alias generatorDisplayNameLabel: feedGeneratorDisplayNameLabel + // property alias generatorCreatorHandleLabel: feedGeneratorCreatorHandleLabel + // property alias generatorLikeCountLabel: feedGeneratorLikeCountLabel property alias postControls: postControls signal requestViewProfile(string did) @@ -284,7 +284,7 @@ ClickableFrame { } // ClickableFrame { - // id: generatorFeedFrame + // id: feedGeneratorFrame // Layout.preferredWidth: parent.width // Layout.topMargin: 5 @@ -293,19 +293,19 @@ ClickableFrame { // columns: 2 // rowSpacing: 3 // AvatarImage { - // id: generatorFeedAvatarImage + // id: feedGeneratorAvatarImage // Layout.preferredWidth: 24 // Layout.preferredHeight: 24 // Layout.rowSpan: 2 // altSource: "../images/account_icon.png" // } // Label { - // id: generatorFeedDisplayNameLabel + // id: feedGeneratorDisplayNameLabel // Layout.fillWidth: true // font.pointSize: 10 // } // Label { - // id: generatorFeedCreatorHandleLabel + // id: feedGeneratorCreatorHandleLabel // color: Material.color(Material.Grey) // font.pointSize: 8 // } @@ -323,7 +323,7 @@ ClickableFrame { // } // } // Label { - // id: generatorFeedLikeCountLabel + // id: feedGeneratorLikeCountLabel // Layout.alignment: Qt.AlignVCenter // Layout.fillWidth: true // font.pointSize: 8 diff --git a/app/qml/parts/PostDelegate.qml b/app/qml/parts/PostDelegate.qml index 99b78a3a..afe6bc5e 100644 --- a/app/qml/parts/PostDelegate.qml +++ b/app/qml/parts/PostDelegate.qml @@ -39,11 +39,7 @@ ClickableFrame { property alias quoteRecordImagePreview: quoteRecordImagePreview property alias blockedQuoteFrame: blockedQuoteFrame property alias externalLinkFrame: externalLinkFrame - property alias generatorViewFrame: generatorFeedFrame - property alias generatorAvatarImage: generatorFeedAvatarImage - property alias generatorDisplayNameLabel: generatorFeedDisplayNameLabel - property alias generatorCreatorHandleLabel: generatorFeedCreatorHandleLabel - property alias generatorLikeCountLabel: generatorFeedLikeCountLabel + property alias feedGeneratorFrame: feedGeneratorFrame property alias postInformation: postInformation property alias postControls: postControls @@ -242,53 +238,10 @@ ClickableFrame { } } - ClickableFrame { - id: generatorFeedFrame + FeedGeneratorLinkCard { + id: feedGeneratorFrame Layout.preferredWidth: parent.width Layout.topMargin: 5 - - ColumnLayout { - GridLayout { - columns: 2 - rowSpacing: 3 - AvatarImage { - id: generatorFeedAvatarImage - Layout.preferredWidth: AdjustedValues.i24 - Layout.preferredHeight: AdjustedValues.i24 - Layout.rowSpan: 2 - altSource: "../images/account_icon.png" - } - Label { - id: generatorFeedDisplayNameLabel - Layout.fillWidth: true - font.pointSize: AdjustedValues.f10 - } - Label { - id: generatorFeedCreatorHandleLabel - color: Material.color(Material.Grey) - font.pointSize: AdjustedValues.f8 - } - } - RowLayout { - Layout.leftMargin: 3 - spacing: 3 - Image { - Layout.preferredWidth: AdjustedValues.i16 - Layout.preferredHeight: AdjustedValues.i16 - source: "../images/like.png" - layer.enabled: true - layer.effect: ColorOverlay { - color: Material.color(Material.Pink) - } - } - Label { - id: generatorFeedLikeCountLabel - Layout.alignment: Qt.AlignVCenter - Layout.fillWidth: true - font.pointSize: AdjustedValues.f8 - } - } - } } PostInformation { diff --git a/app/qml/view/ColumnView.qml b/app/qml/view/ColumnView.qml index 43f86b28..7b9dac54 100644 --- a/app/qml/view/ColumnView.qml +++ b/app/qml/view/ColumnView.qml @@ -38,7 +38,7 @@ ColumnLayout { signal requestMention(string account_uuid, string handle) signal requestViewAuthorFeed(string account_uuid, string did, string handle) signal requestViewImages(int index, var paths, var alts) - signal requestViewGeneratorFeed(string account_uuid, string name, string uri) + signal requestViewFeedGenerator(string account_uuid, string name, string uri) signal requestReportPost(string account_uuid, string uri, string cid) signal requestReportAccount(string account_uuid, string did) @@ -79,7 +79,7 @@ ColumnLayout { onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) - onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) + onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -134,7 +134,7 @@ ColumnLayout { } onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) - onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) + onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -165,7 +165,7 @@ ColumnLayout { onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) - onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) + onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onRequestReportAccount: (did) => columnView.requestReportAccount(account.uuid, did) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -203,7 +203,7 @@ ColumnLayout { onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) - onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) + onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -250,7 +250,7 @@ ColumnLayout { onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) - onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) + onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -282,7 +282,7 @@ ColumnLayout { onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) - onRequestViewGeneratorFeed: (name, uri) => columnView.requestViewGeneratorFeed(account.uuid, name, uri) + onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink diff --git a/app/qml/view/NotificationListView.qml b/app/qml/view/NotificationListView.qml index afa29f67..b3e1a9e6 100644 --- a/app/qml/view/NotificationListView.qml +++ b/app/qml/view/NotificationListView.qml @@ -28,7 +28,7 @@ ScrollView { signal requestViewThread(string uri) signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) - signal requestViewGeneratorFeed(string name, string uri) + signal requestViewFeedGenerator(string name, string uri) signal requestReportPost(string uri, string cid) ListView { @@ -109,12 +109,12 @@ ScrollView { quoteRecordImagePreview.embedAlts: model.quoteRecordEmbedImagesAlt quoteRecordImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.quoteRecordEmbedImagesFull, model.quoteRecordEmbedImagesAlt) -// generatorViewFrame.visible: model.hasGeneratorFeed -// generatorViewFrame.onClicked: notificationListView.requestViewGeneratorFeed(model.generatorFeedDisplayName, model.generatorFeedUri) -// generatorAvatarImage.source: model.generatorFeedAvatar -// generatorDisplayNameLabel.text: model.generatorFeedDisplayName -// generatorCreatorHandleLabel.text: model.generatorFeedCreatorHandle -// generatorLikeCountLabel.text: model.generatorFeedLikeCount +// generatorViewFrame.visible: model.hasFeedGenerator +// generatorViewFrame.onClicked: notificationListView.requestViewFeedGenerator(model.feedGeneratorDisplayName, model.feedGeneratorUri) +// generatorAvatarImage.source: model.feedGeneratorAvatar +// generatorDisplayNameLabel.text: model.feedGeneratorDisplayName +// generatorCreatorHandleLabel.text: model.feedGeneratorCreatorHandle +// generatorLikeCountLabel.text: model.feedGeneratorLikeCount postControls.replyButton.iconText: model.replyCount postControls.repostButton.iconText: model.repostCount diff --git a/app/qml/view/PostThreadView.qml b/app/qml/view/PostThreadView.qml index d22501ad..8d067287 100644 --- a/app/qml/view/PostThreadView.qml +++ b/app/qml/view/PostThreadView.qml @@ -27,7 +27,7 @@ ColumnLayout { signal requestViewThread(string uri) signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) - signal requestViewGeneratorFeed(string name, string uri) + signal requestViewFeedGenerator(string name, string uri) signal requestReportPost(string uri, string cid) signal back() @@ -164,12 +164,12 @@ ColumnLayout { externalLinkFrame.uriLabel.text: model.externalLinkUri externalLinkFrame.descriptionLabel.text: model.externalLinkDescription - generatorViewFrame.visible: model.hasGeneratorFeed - generatorViewFrame.onClicked: postThreadView.requestViewGeneratorFeed(model.generatorFeedDisplayName, model.generatorFeedUri) - generatorAvatarImage.source: model.generatorFeedAvatar - generatorDisplayNameLabel.text: model.generatorFeedDisplayName - generatorCreatorHandleLabel.text: model.generatorFeedCreatorHandle - generatorLikeCountLabel.text: model.generatorFeedLikeCount + feedGeneratorFrame.visible: model.hasFeedGenerator + feedGeneratorFrame.onClicked: postThreadView.requestViewFeedGenerator(model.feedGeneratorDisplayName, model.feedGeneratorUri) + feedGeneratorFrame.avatarImage.source: model.feedGeneratorAvatar + feedGeneratorFrame.displayNameLabel.text: model.feedGeneratorDisplayName + feedGeneratorFrame.creatorHandleLabel.text: model.feedGeneratorCreatorHandle + feedGeneratorFrame.likeCountLabel.text: model.feedGeneratorLikeCount postInformation.visible: (postThreadUri === model.uri) postInformation.labelsLayout.model: postInformation.visible ? model.labels : [] diff --git a/app/qml/view/ProfileView.qml b/app/qml/view/ProfileView.qml index 90367a87..502ea7cd 100644 --- a/app/qml/view/ProfileView.qml +++ b/app/qml/view/ProfileView.qml @@ -37,7 +37,7 @@ ColumnLayout { signal requestViewThread(string uri) signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) - signal requestViewGeneratorFeed(string name, string uri) + signal requestViewFeedGenerator(string name, string uri) signal requestViewAuthorFeed(string did, string handle) signal requestReportPost(string uri, string cid) signal requestReportAccount(string did) @@ -426,7 +426,7 @@ ColumnLayout { profileView.requestViewProfile(did) } } - onRequestViewGeneratorFeed: (name, uri) => profileView.requestViewGeneratorFeed(name, uri) + onRequestViewFeedGenerator: (name, uri) => profileView.requestViewFeedGenerator(name, uri) onRequestReportPost: (uri, cid) => profileView.requestReportPost(uri, cid) onHoveredLinkChanged: profileView.hoveredLink = hoveredLink } @@ -455,7 +455,7 @@ ColumnLayout { profileView.requestViewProfile(did) } } - onRequestViewGeneratorFeed: (name, uri) => profileView.requestViewGeneratorFeed(name, uri) + onRequestViewFeedGenerator: (name, uri) => profileView.requestViewFeedGenerator(name, uri) onRequestReportPost: (uri, cid) => profileView.requestReportPost(uri, cid) onHoveredLinkChanged: profileView.hoveredLink = hoveredLink } @@ -484,7 +484,7 @@ ColumnLayout { profileView.requestViewProfile(did) } } - onRequestViewGeneratorFeed: (name, uri) => profileView.requestViewGeneratorFeed(name, uri) + onRequestViewFeedGenerator: (name, uri) => profileView.requestViewFeedGenerator(name, uri) onRequestReportPost: (uri, cid) => profileView.requestReportPost(uri, cid) onHoveredLinkChanged: profileView.hoveredLink = hoveredLink } @@ -513,7 +513,7 @@ ColumnLayout { profileView.requestViewProfile(did) } } - onRequestViewGeneratorFeed: (name, uri) => profileView.requestViewGeneratorFeed(name, uri) + onRequestViewFeedGenerator: (name, uri) => profileView.requestViewFeedGenerator(name, uri) onRequestReportPost: (uri, cid) => profileView.requestReportPost(uri, cid) onHoveredLinkChanged: profileView.hoveredLink = hoveredLink } diff --git a/app/qml/view/TimelineView.qml b/app/qml/view/TimelineView.qml index 7d4434f2..50663f9c 100644 --- a/app/qml/view/TimelineView.qml +++ b/app/qml/view/TimelineView.qml @@ -29,7 +29,7 @@ ScrollView { signal requestViewThread(string uri) signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) - signal requestViewGeneratorFeed(string name, string uri) + signal requestViewFeedGenerator(string name, string uri) signal requestReportPost(string uri, string cid) @@ -135,12 +135,12 @@ ScrollView { externalLinkFrame.uriLabel.text: model.externalLinkUri externalLinkFrame.descriptionLabel.text: model.externalLinkDescription - generatorViewFrame.visible: model.hasGeneratorFeed - generatorViewFrame.onClicked: requestViewGeneratorFeed(model.generatorFeedDisplayName, model.generatorFeedUri) - generatorAvatarImage.source: model.generatorFeedAvatar - generatorDisplayNameLabel.text: model.generatorFeedDisplayName - generatorCreatorHandleLabel.text: model.generatorFeedCreatorHandle - generatorLikeCountLabel.text: model.generatorFeedLikeCount + feedGeneratorFrame.visible: model.hasFeedGenerator + feedGeneratorFrame.onClicked: requestViewFeedGenerator(model.feedGeneratorDisplayName, model.feedGeneratorUri) + feedGeneratorFrame.avatarImage.source: model.feedGeneratorAvatar + feedGeneratorFrame.displayNameLabel.text: model.feedGeneratorDisplayName + feedGeneratorFrame.creatorHandleLabel.text: model.feedGeneratorCreatorHandle + feedGeneratorFrame.likeCountLabel.text: model.feedGeneratorLikeCount postControls.replyButton.iconText: model.replyCount postControls.repostButton.iconText: model.repostCount diff --git a/app/qtquick/feedgeneratorlink.cpp b/app/qtquick/feedgeneratorlink.cpp new file mode 100644 index 00000000..6d8de421 --- /dev/null +++ b/app/qtquick/feedgeneratorlink.cpp @@ -0,0 +1,192 @@ +#include "feedgeneratorlink.h" +#include "atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.h" + +using AtProtocolInterface::AppBskyFeedGetFeedGenerator; + +FeedGeneratorLink::FeedGeneratorLink(QObject *parent) + : QObject { parent }, m_running(false), m_valid(false), m_likeCount(0) +{ +} + +void FeedGeneratorLink::setAccount(const QString &service, const QString &did, + const QString &handle, const QString &email, + const QString &accessJwt, const QString &refreshJwt) +{ + m_account.service = service; + m_account.did = did; + m_account.handle = handle; + m_account.email = email; + m_account.accessJwt = accessJwt; + m_account.refreshJwt = refreshJwt; +} + +bool FeedGeneratorLink::checkUri(const QString &uri) const +{ + // https://bsky.app/profile/did:plc:hoge/feed/aaaaaaaaaa + if (uri.isEmpty()) + return false; + if (!uri.startsWith("https://bsky.app/profile/")) + return false; + QStringList items = uri.split("/"); + if (items.length() != 7) + return false; + if (!items.at(4).startsWith("did:plc:")) + return false; + if (items.at(5) != "feed") + return false; + if (items.at(6).isEmpty()) + return false; + + return true; +} + +QString FeedGeneratorLink::convertToAtUri(const QString &uri) +{ + if (!checkUri(uri)) + return QString(); + + QStringList items = uri.split("/"); + + return QString("at://%1/app.bsky.feed.generator/%2").arg(items.at(4), items.at(6)); +} + +void FeedGeneratorLink::getFeedGenerator(const QString &uri) +{ + if (running()) + return; + setRunning(true); + + clear(); + + AppBskyFeedGetFeedGenerator *generator = new AppBskyFeedGetFeedGenerator(this); + connect(generator, &AppBskyFeedGetFeedGenerator::finished, [=](bool success) { + if (success) { + setAvatar(generator->generatorView().avatar); + setDisplayName(generator->generatorView().displayName); + setCreatorHandle(generator->generatorView().creator.handle); + setLikeCount(generator->generatorView().likeCount); + setUri(generator->generatorView().uri); + setCid(generator->generatorView().cid); + setValid(true); + } + setRunning(false); + generator->deleteLater(); + }); + generator->setAccount(m_account); + generator->getFeedGenerator(uri); +} + +void FeedGeneratorLink::clear() +{ + setValid(false); + setAvatar(QString()); + setDisplayName(QString()); + setCreatorHandle(QString()); + setLikeCount(0); + setUri(QString()); + setCid(QString()); +} + +bool FeedGeneratorLink::running() const +{ + return m_running; +} + +void FeedGeneratorLink::setRunning(bool newRunning) +{ + if (m_running == newRunning) + return; + m_running = newRunning; + emit runningChanged(); +} + +bool FeedGeneratorLink::valid() const +{ + return m_valid; +} + +void FeedGeneratorLink::setValid(bool newValid) +{ + if (m_valid == newValid) + return; + m_valid = newValid; + emit validChanged(); +} + +QString FeedGeneratorLink::avatar() const +{ + return m_avatar; +} + +void FeedGeneratorLink::setAvatar(const QString &newAvatar) +{ + if (m_avatar == newAvatar) + return; + m_avatar = newAvatar; + emit avatarChanged(); +} + +QString FeedGeneratorLink::displayName() const +{ + return m_displayName; +} + +void FeedGeneratorLink::setDisplayName(const QString &newDisplayName) +{ + if (m_displayName == newDisplayName) + return; + m_displayName = newDisplayName; + emit displayNameChanged(); +} + +QString FeedGeneratorLink::creatorHandle() const +{ + return m_creatorHandle; +} + +void FeedGeneratorLink::setCreatorHandle(const QString &newCreatorHandle) +{ + if (m_creatorHandle == newCreatorHandle) + return; + m_creatorHandle = newCreatorHandle; + emit creatorHandleChanged(); +} + +int FeedGeneratorLink::likeCount() const +{ + return m_likeCount; +} + +void FeedGeneratorLink::setLikeCount(int newLikeCount) +{ + if (m_likeCount == newLikeCount) + return; + m_likeCount = newLikeCount; + emit likeCountChanged(); +} + +QString FeedGeneratorLink::uri() const +{ + return m_uri; +} + +void FeedGeneratorLink::setUri(const QString &newUri) +{ + if (m_uri == newUri) + return; + m_uri = newUri; + emit uriChanged(); +} + +QString FeedGeneratorLink::cid() const +{ + return m_cid; +} + +void FeedGeneratorLink::setCid(const QString &newCid) +{ + if (m_cid == newCid) + return; + m_cid = newCid; + emit cidChanged(); +} diff --git a/app/qtquick/feedgeneratorlink.h b/app/qtquick/feedgeneratorlink.h new file mode 100644 index 00000000..41176af4 --- /dev/null +++ b/app/qtquick/feedgeneratorlink.h @@ -0,0 +1,73 @@ +#ifndef FEEDGENERATORLINK_H +#define FEEDGENERATORLINK_H + +#include "atprotocol/accessatprotocol.h" +#include + +class FeedGeneratorLink : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool running READ running WRITE setRunning NOTIFY runningChanged) + Q_PROPERTY(bool valid READ valid WRITE setValid NOTIFY validChanged) + + Q_PROPERTY(QString avatar READ avatar WRITE setAvatar NOTIFY avatarChanged) + Q_PROPERTY(QString displayName READ displayName WRITE setDisplayName NOTIFY displayNameChanged) + Q_PROPERTY(QString creatorHandle READ creatorHandle WRITE setCreatorHandle NOTIFY + creatorHandleChanged) + Q_PROPERTY(int likeCount READ likeCount WRITE setLikeCount NOTIFY likeCountChanged) + Q_PROPERTY(QString uri READ uri WRITE setUri NOTIFY uriChanged) + Q_PROPERTY(QString cid READ cid WRITE setCid NOTIFY cidChanged) +public: + explicit FeedGeneratorLink(QObject *parent = nullptr); + + Q_INVOKABLE void setAccount(const QString &service, const QString &did, const QString &handle, + const QString &email, const QString &accessJwt, + const QString &refreshJwt); + Q_INVOKABLE bool checkUri(const QString &uri) const; + Q_INVOKABLE QString convertToAtUri(const QString &uri); + Q_INVOKABLE void getFeedGenerator(const QString &uri); + Q_INVOKABLE void clear(); + + bool running() const; + void setRunning(bool newRunning); + bool valid() const; + void setValid(bool newValid); + QString avatar() const; + void setAvatar(const QString &newAvatar); + QString displayName() const; + void setDisplayName(const QString &newDisplayName); + QString creatorHandle() const; + void setCreatorHandle(const QString &newCreatorHandle); + int likeCount() const; + void setLikeCount(int newLikeCount); + QString uri() const; + void setUri(const QString &newUri); + QString cid() const; + void setCid(const QString &newCid); + +signals: + void runningChanged(); + void validChanged(); + void avatarChanged(); + void displayNameChanged(); + void creatorHandleChanged(); + void likeCountChanged(); + + void uriChanged(); + + void cidChanged(); + +private: + AtProtocolInterface::AccountData m_account; + bool m_running; + bool m_valid; + QString m_avatar; + QString m_displayName; + QString m_creatorHandle; + int m_likeCount; + QString m_uri; + QString m_cid; +}; + +#endif // FEEDGENERATORLINK_H diff --git a/app/qtquick/notificationlistmodel.cpp b/app/qtquick/notificationlistmodel.cpp index 5dd56b6e..b46923fd 100644 --- a/app/qtquick/notificationlistmodel.cpp +++ b/app/qtquick/notificationlistmodel.cpp @@ -261,7 +261,7 @@ QVariant NotificationListModel::item(int row, NotificationListModelRoles role) c else return false; - } else if (role == HasGeneratorFeedRole) { + } else if (role == HasFeedGeneratorRole) { if (m_postHash.contains(record_cid) && !m_postHash[record_cid].embed_AppBskyEmbedRecord_View.isNull()) { return m_postHash[record_cid].embed_type @@ -273,7 +273,7 @@ QVariant NotificationListModel::item(int row, NotificationListModelRoles role) c } else { return false; } - } else if (role == GeneratorFeedUriRole) { + } else if (role == FeedGeneratorUriRole) { if (m_postHash.contains(record_cid) && !m_postHash[record_cid].embed_AppBskyEmbedRecord_View.isNull()) { return m_postHash[record_cid] @@ -281,7 +281,7 @@ QVariant NotificationListModel::item(int row, NotificationListModelRoles role) c } else { return QString(); } - } else if (role == GeneratorFeedCreatorHandleRole) { + } else if (role == FeedGeneratorCreatorHandleRole) { if (m_postHash.contains(record_cid) && !m_postHash[record_cid].embed_AppBskyEmbedRecord_View.isNull()) { return m_postHash[record_cid] @@ -290,7 +290,7 @@ QVariant NotificationListModel::item(int row, NotificationListModelRoles role) c } else { return QString(); } - } else if (role == GeneratorFeedDisplayNameRole) { + } else if (role == FeedGeneratorDisplayNameRole) { if (m_postHash.contains(record_cid) && !m_postHash[record_cid].embed_AppBskyEmbedRecord_View.isNull()) { return m_postHash[record_cid] @@ -299,7 +299,7 @@ QVariant NotificationListModel::item(int row, NotificationListModelRoles role) c } else { return QString(); } - } else if (role == GeneratorFeedLikeCountRole) { + } else if (role == FeedGeneratorLikeCountRole) { if (m_postHash.contains(record_cid) && !m_postHash[record_cid].embed_AppBskyEmbedRecord_View.isNull()) { return m_postHash[record_cid] @@ -308,7 +308,7 @@ QVariant NotificationListModel::item(int row, NotificationListModelRoles role) c } else { return QString(); } - } else if (role == GeneratorFeedAvatarRole) { + } else if (role == FeedGeneratorAvatarRole) { if (m_postHash.contains(record_cid) && !m_postHash[record_cid].embed_AppBskyEmbedRecord_View.isNull()) { return m_postHash[record_cid] @@ -679,12 +679,12 @@ QHash NotificationListModel::roleNames() const roles[QuoteRecordIsRepostedRole] = "quoteRecordIsReposted"; roles[QuoteRecordIsLikedRole] = "quoteRecordIsLiked"; - roles[HasGeneratorFeedRole] = "hasGeneratorFeed"; - roles[GeneratorFeedUriRole] = "generatorFeedUri"; - roles[GeneratorFeedCreatorHandleRole] = "generatorFeedCreatorHandle"; - roles[GeneratorFeedDisplayNameRole] = "generatorFeedDisplayName"; - roles[GeneratorFeedLikeCountRole] = "generatorFeedLikeCount"; - roles[GeneratorFeedAvatarRole] = "generatorFeedAvatar"; + roles[HasFeedGeneratorRole] = "hasFeedGenerator"; + roles[FeedGeneratorUriRole] = "feedGeneratorUri"; + roles[FeedGeneratorCreatorHandleRole] = "feedGeneratorCreatorHandle"; + roles[FeedGeneratorDisplayNameRole] = "feedGeneratorDisplayName"; + roles[FeedGeneratorLikeCountRole] = "feedGeneratorLikeCount"; + roles[FeedGeneratorAvatarRole] = "feedGeneratorAvatar"; roles[UserFilterMatchedRole] = "userFilterMatched"; roles[UserFilterMessageRole] = "userFilterMessage"; diff --git a/app/qtquick/notificationlistmodel.h b/app/qtquick/notificationlistmodel.h index 637d8aab..9ded1490 100644 --- a/app/qtquick/notificationlistmodel.h +++ b/app/qtquick/notificationlistmodel.h @@ -65,12 +65,12 @@ class NotificationListModel : public AtpAbstractListModel QuoteRecordIsRepostedRole, QuoteRecordIsLikedRole, - HasGeneratorFeedRole, - GeneratorFeedUriRole, - GeneratorFeedCreatorHandleRole, - GeneratorFeedDisplayNameRole, - GeneratorFeedLikeCountRole, - GeneratorFeedAvatarRole, + HasFeedGeneratorRole, + FeedGeneratorUriRole, + FeedGeneratorCreatorHandleRole, + FeedGeneratorDisplayNameRole, + FeedGeneratorLikeCountRole, + FeedGeneratorAvatarRole, UserFilterMatchedRole, UserFilterMessageRole, diff --git a/app/qtquick/qtquick.pri b/app/qtquick/qtquick.pri index 93d7ed32..121beb2b 100644 --- a/app/qtquick/qtquick.pri +++ b/app/qtquick/qtquick.pri @@ -12,6 +12,7 @@ SOURCES += \ $$PWD/customfeedlistmodel.cpp \ $$PWD/encryption.cpp \ $$PWD/externallink.cpp \ + $$PWD/feedgeneratorlink.cpp \ $$PWD/feedgeneratorlistmodel.cpp \ $$PWD/feedtypelistmodel.cpp \ $$PWD/followerslistmodel.cpp \ @@ -41,6 +42,7 @@ HEADERS += \ $$PWD/encryption.h \ $$PWD/encryption_seed.h \ $$PWD/externallink.h \ + $$PWD/feedgeneratorlink.h \ $$PWD/feedgeneratorlistmodel.h \ $$PWD/feedtypelistmodel.h \ $$PWD/followerslistmodel.h \ diff --git a/app/qtquick/recordoperator.cpp b/app/qtquick/recordoperator.cpp index 1dd6fa6d..62f04c3e 100644 --- a/app/qtquick/recordoperator.cpp +++ b/app/qtquick/recordoperator.cpp @@ -94,6 +94,12 @@ void RecordOperator::setExternalLink(const QString &uri, const QString &title, m_embedImages.append(e); } +void RecordOperator::setFeedGeneratorLink(const QString &uri, const QString &cid) +{ + m_feedGeneratorLinkUri = uri; + m_feedGeneratorLinkCid = cid; +} + void RecordOperator::setSelfLabels(const QStringList &labels) { m_selfLabels = labels; @@ -111,6 +117,8 @@ void RecordOperator::clear() m_externalLinkUri.clear(); m_externalLinkTitle.clear(); m_externalLinkDescription.clear(); + m_feedGeneratorLinkUri.clear(); + m_feedGeneratorLinkCid.clear(); } void RecordOperator::post() @@ -139,6 +147,7 @@ void RecordOperator::post() create_record->setPostLanguages(m_postLanguages); create_record->setExternalLink(m_externalLinkUri, m_externalLinkTitle, m_externalLinkDescription); + create_record->setFeedGeneratorLink(m_feedGeneratorLinkUri, m_feedGeneratorLinkCid); create_record->setSelfLabels(m_selfLabels); create_record->post(m_text); }); diff --git a/app/qtquick/recordoperator.h b/app/qtquick/recordoperator.h index 690beac6..75102e90 100644 --- a/app/qtquick/recordoperator.h +++ b/app/qtquick/recordoperator.h @@ -32,6 +32,7 @@ class RecordOperator : public QObject Q_INVOKABLE void setPostLanguages(const QStringList &langs); Q_INVOKABLE void setExternalLink(const QString &uri, const QString &title, const QString &description, const QString &image_path); + Q_INVOKABLE void setFeedGeneratorLink(const QString &uri, const QString &cid); Q_INVOKABLE void setSelfLabels(const QStringList &labels); Q_INVOKABLE void clear(); @@ -77,6 +78,8 @@ class RecordOperator : public QObject QString m_externalLinkUri; QString m_externalLinkTitle; QString m_externalLinkDescription; + QString m_feedGeneratorLinkUri; + QString m_feedGeneratorLinkCid; QStringList m_selfLabels; bool m_running; diff --git a/app/qtquick/timelinelistmodel.cpp b/app/qtquick/timelinelistmodel.cpp index 05df75c3..6cb9e61b 100644 --- a/app/qtquick/timelinelistmodel.cpp +++ b/app/qtquick/timelinelistmodel.cpp @@ -98,7 +98,7 @@ QVariant TimelineListModel::item(int row, TimelineListModelRoles role) const else if (role == ExternalLinkThumbRole) return current.post.embed_AppBskyEmbedExternal_View.external.thumb; - else if (role == HasGeneratorFeedRole) { + else if (role == HasFeedGeneratorRole) { if (current.post.embed_AppBskyEmbedRecord_View.isNull()) return false; else @@ -106,31 +106,31 @@ QVariant TimelineListModel::item(int row, TimelineListModelRoles role) const == AppBskyFeedDefs::PostViewEmbedType::embed_AppBskyEmbedRecord_View && current.post.embed_AppBskyEmbedRecord_View->record_type == AppBskyEmbedRecord::ViewRecordType::record_AppBskyFeedDefs_GeneratorView; - } else if (role == GeneratorFeedUriRole) { + } else if (role == FeedGeneratorUriRole) { if (current.post.embed_AppBskyEmbedRecord_View.isNull()) return QString(); else return current.post.embed_AppBskyEmbedRecord_View->record_AppBskyFeedDefs_GeneratorView .uri; - } else if (role == GeneratorFeedCreatorHandleRole) { + } else if (role == FeedGeneratorCreatorHandleRole) { if (current.post.embed_AppBskyEmbedRecord_View.isNull()) return QString(); else return current.post.embed_AppBskyEmbedRecord_View->record_AppBskyFeedDefs_GeneratorView .creator.handle; - } else if (role == GeneratorFeedDisplayNameRole) { + } else if (role == FeedGeneratorDisplayNameRole) { if (current.post.embed_AppBskyEmbedRecord_View.isNull()) return QString(); else return current.post.embed_AppBskyEmbedRecord_View->record_AppBskyFeedDefs_GeneratorView .displayName; - } else if (role == GeneratorFeedLikeCountRole) { + } else if (role == FeedGeneratorLikeCountRole) { if (current.post.embed_AppBskyEmbedRecord_View.isNull()) return QString(); else return current.post.embed_AppBskyEmbedRecord_View->record_AppBskyFeedDefs_GeneratorView .likeCount; - } else if (role == GeneratorFeedAvatarRole) { + } else if (role == FeedGeneratorAvatarRole) { if (current.post.embed_AppBskyEmbedRecord_View.isNull()) return QString(); else @@ -429,12 +429,12 @@ QHash TimelineListModel::roleNames() const roles[ExternalLinkDescriptionRole] = "externalLinkDescription"; roles[ExternalLinkThumbRole] = "externalLinkThumb"; - roles[HasGeneratorFeedRole] = "hasGeneratorFeed"; - roles[GeneratorFeedUriRole] = "generatorFeedUri"; - roles[GeneratorFeedCreatorHandleRole] = "generatorFeedCreatorHandle"; - roles[GeneratorFeedDisplayNameRole] = "generatorFeedDisplayName"; - roles[GeneratorFeedLikeCountRole] = "generatorFeedLikeCount"; - roles[GeneratorFeedAvatarRole] = "generatorFeedAvatar"; + roles[HasFeedGeneratorRole] = "hasFeedGenerator"; + roles[FeedGeneratorUriRole] = "feedGeneratorUri"; + roles[FeedGeneratorCreatorHandleRole] = "feedGeneratorCreatorHandle"; + roles[FeedGeneratorDisplayNameRole] = "feedGeneratorDisplayName"; + roles[FeedGeneratorLikeCountRole] = "feedGeneratorLikeCount"; + roles[FeedGeneratorAvatarRole] = "feedGeneratorAvatar"; roles[HasReplyRole] = "hasReply"; roles[ReplyRootCidRole] = "replyRootCid"; diff --git a/app/qtquick/timelinelistmodel.h b/app/qtquick/timelinelistmodel.h index 44f31c67..dfc11ae5 100644 --- a/app/qtquick/timelinelistmodel.h +++ b/app/qtquick/timelinelistmodel.h @@ -61,12 +61,12 @@ class TimelineListModel : public AtpAbstractListModel ExternalLinkDescriptionRole, ExternalLinkThumbRole, - HasGeneratorFeedRole, - GeneratorFeedUriRole, - GeneratorFeedCreatorHandleRole, - GeneratorFeedDisplayNameRole, - GeneratorFeedLikeCountRole, - GeneratorFeedAvatarRole, + HasFeedGeneratorRole, + FeedGeneratorUriRole, + FeedGeneratorCreatorHandleRole, + FeedGeneratorDisplayNameRole, + FeedGeneratorLikeCountRole, + FeedGeneratorAvatarRole, HasReplyRole, ReplyRootCidRole, diff --git a/lib/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.cpp b/lib/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.cpp new file mode 100644 index 00000000..c77e4271 --- /dev/null +++ b/lib/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.cpp @@ -0,0 +1,42 @@ +#include "appbskyfeedgetfeedgenerator.h" +#include "atprotocol/lexicons_func.h" + +#include +#include +#include + +namespace AtProtocolInterface { + +AppBskyFeedGetFeedGenerator::AppBskyFeedGetFeedGenerator(QObject *parent) + : AccessAtProtocol { parent } +{ +} + +void AppBskyFeedGetFeedGenerator::getFeedGenerator(const QString &feed) +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("feed"), feed); // at:uri + + get(QStringLiteral("xrpc/app.bsky.feed.getFeedGenerator"), query); +} + +const AtProtocolType::AppBskyFeedDefs::GeneratorView & +AppBskyFeedGetFeedGenerator::generatorView() const +{ + return m_generatorView; +} + +void AppBskyFeedGetFeedGenerator::parseJson(bool success, const QString reply_json) +{ + QJsonDocument json_doc = QJsonDocument::fromJson(reply_json.toUtf8()); + if (json_doc.isEmpty()) { + success = false; + } else { + AtProtocolType::AppBskyFeedDefs::copyGeneratorView( + json_doc.object().value("view").toObject(), m_generatorView); + } + + emit finished(success); +} + +} diff --git a/lib/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.h b/lib/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.h new file mode 100644 index 00000000..327be215 --- /dev/null +++ b/lib/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.h @@ -0,0 +1,26 @@ +#ifndef APPBSKYFEEDGETFEEDGENERATOR_H +#define APPBSKYFEEDGETFEEDGENERATOR_H + +#include "atprotocol/accessatprotocol.h" +#include "atprotocol/lexicons.h" + +namespace AtProtocolInterface { + +class AppBskyFeedGetFeedGenerator : public AccessAtProtocol +{ +public: + explicit AppBskyFeedGetFeedGenerator(QObject *parent = nullptr); + + void getFeedGenerator(const QString &feed); + + const AtProtocolType::AppBskyFeedDefs::GeneratorView &generatorView() const; + +private: + virtual void parseJson(bool success, const QString reply_json); + + AtProtocolType::AppBskyFeedDefs::GeneratorView m_generatorView; +}; + +} + +#endif // APPBSKYFEEDGETFEEDGENERATOR_H diff --git a/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.cpp b/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.cpp index 550e8668..48fa21e7 100644 --- a/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.cpp +++ b/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.cpp @@ -75,6 +75,14 @@ void ComAtprotoRepoCreateRecord::post(const QString &text) json_embed_images.insert("$type", "app.bsky.embed.external"); json_embed_images.insert("external", json_external); + } else if (!m_feedGeneratorLinkUri.isEmpty()) { + // カスタムフィードカード + QJsonObject json_generator; + json_generator.insert("uri", m_feedGeneratorLinkUri); + json_generator.insert("cid", m_feedGeneratorLinkCid); + json_embed_images.insert("$type", "app.bsky.embed.record"); + json_embed_images.insert("record", json_generator); + } else if (!m_embedImageBlobs.isEmpty()) { QJsonArray json_blobs; for (const auto &blob : qAsConst(m_embedImageBlobs)) { @@ -297,6 +305,12 @@ void ComAtprotoRepoCreateRecord::setExternalLink(const QString &uri, const QStri m_externalLinkDescription = description; } +void ComAtprotoRepoCreateRecord::setFeedGeneratorLink(const QString &uri, const QString &cid) +{ + m_feedGeneratorLinkUri = uri; + m_feedGeneratorLinkCid = cid; +} + void ComAtprotoRepoCreateRecord::setSelfLabels(const QStringList &labels) { m_selfLabels = labels; diff --git a/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.h b/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.h index aa292d50..df157f5a 100644 --- a/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.h +++ b/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.h @@ -25,6 +25,7 @@ class ComAtprotoRepoCreateRecord : public AccessAtProtocol void setFacets(const QList &newFacets); void setPostLanguages(const QStringList &newPostLanguages); void setExternalLink(const QString &uri, const QString &title, const QString &description); + void setFeedGeneratorLink(const QString &uri, const QString &cid); void setSelfLabels(const QStringList &labels); QString replyCid() const; @@ -42,6 +43,8 @@ class ComAtprotoRepoCreateRecord : public AccessAtProtocol QString m_externalLinkUri; QString m_externalLinkTitle; QString m_externalLinkDescription; + QString m_feedGeneratorLinkUri; + QString m_feedGeneratorLinkCid; QStringList m_selfLabels; QString m_replyCid; diff --git a/lib/lib.pri b/lib/lib.pri index 78a578d4..45adbd91 100644 --- a/lib/lib.pri +++ b/lib/lib.pri @@ -10,6 +10,7 @@ SOURCES += \ $$PWD/atprotocol/app/bsky/actor/appbskyactorputpreferences.cpp \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetauthorfeed.cpp \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeed.cpp \ + $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.cpp \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerators.cpp \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetposts.cpp \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetpostthread.cpp \ @@ -44,6 +45,7 @@ HEADERS += \ $$PWD/atprotocol/app/bsky/actor/appbskyactorputpreferences.h \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetauthorfeed.h \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeed.h \ + $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.h \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerators.h \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetposts.h \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetpostthread.h \ diff --git a/tests/atprotocol_test/atprotocol_test.qrc b/tests/atprotocol_test/atprotocol_test.qrc index cacbc0fa..b47a031a 100644 --- a/tests/atprotocol_test/atprotocol_test.qrc +++ b/tests/atprotocol_test/atprotocol_test.qrc @@ -21,5 +21,6 @@ response/labels/save/3/xrpc/app.bsky.actor.getPreferences data/labels/save/3/app.bsky.actor.putPreferences response/ogp/file6.html + response/xrpc/app.bsky.feed.getFeedGenerator diff --git a/tests/atprotocol_test/response/xrpc/app.bsky.feed.getFeedGenerator b/tests/atprotocol_test/response/xrpc/app.bsky.feed.getFeedGenerator new file mode 100644 index 00000000..efd64b5c --- /dev/null +++ b/tests/atprotocol_test/response/xrpc/app.bsky.feed.getFeedGenerator @@ -0,0 +1,26 @@ +{ + "view": { + "uri": "at://did:plc:42fxwa2jeumqzzggx/app.bsky.feed.generator/aaagrsa", + "cid": "bafyreib7pgajpklwexy4lidm", + "did": "did:web:view.bsky.social", + "creator": { + "did": "did:plc:42fxwa2jeumqzzggxj", + "handle": "creator.bsky.social", + "displayName": "creator:displayName", + "avatar": "https://cdn.bsky.social/creator_avator.jpeg", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "displayName": "view:displayName", + "description": "view:description", + "avatar": "https://cdn.bsky.social/view_avator.jpeg", + "likeCount": 9, + "viewer": {}, + "indexedAt": "2023-07-27T14:40:06.637Z" + }, + "isOnline": true, + "isValid": true +} diff --git a/tests/atprotocol_test/tst_atprotocol_test.cpp b/tests/atprotocol_test/tst_atprotocol_test.cpp index 27eb6aa0..9d040719 100644 --- a/tests/atprotocol_test/tst_atprotocol_test.cpp +++ b/tests/atprotocol_test/tst_atprotocol_test.cpp @@ -6,6 +6,7 @@ #include "atprotocol/com/atproto/server/comatprotoservercreatesession.h" #include "atprotocol/com/atproto/repo/comatprotorepocreaterecord.h" #include "atprotocol/app/bsky/feed/appbskyfeedgettimeline.h" +#include "atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.h" #include "tools/opengraphprotocol.h" #include "atprotocol/lexicons_func_unknown.h" #include "tools/configurablelabels.h" @@ -32,6 +33,7 @@ private slots: void test_ConfigurableLabels_copy(); void test_ConfigurableLabels_save(); void test_ComAtprotoRepoCreateRecord_post(); + void test_AppBskyFeedGetFeedGenerator(); private: void test_putPreferences(const QString &path, const QByteArray &body); @@ -711,6 +713,31 @@ void atprotocol_test::test_ComAtprotoRepoCreateRecord_post() QVERIFY(arguments.at(0).toBool()); } +void atprotocol_test::test_AppBskyFeedGetFeedGenerator() +{ + AtProtocolInterface::AppBskyFeedGetFeedGenerator generator; + generator.setAccount(m_account); + generator.setService(QString("http://localhost:%1/response").arg(m_listenPort)); + + QSignalSpy spy(&generator, SIGNAL(finished(bool))); + generator.getFeedGenerator("at://did:plc:42fxwa2jeumqzzggx/app.bsky.feed.generator/aaagrsa"); + spy.wait(); + QVERIFY(spy.count() == 1); + + QVERIFY(generator.generatorView().uri + == "at://did:plc:42fxwa2jeumqzzggx/app.bsky.feed.generator/aaagrsa"); + QVERIFY(generator.generatorView().cid == "bafyreib7pgajpklwexy4lidm"); + QVERIFY(generator.generatorView().did == "did:web:view.bsky.social"); + QVERIFY(generator.generatorView().displayName == "view:displayName"); + QVERIFY(generator.generatorView().description == "view:description"); + QVERIFY(generator.generatorView().avatar == "https://cdn.bsky.social/view_avator.jpeg"); + QVERIFY(generator.generatorView().creator.did == "did:plc:42fxwa2jeumqzzggxj"); + QVERIFY(generator.generatorView().creator.handle == "creator.bsky.social"); + QVERIFY(generator.generatorView().creator.displayName == "creator:displayName"); + QVERIFY(generator.generatorView().creator.avatar + == "https://cdn.bsky.social/creator_avator.jpeg"); +} + void atprotocol_test::test_putPreferences(const QString &path, const QByteArray &body) { QJsonDocument json_doc_expect; diff --git a/tests/hagoromo_test/hagoromo_test.qrc b/tests/hagoromo_test/hagoromo_test.qrc index e8404289..faa19aba 100644 --- a/tests/hagoromo_test/hagoromo_test.qrc +++ b/tests/hagoromo_test/hagoromo_test.qrc @@ -26,5 +26,6 @@ response/timeline/next/1st/xrpc/app.bsky.feed.getTimeline response/timeline/next/2nd/xrpc/app.bsky.actor.getPreferences response/timeline/next/2nd/xrpc/app.bsky.feed.getTimeline + response/generator/xrpc/app.bsky.feed.getFeedGenerator diff --git a/tests/hagoromo_test/response/generator/xrpc/app.bsky.feed.getFeedGenerator b/tests/hagoromo_test/response/generator/xrpc/app.bsky.feed.getFeedGenerator new file mode 100644 index 00000000..efd64b5c --- /dev/null +++ b/tests/hagoromo_test/response/generator/xrpc/app.bsky.feed.getFeedGenerator @@ -0,0 +1,26 @@ +{ + "view": { + "uri": "at://did:plc:42fxwa2jeumqzzggx/app.bsky.feed.generator/aaagrsa", + "cid": "bafyreib7pgajpklwexy4lidm", + "did": "did:web:view.bsky.social", + "creator": { + "did": "did:plc:42fxwa2jeumqzzggxj", + "handle": "creator.bsky.social", + "displayName": "creator:displayName", + "avatar": "https://cdn.bsky.social/creator_avator.jpeg", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + "displayName": "view:displayName", + "description": "view:description", + "avatar": "https://cdn.bsky.social/view_avator.jpeg", + "likeCount": 9, + "viewer": {}, + "indexedAt": "2023-07-27T14:40:06.637Z" + }, + "isOnline": true, + "isValid": true +} diff --git a/tests/hagoromo_test/tst_hagoromo_test.cpp b/tests/hagoromo_test/tst_hagoromo_test.cpp index 16372148..d2cae4d3 100644 --- a/tests/hagoromo_test/tst_hagoromo_test.cpp +++ b/tests/hagoromo_test/tst_hagoromo_test.cpp @@ -10,6 +10,7 @@ #include "notificationlistmodel.h" #include "userprofile.h" #include "tools/qstringex.h" +#include "feedgeneratorlink.h" class hagoromo_test : public QObject { @@ -37,6 +38,7 @@ private slots: void test_TimelineListModel_quote_label(); void test_NotificationListModel_warn(); void test_TimelineListModel_next(); + void test_FeedGeneratorLink(); private: WebServer m_mockServer; @@ -1225,6 +1227,34 @@ void hagoromo_test::test_TimelineListModel_next() == "bafyreiejog3yvjc2tdg4muknodbplaib2yqftukwurd4qjcnal3zdxu4ni11_13"); } +void hagoromo_test::test_FeedGeneratorLink() +{ + FeedGeneratorLink link; + + QVERIFY(link.checkUri("https://bsky.app/profile/did:plc:hoge/feed/aaaaaaaa") == true); + QVERIFY(link.checkUri("https://staging.bsky.app/profile/did:plc:hoge/feed/aaaaaaaa") == false); + QVERIFY(link.checkUri("https://bsky.app/feeds/did:plc:hoge/feed/aaaaaaaa") == false); + QVERIFY(link.checkUri("https://bsky.app/profile/did:plc:hoge/feeds/aaaaaaaa") == false); + QVERIFY(link.checkUri("https://bsky.app/profile/did:plc:hoge/feed/") == false); + QVERIFY(link.checkUri("https://bsky.app/profile/handle/feed/aaaaaaaa") == false); + + QVERIFY(link.convertToAtUri("https://bsky.app/profile/did:plc:hoge/feed/aaaaaaaa") + == "at://did:plc:hoge/app.bsky.feed.generator/aaaaaaaa"); + + link.setAccount(m_service + "/generator", QString(), QString(), QString(), "dummy", QString()); + { + QSignalSpy spy(&link, SIGNAL(runningChanged())); + link.getFeedGenerator("https://bsky.app/profile/did:plc:hoge/feed/aaaaaaaa"); + spy.wait(); + QVERIFY2(spy.count() == 2, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + } + + QVERIFY(link.avatar() == "https://cdn.bsky.social/view_avator.jpeg"); + QVERIFY(link.displayName() == "view:displayName"); + QVERIFY(link.creatorHandle() == "creator.bsky.social"); + QVERIFY(link.likeCount() == 9); +} + void hagoromo_test::test_RecordOperatorCreateRecord(const QByteArray &body) { QJsonDocument json_doc = QJsonDocument::fromJson(body); From 89ca9d0fb867e136517c32b1e63feb2b185e2db7 Mon Sep 17 00:00:00 2001 From: Takayuki Orito Date: Sat, 9 Sep 2023 00:28:03 +0900 Subject: [PATCH 08/10] =?UTF-8?q?OGP=E3=81=AE=E3=82=BF=E3=82=B0=E3=81=AE?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=87=A6=E7=90=86=E3=81=AE=E8=A6=8B=E7=9B=B4?= =?UTF-8?q?=E3=81=97=20(#40)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * OGPのタグの解析処理の見直し * サムネ画像のURLの修正 --- lib/tools/opengraphprotocol.cpp | 161 +++++++++++------- lib/tools/opengraphprotocol.h | 5 +- tests/atprotocol_test/response/ogp/file4.html | 2 +- tests/atprotocol_test/response/ogp/file5.html | 2 +- tests/atprotocol_test/response/ogp/file6.html | 2 +- tests/atprotocol_test/tst_atprotocol_test.cpp | 9 +- 6 files changed, 111 insertions(+), 70 deletions(-) diff --git a/lib/tools/opengraphprotocol.cpp b/lib/tools/opengraphprotocol.cpp index 854fd1d2..db4db0c1 100644 --- a/lib/tools/opengraphprotocol.cpp +++ b/lib/tools/opengraphprotocol.cpp @@ -121,50 +121,46 @@ bool OpenGraphProtocol::parse(const QByteArray &data, const QString &src_uri) QTextStream ts(data); ts.setCodec(charset.toLatin1()); - QString rebuild_text = rebuildHtml(ts.readAll()); - - QString errorMsg; - int errorLine; - int errorColumn; - QDomDocument doc; - if (!doc.setContent(rebuild_text, false, &errorMsg, &errorLine, &errorColumn)) { - qDebug() << "parse" << errorMsg << ", Line=" << errorLine << ", Column=" << errorColumn; - qDebug().noquote().nospace() << "--- rebuild_text -----------------"; - qDebug().noquote().nospace() << rebuild_text; - qDebug().noquote().nospace() << "----------------------------------"; - } else { - QDomElement root = doc.documentElement(); - QDomElement head = root.firstChildElement("head"); - - setUri(src_uri); - - QDomElement element = head.firstChildElement(); - while (!element.isNull()) { - if (element.tagName().toLower() == "meta") { - QString property = element.attribute("property"); - QString content = element.attribute("content"); - if (property == "og:url") { - setUri(content); - } else if (property == "og:title") { - setTitle(content); - } else if (property == "og:description") { - setDescription(content); - } else if (property == "og:image") { - // ダウンロードしてローカルパスに置換が必要 + rebuildHtml(ts.readAll(), doc); + qDebug().noquote().nospace() << doc.toString(); + + QDomElement root = doc.documentElement(); + QDomElement head = root.firstChildElement("head"); + + setUri(src_uri); + + QDomElement element = head.firstChildElement(); + while (!element.isNull()) { + if (element.tagName().toLower() == "meta") { + QString property = element.attribute("property"); + QString content = element.attribute("content"); + if (property == "og:url") { + setUri(content); + } else if (property == "og:title") { + setTitle(content); + } else if (property == "og:description") { + setDescription(content); + } else if (property == "og:image") { + // ダウンロードしてローカルパスに置換が必要 + if (content.startsWith("/")) { + QUrl uri(src_uri); + setThumb(uri.toString(QUrl::RemovePath) + content); + } else { setThumb(content); } - } else if (element.tagName().toLower() == "title") { - if (title().isEmpty()) { - setTitle(element.text()); - } } - element = element.nextSiblingElement(); - } - if (!uri().isEmpty() && !title().isEmpty()) { - ret = true; + } else if (element.tagName().toLower() == "title") { + if (title().isEmpty()) { + setTitle(element.text()); + } } + element = element.nextSiblingElement(); } + if (!uri().isEmpty() && !title().isEmpty()) { + ret = true; + } + return ret; } @@ -176,20 +172,12 @@ QString OpenGraphProtocol::extractCharset(const QString &data) const if (match.capturedTexts().isEmpty()) return charset; - QString errorMsg; - int errorLine; - int errorColumn; - QDomDocument doc; QString result; int pos; while ((pos = match.capturedStart()) != -1) { - result = rebuildTag(match.captured()); - - if (!doc.setContent(result, false, &errorMsg, &errorLine, &errorColumn)) { - qDebug() << "parse" << errorMsg << ", Line=" << errorLine << ", Column=" << errorColumn; - } else { - QDomElement element = doc.documentElement(); + QDomElement element = doc.createElement("meta"); + if (rebuildTag(match.captured(), element)) { if (element.tagName().toLower() == "meta") { if (element.attribute("http-equiv") == "content-type") { QString content = element.attribute("content"); @@ -223,30 +211,31 @@ QString OpenGraphProtocol::extractCharset(const QString &data) const return charset; } -QString OpenGraphProtocol::rebuildHtml(const QString &text) const +void OpenGraphProtocol::rebuildHtml(const QString &text, QDomDocument &doc) const { QRegularExpressionMatch match = m_rxMeta.match(text); if (match.capturedTexts().isEmpty()) - return text; + return; + + QDomElement html = doc.createElement("html"); + QDomElement head = doc.createElement("head"); - QString errorMsg; - int errorLine; - int errorColumn; - QDomDocument doc; - QString result; int pos; while ((pos = match.capturedStart()) != -1) { - QString temp = rebuildTag(match.captured()); - if (doc.setContent(temp, false, &errorMsg, &errorLine, &errorColumn)) { - result += temp + "\n"; + QDomElement element = doc.createElement("meta"); + if (rebuildTag(match.captured(), element)) { + head.appendChild(element); } match = m_rxMeta.match(text, pos + match.capturedLength()); } - return QString("%1").arg(result); + html.appendChild(head); + doc.appendChild(html); + + return; } -QString OpenGraphProtocol::rebuildTag(QString text) const +bool OpenGraphProtocol::rebuildTag(QString text, QDomElement &element) const { QChar c; int state = 0; @@ -256,7 +245,17 @@ QString OpenGraphProtocol::rebuildTag(QString text) const // 2:スペース(=より後ろ) // 3:属性値 QString result; + QStringList names; + QStringList values; + text = text.trimmed(); + + int close_tag_pos = -1; + if (text.toLower().startsWith("$", QRegularExpression::CaseInsensitiveOption)); + } if (!text.endsWith("/>") && !text.toLower().startsWith("", "/>"); } @@ -266,12 +265,22 @@ QString OpenGraphProtocol::rebuildTag(QString text) const if (state == 0) { if (c == ' ') { state = 1; + names.append(""); + values.append(""); } result += c; } else if (state == 1) { - if (c == '=') { + if (c == '/' || c == '>') { + if (close_tag_pos > i) { + element.appendChild(element.toDocument().createTextNode( + text.mid(i + 1, close_tag_pos - (i + 1)))); + } + break; + } else if (c == '=') { state = 2; in_quote = false; + } else { + names.last().append(c); } result += c; } else if (state == 2) { @@ -282,6 +291,7 @@ QString OpenGraphProtocol::rebuildTag(QString text) const result += '\"'; in_quote = false; state = 3; + values.last().append(c); } result += c; } else if (state == 3) { @@ -289,16 +299,41 @@ QString OpenGraphProtocol::rebuildTag(QString text) const if (c == '\"') { state = 1; in_quote = false; + names.append(""); + values.append(""); + } else { + values.last().append(c); } } else { - if (c == ' ') { + if (c == '/' || c == '>') { + if (close_tag_pos > i) { + element.appendChild(element.toDocument().createTextNode( + text.mid(i + 1, close_tag_pos - (i + 1)))); + } + break; + } else if (c == ' ') { result += '\"'; state = 1; + names.append(""); + values.append(""); + } else { + values.last().append(c); } } result += c; } else { } } - return result; + + if (names.length() == values.length() && !names.isEmpty()) { + for (int i = 0; i < names.length(); i++) { + if (!names.at(i).trimmed().isEmpty()) + element.setAttribute(names.at(i).trimmed(), values.at(i).trimmed()); + } + return true; + } else if (element.tagName() == "title") { + return true; + } else { + return false; + } } diff --git a/lib/tools/opengraphprotocol.h b/lib/tools/opengraphprotocol.h index 9b19efa9..de9d7c65 100644 --- a/lib/tools/opengraphprotocol.h +++ b/lib/tools/opengraphprotocol.h @@ -4,6 +4,7 @@ #include #include #include +#include class OpenGraphProtocol : public QObject { @@ -30,8 +31,8 @@ class OpenGraphProtocol : public QObject private: bool parse(const QByteArray &data, const QString &src_uri); QString extractCharset(const QString &data) const; - QString rebuildHtml(const QString &text) const; - QString rebuildTag(QString text) const; + void rebuildHtml(const QString &text, QDomDocument &doc) const; + bool rebuildTag(QString text, QDomElement &element) const; QRegularExpression m_rxMeta; diff --git a/tests/atprotocol_test/response/ogp/file4.html b/tests/atprotocol_test/response/ogp/file4.html index 2f0d723a..3b643ce0 100644 --- a/tests/atprotocol_test/response/ogp/file4.html +++ b/tests/atprotocol_test/response/ogp/file4.html @@ -12,7 +12,7 @@ - file4 タイトル + file4 タイトル ファイル4 diff --git a/tests/atprotocol_test/response/ogp/file5.html b/tests/atprotocol_test/response/ogp/file5.html index 15f4dd54..502f2c97 100644 --- a/tests/atprotocol_test/response/ogp/file5.html +++ b/tests/atprotocol_test/response/ogp/file5.html @@ -9,7 +9,7 @@ - + diff --git a/tests/atprotocol_test/response/ogp/file6.html b/tests/atprotocol_test/response/ogp/file6.html index 097544ec..d9386799 100644 --- a/tests/atprotocol_test/response/ogp/file6.html +++ b/tests/atprotocol_test/response/ogp/file6.html @@ -19,7 +19,7 @@ - + diff --git a/tests/atprotocol_test/tst_atprotocol_test.cpp b/tests/atprotocol_test/tst_atprotocol_test.cpp index 9d040719..7ffcbf8c 100644 --- a/tests/atprotocol_test/tst_atprotocol_test.cpp +++ b/tests/atprotocol_test/tst_atprotocol_test.cpp @@ -110,6 +110,7 @@ void atprotocol_test::test_ComAtprotoServerCreateSession() void atprotocol_test::test_OpenGraphProtocol() { OpenGraphProtocol ogp; + { QSignalSpy spy(&ogp, SIGNAL(finished(bool))); ogp.getData(m_service + "/ogp/file1.html"); @@ -282,7 +283,8 @@ void atprotocol_test::test_OpenGraphProtocol() QList arguments = spy.takeFirst(); QVERIFY(arguments.at(0).toBool()); - QVERIFY2(ogp.uri() == "http://localhost/response/ogp/file5.html", ogp.uri().toLocal8Bit()); + QVERIFY2(ogp.uri() == "http://localhost/response/ogp/file5.html?id=10186&s=720", + ogp.uri().toLocal8Bit()); QVERIFY2(ogp.title() == QString("file5 ") .append(QChar(0x30bf)) @@ -308,7 +310,10 @@ void atprotocol_test::test_OpenGraphProtocol() QVERIFY2(ogp.title() == QString("file6 TITLE"), ogp.title().toLocal8Bit()); QVERIFY2(ogp.description() == QString("file6 ").append(QChar(0x8a73)).append(QChar(0x7d30)), ogp.description().toLocal8Bit()); - QVERIFY(ogp.thumb() == "http://localhost:%1/response/ogp/images/file6.png"); + QVERIFY2(ogp.thumb() + == QString("http://localhost:%1/response/ogp/images/file6.png") + .arg(QString::number(m_listenPort)), + ogp.thumb().toLocal8Bit()); } } From 97da89f0743c032e5fa807177accde673561dc7d Mon Sep 17 00:00:00 2001 From: Takayuki Orito Date: Sat, 9 Sep 2023 23:01:46 +0900 Subject: [PATCH 09/10] =?UTF-8?q?=E3=83=9D=E3=82=B9=E3=83=88=E3=81=AE?= =?UTF-8?q?=E3=81=84=E3=81=84=E3=81=AD=E3=81=A8=E3=83=AA=E3=83=9D=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=81=97=E3=81=9F=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E3=81=AE=E4=B8=80=E8=A6=A7=E5=AF=BE=E5=BF=9C=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ポストのいいねとリポスト表示 * レイアウトを修正 * レイアウト調整 * 読み込み件数をデフォルトに変更 --- app/app.pro | 1 + app/i18n/qt_ja_JP.qm | Bin 26689 -> 26987 bytes app/i18n/qt_ja_JP.ts | 119 ++++++++----- app/main.cpp | 3 + app/qml/parts/PostControls.qml | 31 ++++ app/qml/view/AnyProfileListView.qml | 64 +++++++ app/qml/view/ColumnView.qml | 50 ++++++ app/qml/view/NotificationListView.qml | 4 + app/qml/view/PostThreadView.qml | 4 + app/qml/view/ProfileView.qml | 10 ++ app/qml/view/TimelineView.qml | 5 +- app/qtquick/anyprofilelistmodel.cpp | 168 ++++++++++++++++++ app/qtquick/anyprofilelistmodel.h | 40 +++++ app/qtquick/followslistmodel.cpp | 13 ++ app/qtquick/followslistmodel.h | 1 + app/qtquick/qtquick.pri | 2 + .../app/bsky/feed/appbskyfeedgetlikes.cpp | 49 +++++ .../app/bsky/feed/appbskyfeedgetlikes.h | 26 +++ .../bsky/feed/appbskyfeedgetrepostedby.cpp | 51 ++++++ .../app/bsky/feed/appbskyfeedgetrepostedby.h | 27 +++ lib/lib.pri | 4 + tests/hagoromo_test/hagoromo_test.qrc | 2 + .../anyprofile/xrpc/app.bsky.feed.getLikes | 98 ++++++++++ .../xrpc/app.bsky.feed.getRepostedBy | 34 ++++ tests/hagoromo_test/tst_hagoromo_test.cpp | 30 ++++ 25 files changed, 788 insertions(+), 48 deletions(-) create mode 100644 app/qml/view/AnyProfileListView.qml create mode 100644 app/qtquick/anyprofilelistmodel.cpp create mode 100644 app/qtquick/anyprofilelistmodel.h create mode 100644 lib/atprotocol/app/bsky/feed/appbskyfeedgetlikes.cpp create mode 100644 lib/atprotocol/app/bsky/feed/appbskyfeedgetlikes.h create mode 100644 lib/atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.cpp create mode 100644 lib/atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.h create mode 100644 tests/hagoromo_test/response/anyprofile/xrpc/app.bsky.feed.getLikes create mode 100644 tests/hagoromo_test/response/anyprofile/xrpc/app.bsky.feed.getRepostedBy diff --git a/app/app.pro b/app/app.pro index 4bcff4c1..4e7cd673 100644 --- a/app/app.pro +++ b/app/app.pro @@ -49,6 +49,7 @@ QML_FILES = \ qml/parts/TagLabel.qml \ qml/parts/TagLabelLayout.qml \ qml/parts/VersionInfomation.qml \ + qml/view/AnyProfileListView.qml \ qml/view/ColumnView.qml \ qml/view/ImageFullView.qml \ qml/view/NotificationListView.qml \ diff --git a/app/i18n/qt_ja_JP.qm b/app/i18n/qt_ja_JP.qm index d7d055c78c87bd613b75aa2c6bdd326ceaf54c5e..f45a85aceb8a921f7b3e16490cacc629fee99102 100644 GIT binary patch delta 3433 zcmY+Gc~lfv7RGOPS5;SaH{Bwe3knexRKyl#lT{W0*+I4j9R)ptsL`N^NwiTQLFGgv z=%5ghkQhZIDC%g;BrX_%n7AZC3{l9K5K&vFto)d+BL?m0lC|}2Dagr$V2cprpiDEt_ za@bE4ZzOVRCrZ0cG-E1paS9^!bK>6KNMsd8+y<;+b&9zCTZoK_pX1^fk!>4sUmqnB z^NH&zMvx-n{`)b}(6fvd*~C3^!~biEd$AP@T_b+H7m?#t;`LTY+=6(Idc4;}yk`p0 zq%*{4O(4=|5}${}gRF_KUW|Cp7%jFFzi%BGX~>~h7Zhhse8XBI&o_u~K|(XWBfj-4 zk;<9)j`xTxTuHD+0=^&6$jk#o@(#v;)fC{|N|bVeLU%S3%^69v-FKj?ffS*xAR6V) zn7E80+D!M?7!z+(R8b9DWuHM&XY+~d9#iy|Q$(*R7?WpFbm{v2x+!7fCUnG>65cU! zkmi&kLFa3fw9WW{Xv#TC+UXBIp`->>9J`W|moyQ@ou`zqum#F`#(+K)iIyvGP-+nt zc6~;v%}C63E~P$#hNe#@L%}m5k6JPmLN6ZeWY~;7_ms%cf#=*?$Z!uGOq)pQuU|nS zo|JB^Dnq)ql-^uJT^c}rsp_O?de-iEyl50bZkGo5JGLwUznUl5HjFwXf=!F8-e zz)tCE}tL(QT^{&uFPgHvPccDN?ru69xAu z*1WopQxw*pM`ERllFcU}mhpZ>>fp=4domI{1CW+ zi@NnOeBuvo&8z!k+}f&DMCQA=qHc2{>v`O}twBW5>$sA9Bs9XhoGUTS|FBD3X$gL? zSFiHZlelG0s*oMu#!Rl`|&tT*cS(u-sVg zlOEWSSrE71IuVjIaQkCTLz&H(Sk5(m-HZDg?g&8$#))>^WgY6ai{;v zE1qlr?i}2}jnUGc>p1-d>`Bjmx5A#rlydiuIup5M@`6J?(ZmJ3Wib>X{*ND)z6nw-;)jdFzyaQV zG9>Eg%#YuKz)o(ADWiC{F?aZ|7Ffe%6(6zi3h2j2n*4F{Dn5S1ci6z4d|D$mT6dT+ z{zrcP(<`v2DSmubHJn|)gI}@%8jpzPmz60G=Ali`X( z8E3}u@9Uy*0`PoA2oiI>!0(!bh0T-r-6|Kvm-xyO#Ian;825nRI~!Xty@Ib@YJ7{x zO@Ry05XSHG#EWiu{6RNNyWbJM-uqYBiyvRV3%)0G@t>J0^GfAE>wq13ZRNl0f}PFC z5QIu7%GXWME`AS{N`mf0DDJZbr;D&F??Zy;?0yt-hcWf6;B^Oyg^5DAvGQA@u}UG_ z#N-Mg;ZZ9xHy0MS;sO7ij5GU%w<4kOkXoU<5WeWvE9`o46Gz&1;lsdrn1xbd|0swp zcp2lYe+dmPJMjE<;kZM)JxXmsjIpu2t?{YKeuNm5t}H(2#7#gd$~= z0DFp-DbFjfqvCw!4@aP-$a##hHPVn~9CqFR}1a@z|zg7_-OXsp_B6 zi6*hRBOCAei!G)E#@mP&>-;ePHV4H^8{#ll^TkWuk!Wp(*u5L$667z*rs9OKdRtOC zt;CDRCAXUIap=cNJ|?HPQAqxWn^Bn!V{DBSd3-(5?2}T;!JE*=b1AbJ2JbpnS`veW zhWAMYmaCz)RfssX}i9^;=0TTOp2VkEB+gAK+v=q}EFqH-CSr z%_JJv?To4Sq|VKw;TKNQjT{L_RFHIc&r4|IP3gYrLv3WQ^t`153D~N*+em!+MV0T- zg+u|qs;pQfK)wTOjx%>O7PScU*{*2Mktm?}`2dCt#25X+;`H_q>*Q=hi zp&-u>)v7m)5RH(h4jN3voaU-CiZd`q4eH`Sb2!{$^}a75qM?h_hx7X(wvW`O+IZ}x zkGjVW@yJyCbp9feq%)OR2LiutZn-+zkx(00biVH&YC9L9D;W7{`kp#Rq%*K_4p4>r~kMQzs}I=Bk) zMeU)-V90W#_CyIp^x9hO1=IJBTb=glZCLBbCY|gE_AJpuH|Fv*bRb!`zSRQ8@mN=B znu@5Qy2ei&u{GwpCgV+fHQZuMsnj*?&W7mb>rS*3!-zU{R}~73XT0t=OOVqtN>z;M zF7div!uWY!*Y*F$0V27~C}lFH+v)x=iOS~>L`{FUsaf-rTxqEf#^18CL$NmIVq|vC zqJ{PuYoS~kI&=BjvTd=}{2l!b{mHWXvBNFZ@rzcxp1bPrk95aaHtp37_5Euk>CZ@I gvMB4ap7;r6osqRlYyF?GG=Gg{dZ=lA)VUG=2gzZ;X8-^I delta 3277 zcmXYzd0bTG8pofRIq#WsW`-Gw#U(}16bctyfI)CX1O?f57?4W1S5*3_nTn*0iYOu< zl~6CJg_a`Xh7Oit)>Sb9DKOKft088tD4CW^E}#1w-~RfZGjq=SKFjaV$5{VBv$FJZtUE@rBKs041k>XuWkg+wksB-E}YvLqeG#Sx#PeVxFGNnV*dN^-X3DcG$L<5V!3Y; zjkFP4h`>X1#Hwdty}6uDYGO4@$wIvby}F<{{BU3~k^fC%%?M~*II)(KMDh+|wsm-M zH;Mfb!1&iFcvuTj>T#N~xrr#zk*4}>LKQ?P+J%&=b=i4ucQi3R^tenWBl46BDM1y=OZUYv0;}*r=O+w}qDIw9jGaNLv#dmM$0+Svlw9q>89ap2&Y;BV1WI3tjYrR=^dIX`H1kH^3 zl+4SXp<;c=T=X1_By%Zx>|aP`8=mtUPv*O*nAx2&=bS?#`zdo<1zPkPWj3J_D)>TS)f`T8MNr6gk&D(2C`XnLPp$^;4|O{);GPlEP9v z1tl4BQn7t*5z*9-6b;W1xv^Anau6dLK2UKxya%dXskm)LC6bv^x?)BPdMWia5LKv$ z((%e@XdqNMD)T9kAzhjG024IEO<59&R=7-8u5CwvUI#f-1C{GXy+CW4lohlHk~wJM z7v>ISbA2-&bX8t#MK8kxlo!7nOB8lTY5VqTqQ32%i5)7zHjn5{50zhqCy~;|>FmTg zX&7f%fy(a?0tuh5@(l>gco*l+?lrD=gbl{!M7=XA4hp?3 zV};6yukiX5PN!*{<0t>a$D9)jI3tKN+Qb=G%9;2~s63m6x>X6gZ@_+ZM}=C~WFp^o zp*9Xm>)*+lR4UY;y$xl!3Wo_ou)MiZ=rAB}x6{JKF|Z5KjWgsCXIQv!@%$+i-j~y9 zlVJPr*BI7aoGICYtqB_p^y5taM!0n57CdynaJd5Ox`%S6I14vEbi+_O3Eh=vF!X1I zyJhQPM}ri?-G*V1o{fov^U>*x%()nfkUn62GS@(=Pg!4hAPt?t+=oM=LwB&DYq79r z9cQW&d({}>Z5G~azy=l;nR5=bvM9SB4!g|~`kjYsXjw)*8f_TL8Q;O)d3p|ZG{VYq ztKsB68a8V=G(I(y%{nm#k?&^YV$L*IRyG-mNzk$~yTPhga|W+w<%Sq^wt!WJ zA+XUwtjZf3_d3hAXhvZDLbkO8>o{HIjJwIUPelvHIcta{N2QK`jGL$WXnWn%3Bh;33fXV`x6z=%zFzFj=+ zvlwULV6iy@tr>7jY^lb&J|SY8OAHdN7cH+Y(*G^~`PdgnO@sKn?nn3>sfQo=4MVt7 z9rXT7WYDbMI2#9YzXJ89js=LkO1&k|85<8%*Pp^hhEC3e0<~3yJw-iMpH}~jjqa&0 z9EO%6lQ?6~t1k`o$Iw1e-w<3doD(eS`*p~~?^pGcB4lW&lWiv6Ymk~> z1yCln)tNB=z1^kuh0>-;U0X0NA-=L=6ix`|KV*&9JQ$0&?7QPfd^$~* zgX~W4nlDc{)P&4pIb;7LM}1yM6md{a+uI3k%#*W=Veq4>~$&EumzLlKm z<(ipsHq8I)t(t|(k1*Gbnx#c*Xko9W(&&N=4{Ns7VWW}THJ?X*4Tl`enVhHjUhxS| z0!L2g7|l=jp>jV_bIE?B`OW1_Z`9n&MFmIP(>!TK0><82&HK;@>()+sn2Z+R(oQe7 zOvl*l)D}NZ?80%An#y+7@>#9AM-$FVVIP#{U8DX}jtg zAWo~c`_Jq6Jl(Fn{}lI=+c+cjI;kuIR_m$je`PExut4X{tP`c{>h};BJ$G#+h1f zux^zrOaK^0q~+pM)Qr~3ULizJOg diff --git a/app/i18n/qt_ja_JP.ts b/app/i18n/qt_ja_JP.ts index 888d97d9..241c5aad 100644 --- a/app/i18n/qt_ja_JP.ts +++ b/app/i18n/qt_ja_JP.ts @@ -75,6 +75,19 @@ 追加 + + AnyProfileListView + + + Liked by + いいねしたアカウント + + + + Reposted by + リポストしたアカウント + + ColumnListModel @@ -199,57 +212,57 @@ ColumnView - + Home ホーム - + Notifications 通知 - + Search posts 検索(ポスト) - + Search users 検索(ユーザー) - + Feed フィード - + User ユーザー - + Unknown 不明 - + Move to left 左へ移動 - + Move to right 右へ移動 - + Delete column 削除 - + Settings 設定 @@ -1463,41 +1476,53 @@ PostControls - + Repost リポスト - + Quote 引用 - - + + Translate 翻訳 - - + + Copy post text ポストをコピー - - + + Open in Official 公式で開く - + + + Reposted by + リポストしたアカウント + + + + + Liked by + いいねしたアカウント + + + Delete post ポストを削除 - - + + Report post ポストを通報 @@ -1557,12 +1582,12 @@ PostThreadView - + Post thread ポストスレッド - + Quoted content warning 閲覧注意な引用 @@ -1593,112 +1618,112 @@ ProfileView - + Edit Profile プロフィールを編集 - + Following フォロー中 - + Follow フォローする - + Profile プロフィール - + Follows you あなたをフォロー中 - + follows フォロー - + followers フォロワー - + posts ポスト - + Send mention メンションを送る - + Copy handle ハンドルをコピー - + Copy DID DIDをコピー - + Open in new col 新しいカラムで開く - + Open in Official 公式で開く - + Unmute account ミュート解除 - + Mute account ミュート - + Unblock account ブロック解除 - + Block account ブロック - + Report account 通報 - + Account blocked ブロックしたアカウント - + Account muted ミュートしたアカウント - + This account has been flagged : このアカウントに設定されたラベル : - + This account has blocked you あなたをブロックしているアカウント @@ -2146,7 +2171,7 @@ TimelineView - + Quoted content warning 閲覧注意な引用 diff --git a/app/main.cpp b/app/main.cpp index 39877255..b3a59d5c 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -22,6 +22,7 @@ #include "qtquick/feedgeneratorlistmodel.h" #include "qtquick/languagelistmodel.h" #include "qtquick/contentfiltersettinglistmodel.h" +#include "qtquick/anyprofilelistmodel.h" #include "qtquick/thumbnailprovider.h" #include "qtquick/encryption.h" #include "qtquick/userprofile.h" @@ -83,6 +84,8 @@ int main(int argc, char *argv[]) qmlRegisterType( "tech.relog.hagoromo.contentfiltersettinglistmodel", 1, 0, "ContentFilterSettingListModel"); + qmlRegisterType("tech.relog.hagoromo.anyprofilelistmodel", 1, 0, + "AnyProfileListModel"); qmlRegisterType("tech.relog.hagoromo.encryption", 1, 0, "Encryption"); qmlRegisterType("tech.relog.hagoromo.userprofile", 1, 0, "UserProfile"); qmlRegisterType("tech.relog.hagoromo.systemtool", 1, 0, "SystemTool"); diff --git a/app/qml/parts/PostControls.qml b/app/qml/parts/PostControls.qml index 40f27512..26136bdc 100644 --- a/app/qml/parts/PostControls.qml +++ b/app/qml/parts/PostControls.qml @@ -25,6 +25,9 @@ RowLayout { signal triggeredCopyToClipboard() signal triggeredDeletePost() signal triggeredRequestReport() + signal triggeredRequestViewLikedBy() + signal triggeredRequestViewRepostedBy() + function openInOhters(uri, handle){ if(uri.length === 0 || uri.startsWith("at://") === false){ @@ -94,6 +97,7 @@ RowLayout { } Menu { id: myMorePopup + width: 230 MenuItem { icon.source: "../images/translate.png" text: qsTr("Translate") @@ -111,6 +115,19 @@ RowLayout { onTriggered: openInOhters(postUri, handle) } MenuSeparator {} + MenuItem { + text: qsTr("Reposted by") + enabled: repostButton.iconText > 0 + icon.source: "../images/repost.png" + onTriggered: triggeredRequestViewRepostedBy() + } + MenuItem { + text: qsTr("Liked by") + enabled: likeButton.iconText > 0 + icon.source: "../images/like.png" + onTriggered: triggeredRequestViewLikedBy() + } + MenuSeparator {} MenuItem { text: qsTr("Delete post") enabled: mine @@ -126,6 +143,7 @@ RowLayout { } Menu { id: theirMorePopup + width: 230 MenuItem { icon.source: "../images/translate.png" text: qsTr("Translate") @@ -143,6 +161,19 @@ RowLayout { onTriggered: openInOhters(postUri, handle) } MenuSeparator {} + MenuItem { + text: qsTr("Reposted by") + enabled: repostButton.iconText > 0 + icon.source: "../images/repost.png" + onTriggered: triggeredRequestViewRepostedBy() + } + MenuItem { + text: qsTr("Liked by") + enabled: likeButton.iconText > 0 + icon.source: "../images/like.png" + onTriggered: triggeredRequestViewLikedBy() + } + MenuSeparator {} MenuItem { text: qsTr("Report post") icon.source: "../images/report.png" diff --git a/app/qml/view/AnyProfileListView.qml b/app/qml/view/AnyProfileListView.qml new file mode 100644 index 00000000..cbaf872a --- /dev/null +++ b/app/qml/view/AnyProfileListView.qml @@ -0,0 +1,64 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls.Material 2.15 + +import tech.relog.hagoromo.anyprofilelistmodel 1.0 +import tech.relog.hagoromo.singleton 1.0 + +import "../parts" +import "../controls" + +ColumnLayout { + id: anyProfileListView + + property alias hoveredLink: profileListView.hoveredLink + property alias accountDid: profileListView.accountDid // 取得するユーザー + + property alias targetUri: anyProfileListModel.targetUri + property alias type: anyProfileListModel.type + property alias autoLoading: anyProfileListModel.autoLoading + property alias model: anyProfileListModel + + signal requestViewProfile(string did) + signal errorOccured(string message) + signal back() + + Frame { + Layout.fillWidth: true + leftPadding: 0 + topPadding: 0 + rightPadding: 10 + bottomPadding: 0 + + RowLayout { + IconButton { + Layout.preferredWidth: AdjustedValues.b30 + Layout.preferredHeight: AdjustedValues.b30 + flat: true + iconSource: "../images/arrow_left_single.png" + onClicked: anyProfileListView.back() + } + Label { + Layout.fillWidth: true + Layout.leftMargin: 10 + font.pointSize: AdjustedValues.f10 + text: anyProfileListModel.type == AnyProfileListModel.Like ? qsTr("Liked by") : qsTr("Reposted by") + } + } + } + + ProfileListView { + id: profileListView + Layout.fillWidth: true + Layout.fillHeight: true + unfollowAndRemove: false + + model: AnyProfileListModel { + id: anyProfileListModel + onErrorOccured: (message) => anyProfileListView.errorOccured(message) + } + + onRequestViewProfile: (did) => anyProfileListView.requestViewProfile(did) + } +} diff --git a/app/qml/view/ColumnView.qml b/app/qml/view/ColumnView.qml index 7b9dac54..648e3564 100644 --- a/app/qml/view/ColumnView.qml +++ b/app/qml/view/ColumnView.qml @@ -11,6 +11,7 @@ import tech.relog.hagoromo.searchpostlistmodel 1.0 import tech.relog.hagoromo.searchprofilelistmodel 1.0 import tech.relog.hagoromo.customfeedlistmodel 1.0 import tech.relog.hagoromo.authorfeedlistmodel 1.0 +import tech.relog.hagoromo.anyprofilelistmodel 1.0 import tech.relog.hagoromo.singleton 1.0 import "../controls" @@ -80,6 +81,8 @@ ColumnLayout { onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) + onRequestViewLikedBy: (uri) => columnStackView.push(likesProfilesComponent, { "targetUri": uri }) + onRequestViewRepostedBy: (uri) => columnStackView.push(repostsProfilesComponent, { "targetUri": uri }) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -112,6 +115,8 @@ ColumnLayout { } onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) + onRequestViewLikedBy: (uri) => columnStackView.push(likesProfilesComponent, { "targetUri": uri }) + onRequestViewRepostedBy: (uri) => columnStackView.push(repostsProfilesComponent, { "targetUri": uri }) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -135,6 +140,8 @@ ColumnLayout { onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) + onRequestViewLikedBy: (uri) => columnStackView.push(likesProfilesComponent, { "targetUri": uri }) + onRequestViewRepostedBy: (uri) => columnStackView.push(repostsProfilesComponent, { "targetUri": uri }) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -166,6 +173,8 @@ ColumnLayout { onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) + onRequestViewLikedBy: (uri) => columnStackView.push(likesProfilesComponent, { "targetUri": uri }) + onRequestViewRepostedBy: (uri) => columnStackView.push(repostsProfilesComponent, { "targetUri": uri }) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onRequestReportAccount: (did) => columnView.requestReportAccount(account.uuid, did) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -204,6 +213,8 @@ ColumnLayout { onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) + onRequestViewLikedBy: (uri) => columnStackView.push(likesProfilesComponent, { "targetUri": uri }) + onRequestViewRepostedBy: (uri) => columnStackView.push(repostsProfilesComponent, { "targetUri": uri }) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -251,6 +262,8 @@ ColumnLayout { onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) + onRequestViewLikedBy: (uri) => columnStackView.push(likesProfilesComponent, { "targetUri": uri }) + onRequestViewRepostedBy: (uri) => columnStackView.push(repostsProfilesComponent, { "targetUri": uri }) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink @@ -283,11 +296,48 @@ ColumnLayout { onRequestViewImages: (index, paths, alts) => columnView.requestViewImages(index, paths, alts) onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) onRequestViewFeedGenerator: (name, uri) => columnView.requestViewFeedGenerator(account.uuid, name, uri) + onRequestViewLikedBy: (uri) => columnStackView.push(likesProfilesComponent, { "targetUri": uri }) + onRequestViewRepostedBy: (uri) => columnStackView.push(repostsProfilesComponent, { "targetUri": uri }) onRequestReportPost: (uri, cid) => columnView.requestReportPost(account.uuid, uri, cid) onHoveredLinkChanged: columnView.hoveredLink = hoveredLink } } + Component { + id: likesProfilesComponent + AnyProfileListView { + accountDid: account.did + autoLoading: settings.autoLoading + type: AnyProfileListModel.Like + + onErrorOccured: (message) => { console.log(message) } + onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) + onHoveredLinkChanged: columnView.hoveredLink = hoveredLink + onBack: { + if(!columnStackView.empty){ + columnStackView.pop() + } + } + } + } + Component { + id: repostsProfilesComponent + AnyProfileListView { + accountDid: account.did + autoLoading: settings.autoLoading + type: AnyProfileListModel.Repost + + onErrorOccured: (message) => { console.log(message) } + onRequestViewProfile: (did) => columnStackView.push(profileComponent, { "userDid": did }) + onHoveredLinkChanged: columnView.hoveredLink = hoveredLink + onBack: { + if(!columnStackView.empty){ + columnStackView.pop() + } + } + } + } + function load(){ console.log("ColumnLayout:componentType=" + componentType) diff --git a/app/qml/view/NotificationListView.qml b/app/qml/view/NotificationListView.qml index b3e1a9e6..c8b5e399 100644 --- a/app/qml/view/NotificationListView.qml +++ b/app/qml/view/NotificationListView.qml @@ -29,6 +29,8 @@ ScrollView { signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) signal requestViewFeedGenerator(string name, string uri) + signal requestViewLikedBy(string uri) + signal requestViewRepostedBy(string uri) signal requestReportPost(string uri, string cid) ListView { @@ -132,6 +134,8 @@ ScrollView { postControls.postUri: model.uri postControls.handle: model.handle postControls.onTriggeredCopyToClipboard: systemTool.copyToClipboard(model.recordTextPlain) + postControls.onTriggeredRequestViewLikedBy: notificationListView.requestViewLikedBy(model.uri) + postControls.onTriggeredRequestViewRepostedBy: notificationListView.requestViewRepostedBy(model.uri) postControls.onTriggeredRequestReport: notificationListView.requestReportPost(model.uri, model.cid) onClicked: { diff --git a/app/qml/view/PostThreadView.qml b/app/qml/view/PostThreadView.qml index 8d067287..b5f52166 100644 --- a/app/qml/view/PostThreadView.qml +++ b/app/qml/view/PostThreadView.qml @@ -28,6 +28,8 @@ ColumnLayout { signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) signal requestViewFeedGenerator(string name, string uri) + signal requestViewLikedBy(string uri) + signal requestViewRepostedBy(string uri) signal requestReportPost(string uri, string cid) signal back() @@ -196,6 +198,8 @@ ColumnLayout { postControls.onTriggeredCopyToClipboard: systemTool.copyToClipboard(model.recordTextPlain) postControls.onTriggeredDeletePost: rootListView.model.deletePost(model.index) postControls.onTriggeredRequestReport: postThreadView.requestReportPost(model.uri, model.cid) + postControls.onTriggeredRequestViewLikedBy: postThreadView.requestViewLikedBy(model.uri) + postControls.onTriggeredRequestViewRepostedBy: postThreadView.requestViewRepostedBy(model.uri) onHoveredLinkChanged: postThreadView.hoveredLink = hoveredLink } diff --git a/app/qml/view/ProfileView.qml b/app/qml/view/ProfileView.qml index 502ea7cd..f037ef21 100644 --- a/app/qml/view/ProfileView.qml +++ b/app/qml/view/ProfileView.qml @@ -39,6 +39,8 @@ ColumnLayout { signal requestViewProfile(string did) signal requestViewFeedGenerator(string name, string uri) signal requestViewAuthorFeed(string did, string handle) + signal requestViewLikedBy(string uri) + signal requestViewRepostedBy(string uri) signal requestReportPost(string uri, string cid) signal requestReportAccount(string did) @@ -427,6 +429,8 @@ ColumnLayout { } } onRequestViewFeedGenerator: (name, uri) => profileView.requestViewFeedGenerator(name, uri) + onRequestViewLikedBy: (uri) => profileView.requestViewLikedBy(uri) + onRequestViewRepostedBy: (uri) => profileView.requestViewRepostedBy(uri) onRequestReportPost: (uri, cid) => profileView.requestReportPost(uri, cid) onHoveredLinkChanged: profileView.hoveredLink = hoveredLink } @@ -456,6 +460,8 @@ ColumnLayout { } } onRequestViewFeedGenerator: (name, uri) => profileView.requestViewFeedGenerator(name, uri) + onRequestViewLikedBy: (uri) => profileView.requestViewLikedBy(uri) + onRequestViewRepostedBy: (uri) => profileView.requestViewRepostedBy(uri) onRequestReportPost: (uri, cid) => profileView.requestReportPost(uri, cid) onHoveredLinkChanged: profileView.hoveredLink = hoveredLink } @@ -485,6 +491,8 @@ ColumnLayout { } } onRequestViewFeedGenerator: (name, uri) => profileView.requestViewFeedGenerator(name, uri) + onRequestViewLikedBy: (uri) => profileView.requestViewLikedBy(uri) + onRequestViewRepostedBy: (uri) => profileView.requestViewRepostedBy(uri) onRequestReportPost: (uri, cid) => profileView.requestReportPost(uri, cid) onHoveredLinkChanged: profileView.hoveredLink = hoveredLink } @@ -514,6 +522,8 @@ ColumnLayout { } } onRequestViewFeedGenerator: (name, uri) => profileView.requestViewFeedGenerator(name, uri) + onRequestViewLikedBy: (uri) => profileView.requestViewLikedBy(uri) + onRequestViewRepostedBy: (uri) => profileView.requestViewRepostedBy(uri) onRequestReportPost: (uri, cid) => profileView.requestReportPost(uri, cid) onHoveredLinkChanged: profileView.hoveredLink = hoveredLink } diff --git a/app/qml/view/TimelineView.qml b/app/qml/view/TimelineView.qml index 50663f9c..b58df161 100644 --- a/app/qml/view/TimelineView.qml +++ b/app/qml/view/TimelineView.qml @@ -30,6 +30,8 @@ ScrollView { signal requestViewImages(int index, var paths, var alts) signal requestViewProfile(string did) signal requestViewFeedGenerator(string name, string uri) + signal requestViewLikedBy(string uri) + signal requestViewRepostedBy(string uri) signal requestReportPost(string uri, string cid) @@ -161,7 +163,8 @@ ScrollView { postControls.onTriggeredCopyToClipboard: systemTool.copyToClipboard(model.recordTextPlain) postControls.onTriggeredDeletePost: rootListView.model.deletePost(model.index) postControls.onTriggeredRequestReport: timelineView.requestReportPost(model.uri, model.cid) - + postControls.onTriggeredRequestViewLikedBy: timelineView.requestViewLikedBy(model.uri) + postControls.onTriggeredRequestViewRepostedBy: timelineView.requestViewRepostedBy(model.uri) onHoveredLinkChanged: timelineView.hoveredLink = hoveredLink } } diff --git a/app/qtquick/anyprofilelistmodel.cpp b/app/qtquick/anyprofilelistmodel.cpp new file mode 100644 index 00000000..cc3b3d67 --- /dev/null +++ b/app/qtquick/anyprofilelistmodel.cpp @@ -0,0 +1,168 @@ +#include "anyprofilelistmodel.h" + +#include "atprotocol/app/bsky/feed/appbskyfeedgetlikes.h" +#include "atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.h" + +using AtProtocolInterface::AppBskyFeedGetLikes; +using AtProtocolInterface::AppBskyFeedGetRepostedBy; + +AnyProfileListModel::AnyProfileListModel(QObject *parent) : FollowsListModel { parent } { } + +void AnyProfileListModel::getLatest() +{ + if (running() || targetUri().isEmpty()) + return; + setRunning(true); + + clear(); + + if (type() == AnyProfileListModelType::Like) { + AppBskyFeedGetLikes *likes = new AppBskyFeedGetLikes(this); + connect(likes, &AppBskyFeedGetLikes::finished, [=](bool success) { + if (success) { + m_cursor = likes->cursor(); + for (const auto &like : *likes->likes()) { + m_profileHash[like.actor.did] = like.actor; + m_formattedDescriptionHash[like.actor.did] = + m_systemTool.markupText(like.actor.description); + if (m_didList.contains(like.actor.did)) { + int row = m_didList.indexOf(like.actor.did); + emit dataChanged(index(row), index(row)); + } else { + beginInsertRows(QModelIndex(), m_didList.count(), m_didList.count()); + m_didList.append(like.actor.did); + endInsertRows(); + } + } + } else { + emit errorOccured(likes->errorMessage()); + } + setRunning(false); + likes->deleteLater(); + }); + likes->setAccount(account()); + likes->getLikes(targetUri(), QString(), 0, QString()); + + } else if (type() == AnyProfileListModelType::Repost) { + AppBskyFeedGetRepostedBy *reposts = new AppBskyFeedGetRepostedBy(this); + connect(reposts, &AppBskyFeedGetRepostedBy::finished, [=](bool success) { + if (success) { + m_cursor = reposts->cursor(); + for (const auto &profile : *reposts->profileViewList()) { + m_profileHash[profile.did] = profile; + m_formattedDescriptionHash[profile.did] = + m_systemTool.markupText(profile.description); + if (m_didList.contains(profile.did)) { + int row = m_didList.indexOf(profile.did); + emit dataChanged(index(row), index(row)); + } else { + beginInsertRows(QModelIndex(), m_didList.count(), m_didList.count()); + m_didList.append(profile.did); + endInsertRows(); + } + } + } else { + emit errorOccured(reposts->errorMessage()); + } + setRunning(false); + reposts->deleteLater(); + }); + reposts->setAccount(account()); + reposts->getRepostedBy(targetUri(), QString(), 0, QString()); + } +} + +void AnyProfileListModel::getNext() +{ + if (running() || targetUri().isEmpty() || m_cursor.isEmpty()) + return; + setRunning(true); + + if (type() == AnyProfileListModelType::Like) { + AppBskyFeedGetLikes *likes = new AppBskyFeedGetLikes(this); + connect(likes, &AppBskyFeedGetLikes::finished, [=](bool success) { + if (success) { + if (likes->likes()->isEmpty()) + m_cursor.clear(); + else + m_cursor = likes->cursor(); + for (const auto &like : *likes->likes()) { + m_profileHash[like.actor.did] = like.actor; + m_formattedDescriptionHash[like.actor.did] = + m_systemTool.markupText(like.actor.description); + if (m_didList.contains(like.actor.did)) { + int row = m_didList.indexOf(like.actor.did); + emit dataChanged(index(row), index(row)); + } else { + beginInsertRows(QModelIndex(), m_didList.count(), m_didList.count()); + m_didList.append(like.actor.did); + endInsertRows(); + } + } + } else { + emit errorOccured(likes->errorMessage()); + } + setRunning(false); + likes->deleteLater(); + }); + likes->setAccount(account()); + likes->getLikes(targetUri(), QString(), 0, m_cursor); + + } else if (type() == AnyProfileListModelType::Repost) { + AppBskyFeedGetRepostedBy *reposts = new AppBskyFeedGetRepostedBy(this); + connect(reposts, &AppBskyFeedGetRepostedBy::finished, [=](bool success) { + if (success) { + if (reposts->profileViewList()->isEmpty()) + m_cursor.clear(); + else + m_cursor = reposts->cursor(); + for (const auto &profile : *reposts->profileViewList()) { + m_profileHash[profile.did] = profile; + m_formattedDescriptionHash[profile.did] = + m_systemTool.markupText(profile.description); + if (m_didList.contains(profile.did)) { + int row = m_didList.indexOf(profile.did); + emit dataChanged(index(row), index(row)); + } else { + beginInsertRows(QModelIndex(), m_didList.count(), m_didList.count()); + m_didList.append(profile.did); + endInsertRows(); + } + } + } else { + emit errorOccured(reposts->errorMessage()); + } + setRunning(false); + reposts->deleteLater(); + }); + reposts->setAccount(account()); + reposts->getRepostedBy(targetUri(), QString(), 0, m_cursor); + } +} + +QString AnyProfileListModel::targetUri() const +{ + return m_targetUri; +} + +void AnyProfileListModel::setTargetUri(const QString &newTargetUri) +{ + if (m_targetUri == newTargetUri) + return; + m_targetUri = newTargetUri; + emit targetUriChanged(); +} + +AnyProfileListModel::AnyProfileListModelType AnyProfileListModel::type() const +{ + return m_type; +} + +void AnyProfileListModel::setType(const AnyProfileListModelType &newType) +{ + if (m_type == newType) + return; + m_type = newType; + clear(); + emit typeChanged(); +} diff --git a/app/qtquick/anyprofilelistmodel.h b/app/qtquick/anyprofilelistmodel.h new file mode 100644 index 00000000..40f297ae --- /dev/null +++ b/app/qtquick/anyprofilelistmodel.h @@ -0,0 +1,40 @@ +#ifndef ANYPROFILELISTMODEL_H +#define ANYPROFILELISTMODEL_H + +#include "followslistmodel.h" + +class AnyProfileListModel : public FollowsListModel +{ + Q_OBJECT + + Q_PROPERTY(QString targetUri READ targetUri WRITE setTargetUri NOTIFY targetUriChanged) + Q_PROPERTY(AnyProfileListModelType type READ type WRITE setType NOTIFY typeChanged) + +public: + explicit AnyProfileListModel(QObject *parent = nullptr); + + enum AnyProfileListModelType { + Like, + Repost, + }; + Q_ENUM(AnyProfileListModelType); + + QString targetUri() const; + void setTargetUri(const QString &newTargetUri); + AnyProfileListModelType type() const; + void setType(const AnyProfileListModelType &newType); + +public slots: + Q_INVOKABLE void getLatest(); + Q_INVOKABLE void getNext(); + +signals: + void targetUriChanged(); + void typeChanged(); + +private: + QString m_targetUri; + AnyProfileListModelType m_type; +}; + +#endif // ANYPROFILELISTMODEL_H diff --git a/app/qtquick/followslistmodel.cpp b/app/qtquick/followslistmodel.cpp index 8ba94243..5864dfc2 100644 --- a/app/qtquick/followslistmodel.cpp +++ b/app/qtquick/followslistmodel.cpp @@ -67,6 +67,19 @@ void FollowsListModel::remove(const QString &did) endRemoveRows(); } +void FollowsListModel::clear() +{ + if (m_didList.isEmpty()) + return; + + beginRemoveRows(QModelIndex(), 0, m_didList.count() - 1); + m_didList.clear(); + m_profileHash.clear(); + m_formattedDescriptionHash.clear(); + m_cursor.clear(); + endRemoveRows(); +} + int FollowsListModel::indexOf(const QString &cid) const { Q_UNUSED(cid) diff --git a/app/qtquick/followslistmodel.h b/app/qtquick/followslistmodel.h index 3d6dc3d7..88fb3d5c 100644 --- a/app/qtquick/followslistmodel.h +++ b/app/qtquick/followslistmodel.h @@ -35,6 +35,7 @@ class FollowsListModel : public AtpAbstractListModel Q_INVOKABLE QVariant item(int row, FollowsListModel::FollowsListModelRoles role) const; Q_INVOKABLE void remove(const QString &did); + Q_INVOKABLE void clear(); virtual Q_INVOKABLE int indexOf(const QString &cid) const; virtual Q_INVOKABLE QString getRecordText(const QString &cid); diff --git a/app/qtquick/qtquick.pri b/app/qtquick/qtquick.pri index 121beb2b..2daa3f74 100644 --- a/app/qtquick/qtquick.pri +++ b/app/qtquick/qtquick.pri @@ -4,6 +4,7 @@ INCLUDEPATH += $$PWD SOURCES += \ $$PWD/accountlistmodel.cpp \ $$PWD/anyfeedlistmodel.cpp \ + $$PWD/anyprofilelistmodel.cpp \ $$PWD/atpabstractlistmodel.cpp \ $$PWD/authorfeedlistmodel.cpp \ $$PWD/columnlistmodel.cpp \ @@ -32,6 +33,7 @@ SOURCES += \ HEADERS += \ $$PWD/accountlistmodel.h \ $$PWD/anyfeedlistmodel.h \ + $$PWD/anyprofilelistmodel.h \ $$PWD/atpabstractlistmodel.h \ $$PWD/authorfeedlistmodel.h \ $$PWD/columnlistmodel.h \ diff --git a/lib/atprotocol/app/bsky/feed/appbskyfeedgetlikes.cpp b/lib/atprotocol/app/bsky/feed/appbskyfeedgetlikes.cpp new file mode 100644 index 00000000..15f8ab1e --- /dev/null +++ b/lib/atprotocol/app/bsky/feed/appbskyfeedgetlikes.cpp @@ -0,0 +1,49 @@ +#include "appbskyfeedgetlikes.h" +#include "atprotocol/lexicons_func.h" + +#include +#include +#include + +namespace AtProtocolInterface { + +AppBskyFeedGetLikes::AppBskyFeedGetLikes(QObject *parent) : AccessAtProtocol { parent } { } + +const QList *AppBskyFeedGetLikes::likes() const +{ + return &m_likes; +} + +void AppBskyFeedGetLikes::getLikes(const QString &uri, const QString &cid, const int limit, + const QString &cursor) +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("uri"), uri); + if (limit > 0) { + query.addQueryItem(QStringLiteral("limit"), QString::number(limit)); + } + if (!cursor.isEmpty()) { + query.addQueryItem(QStringLiteral("cursor"), cursor); + } + + get(QStringLiteral("xrpc/app.bsky.feed.getLikes"), query); +} + +void AppBskyFeedGetLikes::parseJson(bool success, const QString reply_json) +{ + QJsonDocument json_doc = QJsonDocument::fromJson(reply_json.toUtf8()); + if (json_doc.isEmpty()) { + success = false; + } else { + setCursor(json_doc.object().value("cursor").toString()); + for (const auto &obj : json_doc.object().value("likes").toArray()) { + AtProtocolType::AppBskyFeedGetLikes::Like like; + AtProtocolType::AppBskyFeedGetLikes::copyLike(obj.toObject(), like); + m_likes.append(like); + } + } + + emit finished(success); +} + +} diff --git a/lib/atprotocol/app/bsky/feed/appbskyfeedgetlikes.h b/lib/atprotocol/app/bsky/feed/appbskyfeedgetlikes.h new file mode 100644 index 00000000..2e6ec159 --- /dev/null +++ b/lib/atprotocol/app/bsky/feed/appbskyfeedgetlikes.h @@ -0,0 +1,26 @@ +#ifndef APPBSKYFEEDGETLIKES_H +#define APPBSKYFEEDGETLIKES_H + +#include "atprotocol/accessatprotocol.h" +#include "atprotocol/lexicons.h" + +namespace AtProtocolInterface { + +class AppBskyFeedGetLikes : public AccessAtProtocol +{ +public: + explicit AppBskyFeedGetLikes(QObject *parent = nullptr); + + const QList *likes() const; + + void getLikes(const QString &uri, const QString &cid, const int limit, const QString &cursor); + +private: + virtual void parseJson(bool success, const QString reply_json); + + QList m_likes; +}; + +} + +#endif // APPBSKYFEEDGETLIKES_H diff --git a/lib/atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.cpp b/lib/atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.cpp new file mode 100644 index 00000000..643dda4e --- /dev/null +++ b/lib/atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.cpp @@ -0,0 +1,51 @@ +#include "appbskyfeedgetrepostedby.h" +#include "atprotocol/lexicons_func.h" + +#include +#include +#include + +namespace AtProtocolInterface { + +AppBskyFeedGetRepostedBy::AppBskyFeedGetRepostedBy(QObject *parent) + : AccessAtProtocol { parent } { } + +const QList * +AppBskyFeedGetRepostedBy::profileViewList() const +{ + return &m_profileViewList; +} + +void AppBskyFeedGetRepostedBy::getRepostedBy(const QString &uri, const QString &cid, + const int limit, const QString &cursor) +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("uri"), uri); + if (limit > 0) { + query.addQueryItem(QStringLiteral("limit"), QString::number(limit)); + } + if (!cursor.isEmpty()) { + query.addQueryItem(QStringLiteral("cursor"), cursor); + } + + get(QStringLiteral("xrpc/app.bsky.feed.getRepostedBy"), query); +} + +void AppBskyFeedGetRepostedBy::parseJson(bool success, const QString reply_json) +{ + QJsonDocument json_doc = QJsonDocument::fromJson(reply_json.toUtf8()); + if (json_doc.isEmpty()) { + success = false; + } else { + setCursor(json_doc.object().value("cursor").toString()); + for (const auto &obj : json_doc.object().value("repostedBy").toArray()) { + AtProtocolType::AppBskyActorDefs::ProfileView profile; + AtProtocolType::AppBskyActorDefs::copyProfileView(obj.toObject(), profile); + m_profileViewList.append(profile); + } + } + + emit finished(success); +} + +} diff --git a/lib/atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.h b/lib/atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.h new file mode 100644 index 00000000..0c48a54b --- /dev/null +++ b/lib/atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.h @@ -0,0 +1,27 @@ +#ifndef APPBSKYFEEDGETREPOSTEDBY_H +#define APPBSKYFEEDGETREPOSTEDBY_H + +#include "atprotocol/accessatprotocol.h" +#include "atprotocol/lexicons.h" + +namespace AtProtocolInterface { + +class AppBskyFeedGetRepostedBy : public AccessAtProtocol +{ +public: + explicit AppBskyFeedGetRepostedBy(QObject *parent = nullptr); + + const QList *profileViewList() const; + + void getRepostedBy(const QString &uri, const QString &cid, const int limit, + const QString &cursor); + +private: + virtual void parseJson(bool success, const QString reply_json); + + QList m_profileViewList; +}; + +} + +#endif // APPBSKYFEEDGETREPOSTEDBY_H diff --git a/lib/lib.pri b/lib/lib.pri index 45adbd91..f4b9d578 100644 --- a/lib/lib.pri +++ b/lib/lib.pri @@ -12,8 +12,10 @@ SOURCES += \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeed.cpp \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.cpp \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerators.cpp \ + $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetlikes.cpp \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetposts.cpp \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetpostthread.cpp \ + $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.cpp \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgettimeline.cpp \ $$PWD/atprotocol/app/bsky/graph/appbskygraphgetfollowers.cpp \ $$PWD/atprotocol/app/bsky/graph/appbskygraphgetfollows.cpp \ @@ -47,8 +49,10 @@ HEADERS += \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeed.h \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.h \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerators.h \ + $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetlikes.h \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetposts.h \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetpostthread.h \ + $$PWD/atprotocol/app/bsky/feed/appbskyfeedgetrepostedby.h \ $$PWD/atprotocol/app/bsky/feed/appbskyfeedgettimeline.h \ $$PWD/atprotocol/app/bsky/graph/appbskygraphgetfollowers.h \ $$PWD/atprotocol/app/bsky/graph/appbskygraphgetfollows.h \ diff --git a/tests/hagoromo_test/hagoromo_test.qrc b/tests/hagoromo_test/hagoromo_test.qrc index faa19aba..48473224 100644 --- a/tests/hagoromo_test/hagoromo_test.qrc +++ b/tests/hagoromo_test/hagoromo_test.qrc @@ -27,5 +27,7 @@ response/timeline/next/2nd/xrpc/app.bsky.actor.getPreferences response/timeline/next/2nd/xrpc/app.bsky.feed.getTimeline response/generator/xrpc/app.bsky.feed.getFeedGenerator + response/anyprofile/xrpc/app.bsky.feed.getLikes + response/anyprofile/xrpc/app.bsky.feed.getRepostedBy diff --git a/tests/hagoromo_test/response/anyprofile/xrpc/app.bsky.feed.getLikes b/tests/hagoromo_test/response/anyprofile/xrpc/app.bsky.feed.getLikes new file mode 100644 index 00000000..ece8f9a4 --- /dev/null +++ b/tests/hagoromo_test/response/anyprofile/xrpc/app.bsky.feed.getLikes @@ -0,0 +1,98 @@ +{ + "uri": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.post/3k6tpw4xr4d27", + "cursor": "1694130477596::bafyreie3732qmxxyvyup3fdshrwu4tgrzavppp3arcudiwkwzc3q4of2tu", + "likes": [ + { + "createdAt": "2023-09-08T08:52:10.115Z", + "indexedAt": "2023-09-08T08:52:10.585Z", + "actor": { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "displayName": "hoge", + "description": "description of hoge", + "avatar": "https://av-cdn.bsky.app/img/avatar/plain/did:plc:l4fsx4ujos7uw7n4ijq2ulgs/hoge@jpeg", + "indexedAt": "2023-08-26T14:42:20.929Z", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + } + }, + { + "createdAt": "2023-09-08T00:16:45.937Z", + "indexedAt": "2023-09-08T00:16:46.244Z", + "actor": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle": "ioriayane2.bsky.social", + "displayName": "fuga", + "description": "description of fuga", + "avatar": "https://av-cdn.bsky.app/img/avatar/plain/did:plc:mqxsuw5b5rhpwo4lw6iwlid5/fuga@jpeg", + "indexedAt": "2023-08-27T10:19:47.742Z", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.graph.follow/3jx4lj4udu22t", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.follow/aaaaaaaaaaaa" + }, + "labels": [] + } + }, + { + "createdAt": "2023-09-08T00:15:38.086Z", + "indexedAt": "2023-09-08T00:15:39.848Z", + "actor": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid6", + "handle": "ioriayane3.bsky.social", + "displayName": "foo", + "description": "description of foo", + "avatar": "https://av-cdn.bsky.app/img/avatar/plain/did:plc:mqxsuw5b5rhpwo4lw6iwlid5/foo@jpeg", + "indexedAt": "2023-08-27T10:19:47.742Z", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.graph.follow/3jx4lj4udu22t", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid6/app.bsky.graph.follow/aaaaaaaaaaaa" + }, + "labels": [] + } + }, + { + "createdAt": "2023-09-08T00:15:38.086Z", + "indexedAt": "2023-09-08T00:15:39.848Z", + "actor": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid7", + "handle": "ioriayane4.bsky.social", + "displayName": "bar", + "description": "description of bar", + "avatar": "https://av-cdn.bsky.app/img/avatar/plain/did:plc:mqxsuw5b5rhpwo4lw6iwlid5/bar@jpeg", + "indexedAt": "2023-08-27T10:19:47.742Z", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.graph.follow/3jx4lj4udu22t", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid7/app.bsky.graph.follow/aaaaaaaaaaaa" + }, + "labels": [] + } + }, + { + "createdAt": "2023-09-08T00:14:05.525Z", + "indexedAt": "2023-09-08T00:14:06.073Z", + "actor": { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid8", + "handle": "ioriayane5.bsky.social", + "displayName": "charry", + "description": "description of charry", + "avatar": "https://av-cdn.bsky.app/img/avatar/plain/did:plc:mqxsuw5b5rhpwo4lw6iwlid5/charry@jpeg", + "indexedAt": "2023-08-27T10:19:47.742Z", + "viewer": { + "muted": false, + "blockedBy": false, + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid8/app.bsky.graph.follow/aaaaaaaaaaaa" + }, + "labels": [] + } + } + ] +} \ No newline at end of file diff --git a/tests/hagoromo_test/response/anyprofile/xrpc/app.bsky.feed.getRepostedBy b/tests/hagoromo_test/response/anyprofile/xrpc/app.bsky.feed.getRepostedBy new file mode 100644 index 00000000..22d64cb3 --- /dev/null +++ b/tests/hagoromo_test/response/anyprofile/xrpc/app.bsky.feed.getRepostedBy @@ -0,0 +1,34 @@ +{ + "uri": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.post/3k6tpw4xr4d27", + "repostedBy": [ + { + "did": "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", + "handle": "ioriayane.bsky.social", + "displayName": "hoge", + "description": "description of hoge", + "avatar": "https://av-cdn.bsky.app/img/avatar/plain/did:plc:l4fsx4ujos7uw7n4ijq2ulgs/hoge@jpeg", + "indexedAt": "2023-08-26T14:42:20.929Z", + "viewer": { + "muted": false, + "blockedBy": false + }, + "labels": [] + }, + { + "did": "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle": "ioriayane2.bsky.social", + "displayName": "fuga", + "description": "description of fuga", + "avatar": "https://av-cdn.bsky.app/img/avatar/plain/did:plc:mqxsuw5b5rhpwo4lw6iwlid5/bafkreighbwf3lntsbbiaxti42o4tx3i4rpehjomyxw6rcniyo3exylh7ge@jpeg", + "indexedAt": "2023-08-27T10:19:47.742Z", + "viewer": { + "muted": false, + "blockedBy": false, + "following": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.graph.follow/3jx4lj4udu22t", + "followedBy": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.follow/aaaaaaaaaaaa" + }, + "labels": [] + } + ], + "cursor": "1694131969065::bafyreie55t7twkdtujjvhsywd654qxjl5dogk4pehll4qzwswgyxm6ar4y" +} diff --git a/tests/hagoromo_test/tst_hagoromo_test.cpp b/tests/hagoromo_test/tst_hagoromo_test.cpp index d2cae4d3..9d3b8c4b 100644 --- a/tests/hagoromo_test/tst_hagoromo_test.cpp +++ b/tests/hagoromo_test/tst_hagoromo_test.cpp @@ -11,6 +11,7 @@ #include "userprofile.h" #include "tools/qstringex.h" #include "feedgeneratorlink.h" +#include "anyprofilelistmodel.h" class hagoromo_test : public QObject { @@ -39,6 +40,7 @@ private slots: void test_NotificationListModel_warn(); void test_TimelineListModel_next(); void test_FeedGeneratorLink(); + void test_AnyProfileListModel(); private: WebServer m_mockServer; @@ -1255,6 +1257,34 @@ void hagoromo_test::test_FeedGeneratorLink() QVERIFY(link.likeCount() == 9); } +void hagoromo_test::test_AnyProfileListModel() +{ + AnyProfileListModel model; + model.setAccount(m_service + "/anyprofile", QString(), QString(), QString(), "dummy", + QString()); + model.setTargetUri("at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.post/3k6tpw4xr4d27"); + + model.setType(AnyProfileListModel::AnyProfileListModelType::Like); + { + QSignalSpy spy(&model, SIGNAL(runningChanged())); + model.getLatest(); + spy.wait(); + QVERIFY2(spy.count() == 2, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + } + + QVERIFY(model.rowCount() == 5); + + model.setType(AnyProfileListModel::AnyProfileListModelType::Repost); + { + QSignalSpy spy(&model, SIGNAL(runningChanged())); + model.getLatest(); + spy.wait(); + QVERIFY2(spy.count() == 2, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + } + + QVERIFY(model.rowCount() == 2); +} + void hagoromo_test::test_RecordOperatorCreateRecord(const QByteArray &body) { QJsonDocument json_doc = QJsonDocument::fromJson(body); From 9ca80026054ecef01efab6e8083ac1ba6ff16164 Mon Sep 17 00:00:00 2001 From: Takayuki Orito Date: Sat, 9 Sep 2023 23:05:20 +0900 Subject: [PATCH 10/10] =?UTF-8?q?=E3=83=90=E3=83=BC=E3=82=B8=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E6=83=85=E5=A0=B1=E6=9B=B4=E6=96=B0=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.cpp b/app/main.cpp index b3a59d5c..115b2a2d 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -44,7 +44,7 @@ int main(int argc, char *argv[]) app.setOrganizationName(QStringLiteral("relog")); app.setOrganizationDomain(QStringLiteral("hagoromo.relog.tech")); app.setApplicationName(QStringLiteral("Hagoromo")); - app.setApplicationVersion(QStringLiteral("0.10.0")); + app.setApplicationVersion(QStringLiteral("0.11.0")); #ifndef HAGOROMO_RELEASE_BUILD app.setApplicationVersion(app.applicationVersion() + "d"); #endif