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 @@
アカウント管理
-
+
+
+ メインに設定
+
+
+
+
+ コンテンツフィルター
+
+
+
+
+ アカウントを削除
+
+
+
閉じる
@@ -47,6 +62,32 @@
追加
+
+ AltEditDialog
+
+
+
+ キャンセル
+
+
+
+
+ 追加
+
+
+
+ AnyProfileListView
+
+
+
+ いいねしたアカウント
+
+
+
+
+ リポストしたアカウント
+
+
ColumnListModel
@@ -171,57 +212,57 @@
ColumnView
-
+
ホーム
-
+
通知
-
+
検索(ポスト)
-
+
検索(ユーザー)
-
+
フィード
-
+
ユーザー
-
+
不明
-
+
左へ移動
-
+
右へ移動
-
+
削除
-
+
設定
@@ -419,12 +460,12 @@
検索
-
+
キャンセル
-
+
追加
@@ -1435,41 +1476,53 @@
PostControls
-
+
リポスト
-
+
引用
-
-
+
+
翻訳
-
-
+
+
ポストをコピー
-
-
+
+
公式で開く
-
+
+
+
+ リポストしたアカウント
+
+
+
+
+
+ いいねしたアカウント
+
+
+
ポストを削除
-
-
+
+
ポストを通報
@@ -1477,12 +1530,12 @@
PostDelegate
-
+
ミュートしているアカウントのポスト
-
+
ブロック済み
@@ -1502,22 +1555,26 @@
リンクカード
-
- リンクカードのURL
+ リンクカードのURL
-
+
+
+ リンクカードかフィードカードのURL
+
+
+
キャンセル
-
+
ポスト
-
+
コンテンツの選択
@@ -1525,12 +1582,12 @@
PostThreadView
-
+
ポストスレッド
-
+
閲覧注意な引用
@@ -1538,22 +1595,22 @@
ProfileListView
-
+
フォロー中
-
+
フォローする
-
+
あなたをフォロー中
-
+
ミュート中
@@ -1561,112 +1618,112 @@
ProfileView
-
+
プロフィールを編集
-
+
フォロー中
-
+
フォローする
-
+
プロフィール
-
+
あなたをフォロー中
-
+
フォロー
-
+
フォロワー
-
+
ポスト
-
+
メンションを送る
-
+
ハンドルをコピー
-
+
DIDをコピー
-
+
新しいカラムで開く
-
+
公式で開く
-
+
ミュート解除
-
+
ミュート
-
+
ブロック解除
-
+
ブロック
-
+
通報
-
+
ブロックしたアカウント
-
+
ミュートしたアカウント
-
+
このアカウントに設定されたラベル :
-
+
あなたをブロックしているアカウント
@@ -2114,7 +2171,7 @@
TimelineView
-
+
閲覧注意な引用
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);