diff --git a/app/app.pro b/app/app.pro index 992a6a86..4e7cd673 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 \ @@ -36,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 \ @@ -47,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 75c685b4..f45a85ac 100644 Binary files a/app/i18n/qt_ja_JP.qm and b/app/i18n/qt_ja_JP.qm differ diff --git a/app/i18n/qt_ja_JP.ts b/app/i18n/qt_ja_JP.ts index 674618b9..241c5aad 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,32 @@ 追加 + + AltEditDialog + + + Cancel + キャンセル + + + + Add + 追加 + + + + AnyProfileListView + + + Liked by + いいねしたアカウント + + + + Reposted by + リポストしたアカウント + + ColumnListModel @@ -171,57 +212,57 @@ ColumnView - + Home ホーム - + Notifications 通知 - + Search posts 検索(ポスト) - + Search users 検索(ユーザー) - + Feed フィード - + User ユーザー - + Unknown 不明 - + Move to left 左へ移動 - + Move to right 右へ移動 - + Delete column 削除 - + Settings 設定 @@ -419,12 +460,12 @@ 検索 - + Cancel キャンセル - + Add 追加 @@ -1435,41 +1476,53 @@ PostControls - + Repost リポスト - + Quote 引用 - - + + Translate 翻訳 - - + + Copy post text ポストをコピー - - + + Open in Official 公式で開く - + + + Reposted by + リポストしたアカウント + + + + + Liked by + いいねしたアカウント + + + Delete post ポストを削除 - - + + Report post ポストを通報 @@ -1477,12 +1530,12 @@ PostDelegate - + Post from an account you muted. ミュートしているアカウントのポスト - + blocked ブロック済み @@ -1502,22 +1555,26 @@ リンクカード - Link card URL - リンクカードのURL + リンクカードのURL - + + Link card URL or custom feed URL + リンクカードかフィードカードのURL + + + Cancel キャンセル - + Post ポスト - + Select contents コンテンツの選択 @@ -1525,12 +1582,12 @@ PostThreadView - + Post thread ポストスレッド - + Quoted content warning 閲覧注意な引用 @@ -1538,22 +1595,22 @@ ProfileListView - + Following フォロー中 - + Follow フォローする - + Follows you あなたをフォロー中 - + Muted user ミュート中 @@ -1561,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 あなたをブロックしているアカウント @@ -2114,7 +2171,7 @@ TimelineView - + Quoted content warning 閲覧注意な引用 diff --git a/app/main.cpp b/app/main.cpp index 1fdcd752..115b2a2d 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -22,12 +22,14 @@ #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" #include "qtquick/systemtool.h" #include "qtquick/externallink.h" #include "qtquick/reporter.h" +#include "qtquick/feedgeneratorlink.h" int main(int argc, char *argv[]) { @@ -42,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 @@ -82,11 +84,15 @@ 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"); 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/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/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/dialogs/PostDialog.qml b/app/qml/dialogs/PostDialog.qml index 12968ead..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 @@ -67,7 +68,9 @@ Dialog { postText.clear() embedImagePreview.embedImages = [] + embedImagePreview.embedAlts = [] externalLink.clear() + feedGeneratorLink.clear() addingExternalLinkUrlText.text = "" } @@ -102,6 +105,9 @@ Dialog { ExternalLink { id: externalLink } + FeedGeneratorLink { + id: feedGeneratorLink + } ColumnLayout { id: mainLayout @@ -226,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 @@ -249,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() + } } } ] @@ -269,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 @@ -276,12 +309,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 +344,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 && 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: { @@ -429,8 +492,11 @@ 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) + createRecord.setImages(embedImagePreview.embedImages, embedImagePreview.embedAlts) createRecord.postWithImages() }else{ createRecord.post() @@ -461,6 +527,7 @@ Dialog { return } var new_images = embedImagePreview.embedImages + var new_alts = embedImagePreview.embedAlts for(var i=0; i= 4){ break @@ -469,8 +536,10 @@ Dialog { continue; } new_images.push(files[i]) + new_alts.push("") } embedImagePreview.embedImages = new_images + embedImagePreview.embedAlts = new_alts } property string prevFolder } @@ -482,4 +551,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 381a0664..5a3f4a10 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(){ @@ -263,8 +260,8 @@ ApplicationWindow { columnManageModel.append(account_uuid, 5, false, 300000, 350, handle, did) scrollView.showRightMost() } - onRequestViewImages: (index, paths) => imageFullView.open(index, paths) - onRequestViewGeneratorFeed: (account_uuid, name, uri) => { + onRequestViewImages: (index, paths, alts) => imageFullView.open(index, paths, alts) + 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/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/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/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/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/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 0a3a0a4b..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" @@ -37,8 +38,8 @@ 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 requestViewGeneratorFeed(string account_uuid, string name, string uri) + signal requestViewImages(int index, var paths, var alts) + 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) @@ -76,10 +77,12 @@ 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) + 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 @@ -110,8 +113,10 @@ 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 }) + 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 @@ -132,9 +137,11 @@ 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) + 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 @@ -163,9 +170,11 @@ 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) + 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 @@ -201,9 +210,11 @@ 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) + 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 @@ -248,9 +259,11 @@ 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) + 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 @@ -280,14 +293,51 @@ 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) + 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/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 fa33c86b..c8b5e399 100644 --- a/app/qml/view/NotificationListView.qml +++ b/app/qml/view/NotificationListView.qml @@ -26,9 +26,11 @@ 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 requestViewFeedGenerator(string name, string uri) + signal requestViewLikedBy(string uri) + signal requestViewRepostedBy(string uri) signal requestReportPost(string uri, string cid) ListView { @@ -40,6 +42,12 @@ ScrollView { id: systemTool } + onMovementEnded: { + if(atYEnd){ + rootListView.model.getNext() + } + } + header: ItemDelegate { width: rootListView.width height: AdjustedValues.h24 @@ -91,7 +99,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 @@ -99,14 +108,15 @@ 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) -// 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 @@ -124,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 97fefa51..b5f52166 100644 --- a/app/qml/view/PostThreadView.qml +++ b/app/qml/view/PostThreadView.qml @@ -25,9 +25,11 @@ 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 requestViewFeedGenerator(string name, string uri) + signal requestViewLikedBy(string uri) + signal requestViewRepostedBy(string uri) signal requestReportPost(string uri, string cid) signal back() @@ -136,7 +138,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 +156,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) @@ -162,12 +166,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 : [] @@ -194,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/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/ProfileView.qml b/app/qml/view/ProfileView.qml index 9ee2ec12..f037ef21 100644 --- a/app/qml/view/ProfileView.qml +++ b/app/qml/view/ProfileView.qml @@ -35,10 +35,12 @@ 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 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) @@ -420,13 +422,15 @@ 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) } } - onRequestViewGeneratorFeed: (name, uri) => profileView.requestViewGeneratorFeed(name, uri) + 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 } @@ -449,13 +453,15 @@ 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) } } - onRequestViewGeneratorFeed: (name, uri) => profileView.requestViewGeneratorFeed(name, uri) + 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 } @@ -478,13 +484,15 @@ 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) } } - onRequestViewGeneratorFeed: (name, uri) => profileView.requestViewGeneratorFeed(name, uri) + 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 } @@ -507,13 +515,15 @@ 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) } } - onRequestViewGeneratorFeed: (name, uri) => profileView.requestViewGeneratorFeed(name, uri) + 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 b08621e0..b58df161 100644 --- a/app/qml/view/TimelineView.qml +++ b/app/qml/view/TimelineView.qml @@ -27,9 +27,11 @@ 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 requestViewFeedGenerator(string name, string uri) + signal requestViewLikedBy(string uri) + signal requestViewRepostedBy(string uri) signal requestReportPost(string uri, string cid) @@ -39,6 +41,12 @@ ScrollView { anchors.rightMargin: parent.ScrollBar.vertical.width spacing: 5 + onMovementEnded: { + if(atYEnd){ + rootListView.model.getNext() + } + } + SystemTool { id: systemTool } @@ -101,7 +109,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") @@ -118,7 +127,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) @@ -127,12 +137,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 @@ -153,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/accountlistmodel.cpp b/app/qtquick/accountlistmodel.cpp index 1377d786..cb10281d 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,38 @@ 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].email = session->email(); + 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,17 +387,20 @@ 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(); 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; - 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 +408,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/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/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/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/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/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..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) @@ -101,6 +114,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 +141,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..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); @@ -45,6 +46,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..b46923fd 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); @@ -243,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 @@ -255,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] @@ -263,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] @@ -272,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] @@ -281,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] @@ -290,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] @@ -373,6 +391,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 +483,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); }); } @@ -547,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"; @@ -564,15 +675,16 @@ QHash NotificationListModel::roleNames() const roles[QuoteRecordRecordTextRole] = "quoteRecordRecordText"; roles[QuoteRecordEmbedImagesRole] = "quoteRecordEmbedImages"; roles[QuoteRecordEmbedImagesFullRole] = "quoteRecordEmbedImagesFull"; + roles[QuoteRecordEmbedImagesAltRole] = "quoteRecordEmbedImagesAlt"; 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 4dfb5955..9ded1490 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,15 +61,16 @@ class NotificationListModel : public AtpAbstractListModel QuoteRecordRecordTextRole, QuoteRecordEmbedImagesRole, QuoteRecordEmbedImagesFullRole, + QuoteRecordEmbedImagesAltRole, QuoteRecordIsRepostedRole, QuoteRecordIsLikedRole, - HasGeneratorFeedRole, - GeneratorFeedUriRole, - GeneratorFeedCreatorHandleRole, - GeneratorFeedDisplayNameRole, - GeneratorFeedLikeCountRole, - GeneratorFeedAvatarRole, + HasFeedGeneratorRole, + FeedGeneratorUriRole, + FeedGeneratorCreatorHandleRole, + FeedGeneratorDisplayNameRole, + FeedGeneratorLikeCountRole, + FeedGeneratorAvatarRole, UserFilterMatchedRole, UserFilterMessageRole, @@ -103,6 +105,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/qtquick.pri b/app/qtquick/qtquick.pri index 93d7ed32..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 \ @@ -12,6 +13,7 @@ SOURCES += \ $$PWD/customfeedlistmodel.cpp \ $$PWD/encryption.cpp \ $$PWD/externallink.cpp \ + $$PWD/feedgeneratorlink.cpp \ $$PWD/feedgeneratorlistmodel.cpp \ $$PWD/feedtypelistmodel.cpp \ $$PWD/followerslistmodel.cpp \ @@ -31,6 +33,7 @@ SOURCES += \ HEADERS += \ $$PWD/accountlistmodel.h \ $$PWD/anyfeedlistmodel.h \ + $$PWD/anyprofilelistmodel.h \ $$PWD/atpabstractlistmodel.h \ $$PWD/authorfeedlistmodel.h \ $$PWD/columnlistmodel.h \ @@ -41,6 +44,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 4c82b311..62f04c3e 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,15 @@ 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::setFeedGeneratorLink(const QString &uri, const QString &cid) +{ + m_feedGeneratorLinkUri = uri; + m_feedGeneratorLinkCid = cid; } void RecordOperator::setSelfLabels(const QStringList &labels) @@ -102,6 +117,8 @@ void RecordOperator::clear() m_externalLinkUri.clear(); m_externalLinkTitle.clear(); m_externalLinkDescription.clear(); + m_feedGeneratorLinkUri.clear(); + m_feedGeneratorLinkCid.clear(); } void RecordOperator::post() @@ -130,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); }); @@ -142,7 +160,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 +174,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..75102e90 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,10 +28,11 @@ 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); + Q_INVOKABLE void setFeedGeneratorLink(const QString &uri, const QString &cid); Q_INVOKABLE void setSelfLabels(const QStringList &labels); Q_INVOKABLE void clear(); @@ -64,13 +71,15 @@ 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; 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 7ac17ded..6cb9e61b 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) @@ -92,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 @@ -100,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 @@ -248,6 +254,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 +269,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()) @@ -370,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"; @@ -386,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"; @@ -394,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"; @@ -477,6 +512,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) { @@ -564,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, + 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, false); + 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 f026bbb5..dfc11ae5 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, @@ -59,12 +61,12 @@ class TimelineListModel : public AtpAbstractListModel ExternalLinkDescriptionRole, ExternalLinkThumbRole, - HasGeneratorFeedRole, - GeneratorFeedUriRole, - GeneratorFeedCreatorHandleRole, - GeneratorFeedDisplayNameRole, - GeneratorFeedLikeCountRole, - GeneratorFeedAvatarRole, + HasFeedGeneratorRole, + FeedGeneratorUriRole, + FeedGeneratorCreatorHandleRole, + FeedGeneratorDisplayNameRole, + FeedGeneratorLikeCountRole, + FeedGeneratorAvatarRole, HasReplyRole, ReplyRootCidRole, @@ -99,6 +101,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 +111,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/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/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/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/comatprotorepocreaterecord.cpp b/lib/atprotocol/com/atproto/repo/comatprotorepocreaterecord.cpp index e27359f3..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)) { @@ -87,7 +95,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); } @@ -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/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/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) diff --git a/lib/lib.pri b/lib/lib.pri index 78a578d4..f4b9d578 100644 --- a/lib/lib.pri +++ b/lib/lib.pri @@ -10,9 +10,12 @@ 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/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 \ @@ -44,9 +47,12 @@ 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/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/lib/tools/opengraphprotocol.cpp b/lib/tools/opengraphprotocol.cpp index c7c2aa49..db4db0c1 100644 --- a/lib/tools/opengraphprotocol.cpp +++ b/lib/tools/opengraphprotocol.cpp @@ -115,53 +115,52 @@ 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); 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; - } 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; } @@ -169,119 +168,172 @@ QString OpenGraphProtocol::extractCharset(const QString &data) const { QString charset = "utf-8"; - QString errorMsg; - int errorLine; - int errorColumn; - QDomDocument doc; + QRegularExpressionMatch match = m_rxMeta.match(data); + if (match.capturedTexts().isEmpty()) + return charset; - 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"); - - 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; + QDomDocument doc; + QString result; + int pos; + while ((pos = match.capturedStart()) != -1) { + 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"); + 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; } -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 result; - QString temp; int pos; + while ((pos = match.capturedStart()) != -1) { + QDomElement element = doc.createElement("meta"); + if (rebuildTag(match.captured(), element)) { + head.appendChild(element); + } + match = m_rxMeta.match(text, pos + match.capturedLength()); + } + + html.appendChild(head); + doc.appendChild(html); + + return; +} + +bool OpenGraphProtocol::rebuildTag(QString text, QDomElement &element) 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; + QStringList names; + QStringList values; - for (int i = 0; i < temp.length(); i++) { - c = temp.at(i); - if (state == 0) { - if (c == ' ') { - state = 1; - } - result += c; - } else if (state == 1) { - if (c == '=') { - state = 2; - in_quote = false; + text = text.trimmed(); + + int close_tag_pos = -1; + if (text.toLower().startsWith("$", QRegularExpression::CaseInsensitiveOption)); + } + 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; + names.append(""); + values.append(""); + } + result += c; + } else if (state == 1) { + if (c == '/' || c == '>') { + if (close_tag_pos > i) { + element.appendChild(element.toDocument().createTextNode( + text.mid(i + 1, close_tag_pos - (i + 1)))); } - result += c; - } else if (state == 2) { + break; + } else if (c == '=') { + state = 2; + in_quote = false; + } else { + names.last().append(c); + } + result += c; + } else if (state == 2) { + if (c == '\"') { + in_quote = true; + state = 3; + } else if (c != ' ') { + result += '\"'; + in_quote = false; + state = 3; + values.last().append(c); + } + result += c; + } else if (state == 3) { + if (in_quote) { if (c == '\"') { - in_quote = true; - state = 3; - } else if (c != ' ') { - result += '\"'; + state = 1; in_quote = false; - state = 3; + names.append(""); + values.append(""); + } else { + values.last().append(c); } - result += c; - } else if (state == 3) { - if (in_quote) { - if (c == '\"') { - state = 1; - in_quote = false; + } else { + 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 { - if (c == ' ') { - result += '\"'; - state = 1; - } + values.last().append(c); } - result += c; - } else { } + result += c; + } else { } - - result += "\n"; - - match = m_rxMeta.match(text, pos + match.capturedLength()); } - return QString("%1").arg(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 c51b2b95..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,7 +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; + void rebuildHtml(const QString &text, QDomDocument &doc) const; + bool rebuildTag(QString text, QDomElement &element) const; QRegularExpression m_rxMeta; 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)) diff --git a/tests/atprotocol_test/atprotocol_test.qrc b/tests/atprotocol_test/atprotocol_test.qrc index 21ca5ac7..b47a031a 100644 --- a/tests/atprotocol_test/atprotocol_test.qrc +++ b/tests/atprotocol_test/atprotocol_test.qrc @@ -20,5 +20,7 @@ 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 + response/xrpc/app.bsky.feed.getFeedGenerator 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 new file mode 100644 index 00000000..d9386799 --- /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/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 7a0148fc..7ffcbf8c 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); @@ -108,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"); @@ -280,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)) @@ -292,6 +296,25 @@ 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()); + QVERIFY2(ogp.thumb() + == QString("http://localhost:%1/response/ogp/images/file6.png") + .arg(QString::number(m_listenPort)), + ogp.thumb().toLocal8Bit()); + } } void atprotocol_test::test_getTimeline() @@ -695,6 +718,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 96cf15fb..48473224 100644 --- a/tests/hagoromo_test/hagoromo_test.qrc +++ b/tests/hagoromo_test/hagoromo_test.qrc @@ -22,5 +22,12 @@ 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 + 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/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/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..9d3b8c4b 100644 --- a/tests/hagoromo_test/tst_hagoromo_test.cpp +++ b/tests/hagoromo_test/tst_hagoromo_test.cpp @@ -10,6 +10,8 @@ #include "notificationlistmodel.h" #include "userprofile.h" #include "tools/qstringex.h" +#include "feedgeneratorlink.h" +#include "anyprofilelistmodel.h" class hagoromo_test : public QObject { @@ -36,6 +38,9 @@ private slots: void test_TimelineListModel_quote_hide2(); void test_TimelineListModel_quote_label(); void test_NotificationListModel_warn(); + void test_TimelineListModel_next(); + void test_FeedGeneratorLink(); + void test_AnyProfileListModel(); private: WebServer m_mockServer; @@ -1153,6 +1158,133 @@ 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_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_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);