diff --git a/.gitignore b/.gitignore index d1b22020..9e7cd6ed 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ openssl/bin openssl/certs openssl/include openssl/lib +openssl/lib64 openssl/misc openssl/private openssl/share diff --git a/app/app.pro b/app/app.pro index 13b96c5a..41319d55 100644 --- a/app/app.pro +++ b/app/app.pro @@ -203,7 +203,8 @@ win32:{ translations.path = $$install_dir/translations mac:translations.path = \ $$install_dir/$${TARGET}.app/Contents/MacOS/translations -translations.files = $$PWD/i18n/*.qm +translations.files = $$PWD/i18n/*.qm \ + $$PWD/../lib/i18n/*.qm #qmファイルが存在しないとmakefileに追加されないので注意 INSTALLS += translations diff --git a/app/i18n/app_ja.qm b/app/i18n/app_ja.qm index 76495fb4..0ea89b69 100644 Binary files a/app/i18n/app_ja.qm and b/app/i18n/app_ja.qm differ diff --git a/app/i18n/app_ja.ts b/app/i18n/app_ja.ts index b7618585..86e44293 100644 --- a/app/i18n/app_ja.ts +++ b/app/i18n/app_ja.ts @@ -9,7 +9,7 @@ アカウント管理 - + Set as main メインに設定 @@ -18,7 +18,7 @@ ポストの統計とログ - + Content filter コンテンツフィルター @@ -27,17 +27,17 @@ ミュート中 - + Muted words and tags ミュートワードの編集 - + Muted accounts ミュート中のアカウント - + Muted lists ミュート中のリスト @@ -46,32 +46,32 @@ ブロック中 - + Statistics and logs 統計とログ - + Mute ミュート - + Block ブロック - + Blocked accounts ブロック中のアカウント - + Blocked lists ブロック中のリスト - + Post interaction settings 投稿への反応の設定 @@ -80,12 +80,12 @@ リプライできるユーザー - + Remove account アカウントを削除 - + Close 閉じる @@ -273,14 +273,14 @@ AtpAbstractListModel - - + + Blocked ブロック中 - - + + Detached by author 投稿者によって切り離し済み @@ -685,137 +685,6 @@ Please recreate AppPassword in the official application. 設定 - - ConfigurableLabels - - Content hidden - 閲覧不可 - - - Moderator overrides for special cases. - 特別な場合のためのモデレーターによるオーバーライド。 - - - Content warning - 閲覧警告 - - - Legal - 法的 - - - Content removed for legal reasons. - 法的な理由で削除されたコンテンツ。 - - - Explicit Sexual Images - 露骨な性的な画像 - - - i.e. pornography - 例えばポルノ - - - Sexually Explicit - 露骨な性的表現 - - - Other Nudity - その他ヌード - - - Including non-sexual and artistic - ノンセクシャルや芸術的なものも含む - - - Nudity - ヌード - - - Basic Moderation - 基本モデレーション - - - Basic configuration independent of moderation services. - モデレーションサービスに依存しない基本設定。 - - - Pornography - ポルノ - - - Explicit sexual images. - 露骨な性的な画像。 - - - Sexually Suggestive - 性的な示唆 - - - Does not include nudity - ヌードは含まない - - - Graphic Media - グラフィックメディア - - - Explicit or potentially disturbing media. - 露骨な、あるいは不穏なメディア。 - - - Non-sexual Nudity - 性的でないヌード - - - E.g. artistic nudes. - 例:芸術的なヌード - - - Violent / Bloody - 暴力 / 流血 - - - Gore, self-harm, torture - 血糊、自傷行為、拷問 - - - Violence - 暴力 - - - Hate Group Iconography - ヘイトグループの象徴 - - - Images of terror groups, articles covering events, etc. - テロ集団の画像、事件を取り上げた記事など - - - Hate Groups - ヘイトグループ - - - Spam - スパム - - - Excessive unwanted interactions - 過剰な不要な干渉 - - - Impersonation / Scam - なりすまし / 詐欺 - - - Impersonation - なりすまし - - - Accounts falsely claiming to be people or orgs - 個人や団体を偽ったアカウント - - ContentFilterSettingDialog @@ -1007,17 +876,22 @@ Please recreate AppPassword in the official application. HashTagMenu - + Search %s posts %s を検索 - + Search %s posts by this user このユーザーによる %s を検索 - + + Copy %s + %sをコピー + + + Mute %s posts %s をミュートする @@ -2082,93 +1956,6 @@ Please recreate AppPassword in the official application. ブロック中 - - LogAccess - - Post - ポスト - - - Total number of posts - 総ポスト数 - - - Number of days posts - ポストした日数 - - - Average number of daily posts - 1日あたりの平均ポスト数 - - - Like - いいね - - - Total number of likes - 総いいね数 - - - Number of days likes - いいねした日数 - - - Average number of daily likes - 1日あたりの平均いいね数 - - - Repost - リポスト - - - Total number of reposts - 総リポスト数 - - - Number of days reposts - リポストした日数 - - - Average number of daily reposts - 1日あたりの平均リポスト数 - - - List - リスト - - - Total number of lists - リストの数 - - - Total number of list items - リストに登録している総アカウント数 - - - Other - その他 - - - Total number of follows - フォロー数 - - - Total number of who can reply - リプライ制限をした数 - - - Total number of blocks - 総ブロック数 - - - Number of registrations for list - リストに登録しているアカウント数 - - - Number of registrations for '%1' - '%1'に登録しているアカウント数 - - LogViewDialog @@ -2293,12 +2080,12 @@ Please recreate AppPassword in the official application. NotificationDelegate - + Post from an account you muted. ミュートしているアカウントのポスト - + signed up with your starter pack あなたのスターターパックで登録しました @@ -2306,7 +2093,7 @@ Please recreate AppPassword in the official application. NotificationListModel - + Post hidden by muted word ミュートワードを含む @@ -2322,83 +2109,83 @@ Please recreate AppPassword in the official application. PostControls - + Repost リポスト - + Quote 引用 - - + + Translate 翻訳 - - + + Copy post text ポストをコピー - - + + Copy url URLをコピー - - + + Open in Official 公式で開く - - + + Reposted by リポストしたアカウント - - + + Liked by いいねしたアカウント - - + + Quotes 引用したポスト - - + + Unpin this post 固定ポストを解除 - - + + Pin this post 固定ポストにする - - + + Unmute thread スレッドミュートの解除 - - + + Mute thread スレッドをミュート - + Edit interaction settings 投稿への反応の設定 @@ -2407,22 +2194,22 @@ Please recreate AppPassword in the official application. リプライできるユーザー - + Delete post ポストを削除 - + Re-attach quote 引用を再接続 - + Detach quote 引用を切断 - + Report post ポストを通報 @@ -2430,7 +2217,7 @@ Please recreate AppPassword in the official application. PostDelegate - + Post from an account you muted. ミュートしているアカウントのポスト @@ -2506,7 +2293,7 @@ Please recreate AppPassword in the official application. ポストスレッド - + Quoted content warning 閲覧注意な引用 @@ -2592,87 +2379,86 @@ Please recreate AppPassword in the official application. 登録日 : - + Send mention メンションを送る - + Send message メッセージを送る - + Copy handle ハンドルをコピー - + Copy DID DIDをコピー - + Copy Official Url 公式のURLをコピー - + Open in new col 新しいカラムで開く - + Open in Official 公式で開く - + Add/Remove from lists リストへ追加/削除 - + Unmute account ミュート解除 - + Mute account ミュート - + Unblock account ブロック解除 - + Block account ブロック - + Report account 通報 - + Account blocked ブロックしたアカウント - + Account muted ミュートしたアカウント - This account has been flagged : - このアカウントに設定されたラベル : + このアカウントに設定されたラベル : - + This account has blocked you あなたをブロックしているアカウント @@ -2810,28 +2596,28 @@ Please recreate AppPassword in the official application. - + Update list ... (%1) - + Update who can reply ... - - + + Update quote status ... - + Uploading images ... (%1/%2) - + Delete list item ... (%1) @@ -3606,7 +3392,7 @@ Why should this message be reviewed? TimelineListModel - + Post hidden by muted word ミュートワードを含む @@ -3614,7 +3400,7 @@ Why should this message be reviewed? TimelineView - + Quoted content warning 閲覧注意な引用 @@ -3627,7 +3413,7 @@ Why should this message be reviewed? - + Search posts 検索(ポスト) @@ -3641,23 +3427,33 @@ Why should this message be reviewed? リプライできるユーザーの更新中 ... - - + + Authentication error + 認証エラー + + + + Some accounts require you to log in again. + いくつかのアカウントでログインが必要です。 + + + + Updating 'Edit interaction settings' ... 投稿への反応の設定を更新中 ... - + Loading lists リストの読み込み中 - + Chat チャット - + Loading account(s) ... アカウント情報の読み込み中 ... diff --git a/app/main.cpp b/app/main.cpp index e29cd2b6..070d4e58 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -91,7 +91,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.38.0")); + app.setApplicationVersion(QStringLiteral("0.39.0")); #ifndef HAGOROMO_RELEASE_BUILD app.setApplicationVersion(app.applicationVersion() + "d"); #endif diff --git a/app/qml/parts/ExternalLinkCard.qml b/app/qml/parts/ExternalLinkCard.qml index e8466cad..fb301b27 100644 --- a/app/qml/parts/ExternalLinkCard.qml +++ b/app/qml/parts/ExternalLinkCard.qml @@ -32,7 +32,7 @@ ClickableFrame { ImageWithIndicator { id: thumbImage Layout.preferredWidth: externalLinkFrame.width - Layout.preferredHeight: thumbImage.source !== "" ? externalLinkFrame.width * 0.5 : 5 + Layout.preferredHeight: (thumbImage.source + "") !== "" ? externalLinkFrame.width * 0.5 : 5 fillMode: Image.PreserveAspectCrop } Label { diff --git a/app/qml/parts/HashTagMenu.qml b/app/qml/parts/HashTagMenu.qml index 276ab98a..0c09d27e 100644 --- a/app/qml/parts/HashTagMenu.qml +++ b/app/qml/parts/HashTagMenu.qml @@ -11,6 +11,7 @@ MenuEx { signal requestViewSearchPosts(string text) signal requestAddMutedWord(string text) + signal requestCopyTagToClipboard(string text) Action { enabled: !logMode @@ -25,6 +26,12 @@ MenuEx { text: qsTr("Search %s posts by this user").replace("%s", tagMenu.tagText) onTriggered: requestViewSearchPosts(tagMenu.tagText + " from:" + postAuthor.handle) } + Action { + enabled: !logMode + icon.source: "../images/copy.png" + text: qsTr("Copy %s").replace("%s", tagMenu.tagText) + onTriggered: requestCopyTagToClipboard(tagMenu.tagText) + } MenuSeparator {} Action { icon.source: "../images/mute.png" diff --git a/app/qml/parts/NotificationDelegate.qml b/app/qml/parts/NotificationDelegate.qml index 5cd3bded..150e52d7 100644 --- a/app/qml/parts/NotificationDelegate.qml +++ b/app/qml/parts/NotificationDelegate.qml @@ -56,13 +56,14 @@ ClickableFrame { signal requestViewProfile(string did) signal requestViewSearchPosts(string text) signal requestAddMutedWord(string text) + signal requestCopyTagToClipboard(string text) - function openLink(url){ + function openLink(url, x, y){ if(url.indexOf("did:") === 0){ requestViewProfile(url) }else if(url.indexOf("search://") === 0){ - tagMenu.x = recrdTextMouseArea.mouseX - tagMenu.y = recrdTextMouseArea.mouseY + tagMenu.x = x + tagMenu.y = y tagMenu.tagText = url.substring(9) if(tagMenu.tagText.charAt(0) !== "#"){ tagMenu.tagText = "#" + tagMenu.tagText @@ -275,13 +276,14 @@ ClickableFrame { wrapMode: Text.WrapAnywhere font.pointSize: AdjustedValues.f10 lineHeight: 1.3 - onLinkActivated: (url) => openLink(url) + onLinkActivated: (url) => openLink(url, recrdTextMouseArea.mouseX, recrdTextMouseArea.mouseY) onHoveredLinkChanged: displayLink(hoveredLink) HashTagMenu { id: tagMenu onRequestViewSearchPosts: (text) => notificationFrame.requestViewSearchPosts(text) onRequestAddMutedWord: (text) => notificationFrame.requestAddMutedWord(text) + onRequestCopyTagToClipboard: (text) => notificationFrame.requestCopyTagToClipboard(text) } } } @@ -359,7 +361,9 @@ ClickableFrame { wrapMode: Text.WrapAnywhere font.pointSize: AdjustedValues.f10 lineHeight: 1.3 - onLinkActivated: (url) => openLink(url) + onLinkActivated: (url) => openLink(url, + quoteRecordRecordText.x + quoteRecordRecordText.width / 4, + quoteRecordRecordText.y + quoteRecordRecordText.height / 2) onHoveredLinkChanged: displayLink(hoveredLink) } ImagePreview { diff --git a/app/qml/parts/PostDelegate.qml b/app/qml/parts/PostDelegate.qml index bbb79032..3dedea2b 100644 --- a/app/qml/parts/PostDelegate.qml +++ b/app/qml/parts/PostDelegate.qml @@ -29,6 +29,7 @@ ClickableFrame { property alias pinnedIndicatorLabel: pinnedIndicatorLabel property alias postAvatarImage: postAvatarImage property alias postAuthor: postAuthor + property alias authorLabels: authorLabels property alias recordText: recordText property alias contentFilterFrame: contentFilterFrame property alias contentMediaFilterFrame: contentMediaFilterFrame @@ -53,13 +54,14 @@ ClickableFrame { signal requestViewProfile(string did) signal requestViewSearchPosts(string text) signal requestAddMutedWord(string text) + signal requestCopyTagToClipboard(string text) - function openLink(url){ + function openLink(url, x, y){ if(url.indexOf("did:") === 0){ requestViewProfile(url) }else if(url.indexOf("search://") === 0){ - tagMenu.x = recrdTextMouseArea.mouseX - tagMenu.y = recrdTextMouseArea.mouseY + tagMenu.x = x + tagMenu.y = y tagMenu.tagText = url.substring(9) if(tagMenu.tagText.charAt(0) !== "#"){ tagMenu.tagText = "#" + tagMenu.tagText @@ -166,6 +168,16 @@ ClickableFrame { Layout.preferredWidth: parent.basisWidth layoutWidth: parent.basisWidth } + TagLabelLayout { + id: authorLabels + Layout.preferredWidth: parent.width + visible: count > 0 + tagSpacing: 2 + tagColor: "transparent" + tagBorderWidth: 1 + fontPointSize: AdjustedValues.f8 + } + CoverFrame { id: contentFilterFrame @@ -198,7 +210,7 @@ ClickableFrame { wrapMode: Text.WrapAnywhere font.pointSize: AdjustedValues.f10 lineHeight: 1.3 - onLinkActivated: (url) => openLink(url) + onLinkActivated: (url) => openLink(url, recrdTextMouseArea.mouseX, recrdTextMouseArea.mouseY) onHoveredLinkChanged: displayLink(hoveredLink) HashTagMenu { @@ -206,6 +218,7 @@ ClickableFrame { logMode: postFrame.logMode onRequestViewSearchPosts: (text) => postFrame.requestViewSearchPosts(text) onRequestAddMutedWord: (text) => postFrame.requestAddMutedWord(text) + onRequestCopyTagToClipboard: (text) => postFrame.requestCopyTagToClipboard(text) } } } @@ -267,7 +280,9 @@ ClickableFrame { visible: postFrame.hasQuote && quoteFilterFrame.showContent basisWidth: bodyLayout.basisWidth - onOpenLink: (url) => postFrame.openLink(url) + onOpenLink: (url) => postFrame.openLink(url, + quoteRecordFrame.x + quoteRecordFrame.width / 4, + quoteRecordFrame.y + quoteRecordFrame.height / 2) onDisplayLink: (url) => postFrame.displayLink(url) } Frame { diff --git a/app/qml/parts/TagLabelLayout.qml b/app/qml/parts/TagLabelLayout.qml index ea712d2e..c7c86472 100644 --- a/app/qml/parts/TagLabelLayout.qml +++ b/app/qml/parts/TagLabelLayout.qml @@ -3,7 +3,10 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import QtQuick.Controls.Material 2.15 +import tech.relog.hagoromo.singleton 1.0 + Item { + id: tagLabelLayout clip: true implicitHeight: { if(repeater.count === 0){ @@ -23,6 +26,7 @@ Item { property alias model: repeater.model property alias count: repeater.count property string iconSource: "../images/label.png" + property real fontPointSize: AdjustedValues.f10 Repeater { id: repeater @@ -73,6 +77,7 @@ Item { text: model.modelData source: repeater.parent.iconSource spacing: repeater.parent.tagSpacing + fontPointSize: tagLabelLayout.fontPointSize } } } diff --git a/app/qml/parts/VideoFrame.qml b/app/qml/parts/VideoFrame.qml index 9d5a2c00..9229f08b 100644 --- a/app/qml/parts/VideoFrame.qml +++ b/app/qml/parts/VideoFrame.qml @@ -33,7 +33,7 @@ ClickableFrame { id: thumbImage width: videoFrame.width height: { - if(thumbImage.source !== "") { + if((thumbImage.source + "") !== "") { if(sourceSize.width > sourceSize.height){ return (width / sourceSize.width) * sourceSize.height }else{ diff --git a/app/qml/view/NotificationListView.qml b/app/qml/view/NotificationListView.qml index 5d4b29e9..7fb86e92 100644 --- a/app/qml/view/NotificationListView.qml +++ b/app/qml/view/NotificationListView.qml @@ -84,6 +84,7 @@ ScrollView { onRequestViewProfile: (did) => notificationListView.requestViewProfile(did) onRequestViewSearchPosts: (text) => notificationListView.requestViewSearchPosts(text) onRequestAddMutedWord: (text) => notificationListView.requestAddMutedWord(text) + onRequestCopyTagToClipboard: (text) => systemTool.copyToClipboard(text) moderationFrame.visible: model.muted userFilterMatched: model.userFilterMatched diff --git a/app/qml/view/PostThreadView.qml b/app/qml/view/PostThreadView.qml index 3153db6d..bd3f4080 100644 --- a/app/qml/view/PostThreadView.qml +++ b/app/qml/view/PostThreadView.qml @@ -102,6 +102,8 @@ ColumnLayout { delegate: PostDelegate { Layout.preferredWidth: rootListView.width + property bool isBasisPost: (postThreadView.postUri === model.uri) + //自分から自分へは移動しない onClicked: (mouse) => { if(postThreadView.postUri !== model.uri){ @@ -111,6 +113,7 @@ ColumnLayout { onRequestViewProfile: (did) => postThreadView.requestViewProfile(did) onRequestViewSearchPosts: (text) => postThreadView.requestViewSearchPosts(text) onRequestAddMutedWord: (text) => postThreadView.requestAddMutedWord(text) + onRequestCopyTagToClipboard: (text) => systemTool.copyToClipboard(text) moderationFrame.visible: model.muted userFilterMatched: model.userFilterMatched @@ -128,7 +131,8 @@ ColumnLayout { postAvatarImage.onClicked: requestViewProfile(model.did) postAuthor.displayName: model.displayName postAuthor.handle: model.handle - postAuthor.indexedAt: (postThreadView.postUri === model.uri) ? "" : model.indexedAt + postAuthor.indexedAt: isBasisPost ? "" : model.indexedAt + authorLabels.model: isBasisPost ? model.authorLabels : [] recordText.text: { var text = model.recordText if(model.recordTextTranslation.length > 0){ @@ -194,7 +198,7 @@ ColumnLayout { listLinkCardFrame.creatorHandleLabel.text: model.listLinkCreatorHandle listLinkCardFrame.descriptionLabel.text: model.listLinkDescription - postInformation.visible: (postThreadView.postUri === model.uri) + postInformation.visible: isBasisPost postInformation.tagsLayout.model: postInformation.visible ? model.tags : [] postInformation.labelsLayout.model: postInformation.visible ? model.labels : [] postInformation.languagesLayout.model: postInformation.visible ? model.languages : [] diff --git a/app/qml/view/ProfileView.qml b/app/qml/view/ProfileView.qml index 32af03e8..53515914 100644 --- a/app/qml/view/ProfileView.qml +++ b/app/qml/view/ProfileView.qml @@ -362,6 +362,15 @@ ColumnLayout { tagBorderWidth: 1 model: userProfile.belongingLists } + TagLabelLayout { + Layout.preferredWidth: profileView.width - 10 + Layout.maximumWidth: profileView.width + Layout.topMargin: 5 + Layout.leftMargin: 5 + Layout.rightMargin: 5 + visible: count > 0 + model: userProfile.labels + } Label { id: descriptionLabel Layout.topMargin: 5 @@ -508,16 +517,6 @@ ColumnLayout { } ] } - IconLabelFrame { - id: moderationFrame3 - Layout.topMargin: 2 - Layout.preferredWidth: profileView.width - visible: userProfile.userFilterMatched - backgroundColor: Material.color(Material.Red) - borderWidth: 0 - iconSource: "../images/labeling.png" - labelText: qsTr("This account has been flagged : ") + userProfile.userFilterTitle - } IconLabelFrame { id: moderationFrame2 Layout.topMargin: 2 diff --git a/app/qml/view/TimelineView.qml b/app/qml/view/TimelineView.qml index a31bbcaa..f7b21c41 100644 --- a/app/qml/view/TimelineView.qml +++ b/app/qml/view/TimelineView.qml @@ -108,6 +108,7 @@ ScrollView { onRequestViewProfile: (did) => timelineView.requestViewProfile(did) onRequestViewSearchPosts: (text) => timelineView.requestViewSearchPosts(text) onRequestAddMutedWord: (text) => timelineView.requestAddMutedWord(text) + onRequestCopyTagToClipboard: (text) => systemTool.copyToClipboard(text) moderationFrame.visible: model.muted userFilterMatched: model.userFilterMatched diff --git a/app/qtquick/atpabstractlistmodel.cpp b/app/qtquick/atpabstractlistmodel.cpp index dd46f95c..7471f862 100644 --- a/app/qtquick/atpabstractlistmodel.cpp +++ b/app/qtquick/atpabstractlistmodel.cpp @@ -400,11 +400,14 @@ bool AtpAbstractListModel::getQuoteFilterMatched( return false; } -QStringList AtpAbstractListModel::getLabels( - const QList &labels) const +QStringList +AtpAbstractListModel::getLabels(const QList &labels, + bool exclude_no_unauth) const { QStringList ret; for (const auto &label : labels) { + if (exclude_no_unauth && label.val == "!no-unauthenticated") + continue; ret.append(label.val); } return ret; diff --git a/app/qtquick/atpabstractlistmodel.h b/app/qtquick/atpabstractlistmodel.h index 2a499ff6..865d496a 100644 --- a/app/qtquick/atpabstractlistmodel.h +++ b/app/qtquick/atpabstractlistmodel.h @@ -202,7 +202,8 @@ public slots: QString getContentFilterMessage(const QList &labels, const bool for_media) const; bool getQuoteFilterMatched(const AtProtocolType::AppBskyFeedDefs::PostView &post) const; - QStringList getLabels(const QList &labels) const; + QStringList getLabels(const QList &labels, + bool exclude_no_unauth = false) const; QStringList getLaunguages(const QVariant &record) const; QString getVia(const QVariant &record) const; diff --git a/app/qtquick/profile/userprofile.cpp b/app/qtquick/profile/userprofile.cpp index 78d707cf..123ac5fe 100644 --- a/app/qtquick/profile/userprofile.cpp +++ b/app/qtquick/profile/userprofile.cpp @@ -84,29 +84,16 @@ void UserProfile::getProfile(const QString &did) setBlocking(detail.viewer.blocking.contains(m_account.did)); setBlockingUri(detail.viewer.blocking); if (detail.labels.isEmpty()) { - setUserFilterMatched(false); - setUserFilterTitle(QString()); + setLabels(QStringList()); } else { - QString title; - QString val; - for (const auto &label : detail.labels) { + QStringList labels; + for (const auto &label : qAsConst(detail.labels)) { if (label.val == QStringLiteral("!no-unauthenticated")) continue; - val = label.val; - title = labelsTitle(label.val, false); + labels.append(label.val); break; } - if (val.isEmpty()) { - setUserFilterMatched(false); - setUserFilterTitle(QString()); - } else { - setUserFilterMatched(true); - if (title.isEmpty()) { - setUserFilterTitle(val); - } else { - setUserFilterTitle(title); - } - } + setLabels(labels); } setBelongingLists( ListItemsCache::getInstance()->getListNames(m_account.did, detail.did)); @@ -373,32 +360,6 @@ void UserProfile::setBlockingUri(const QString &newBlockingUri) emit blockingUriChanged(); } -bool UserProfile::userFilterMatched() const -{ - return m_userFilterMatched; -} - -void UserProfile::setUserFilterMatched(bool newUserFilterMatched) -{ - if (m_userFilterMatched == newUserFilterMatched) - return; - m_userFilterMatched = newUserFilterMatched; - emit userFilterMatchedChanged(); -} - -QString UserProfile::userFilterTitle() const -{ - return m_userFilterTitle; -} - -void UserProfile::setUserFilterTitle(const QString &newUserFilterTitle) -{ - if (m_userFilterTitle == newUserFilterTitle) - return; - m_userFilterTitle = newUserFilterTitle; - emit userFilterTitleChanged(); -} - QString UserProfile::formattedDescription() const { return m_formattedDescription; @@ -540,6 +501,19 @@ QStringList UserProfile::labelerDids() const return LabelerProvider::getInstance()->labelerDids(m_account); } +QStringList UserProfile::labels() const +{ + return m_labels; +} + +void UserProfile::setLabels(const QStringList &newLabels) +{ + if (m_labels == newLabels) + return; + m_labels = newLabels; + emit labelsChanged(); +} + QStringList UserProfile::belongingLists() const { return m_belongingLists; diff --git a/app/qtquick/profile/userprofile.h b/app/qtquick/profile/userprofile.h index b2af02b8..4091364d 100644 --- a/app/qtquick/profile/userprofile.h +++ b/app/qtquick/profile/userprofile.h @@ -47,11 +47,7 @@ class UserProfile : public QObject Q_PROPERTY(bool blocking READ blocking WRITE setBlocking NOTIFY blockingChanged) Q_PROPERTY(QString blockingUri READ blockingUri WRITE setBlockingUri NOTIFY blockingUriChanged) - Q_PROPERTY(bool userFilterMatched READ userFilterMatched WRITE setUserFilterMatched NOTIFY - userFilterMatchedChanged) - Q_PROPERTY(QString userFilterTitle READ userFilterTitle WRITE setUserFilterTitle NOTIFY - userFilterTitleChanged) - + Q_PROPERTY(QStringList labels READ labels WRITE setLabels NOTIFY labelsChanged FINAL) Q_PROPERTY(QStringList belongingLists READ belongingLists WRITE setBelongingLists NOTIFY belongingListsChanged) Q_PROPERTY(QString pinnedPost READ pinnedPost WRITE setPinnedPost NOTIFY pinnedPostChanged) @@ -107,10 +103,8 @@ class UserProfile : public QObject void setBlocking(bool newBlocking); QString blockingUri() const; void setBlockingUri(const QString &newBlockingUri); - bool userFilterMatched() const; - void setUserFilterMatched(bool newUserFilterMatched); - QString userFilterTitle() const; - void setUserFilterTitle(const QString &newUserFilterTitle); + QStringList labels() const; + void setLabels(const QStringList &newLabels); QStringList belongingLists() const; void setBelongingLists(const QStringList &newBelongingLists); @@ -149,8 +143,9 @@ class UserProfile : public QObject void blockingUriChanged(); void userFilterMatchedChanged(); void userFilterTitleChanged(); - void belongingListsChanged(); + void labelsChanged(); + void belongingListsChanged(); void formattedDescriptionChanged(); void pinnedPostChanged(); void serviceEndpointChanged(); @@ -206,6 +201,7 @@ public slots: QString m_registrationDate; QStringList m_handleHistory; bool m_associatedChatAllow; + QStringList m_labels; }; #endif // USERPROFILE_H diff --git a/app/qtquick/timeline/timelinelistmodel.cpp b/app/qtquick/timeline/timelinelistmodel.cpp index f29d4b13..f4e307b9 100644 --- a/app/qtquick/timeline/timelinelistmodel.cpp +++ b/app/qtquick/timeline/timelinelistmodel.cpp @@ -150,6 +150,8 @@ QVariant TimelineListModel::item(int row, TimelineListModelRoles role) const return current.post.author.handle; else if (role == AvatarRole) return current.post.author.avatar; + else if (role == AuthorLabelsRole) + return getLabels(current.post.author.labels, true); else if (role == MutedRole) return current.post.author.viewer.muted; else if (role == RecordTextRole) @@ -756,6 +758,7 @@ QHash TimelineListModel::roleNames() const roles[DisplayNameRole] = "displayName"; roles[HandleRole] = "handle"; roles[AvatarRole] = "avatar"; + roles[AuthorLabelsRole] = "authorLabels"; roles[MutedRole] = "muted"; roles[RecordTextRole] = "recordText"; roles[RecordTextPlainRole] = "recordTextPlain"; diff --git a/app/qtquick/timeline/timelinelistmodel.h b/app/qtquick/timeline/timelinelistmodel.h index 78186bc6..21fa01f2 100644 --- a/app/qtquick/timeline/timelinelistmodel.h +++ b/app/qtquick/timeline/timelinelistmodel.h @@ -45,6 +45,7 @@ class TimelineListModel : public AtpAbstractListModel DisplayNameRole, HandleRole, AvatarRole, + AuthorLabelsRole, MutedRole, RecordTextRole, RecordTextPlainRole, diff --git a/app/tools/translatorchanger.cpp b/app/tools/translatorchanger.cpp index 10d687ea..80ab5777 100644 --- a/app/tools/translatorchanger.cpp +++ b/app/tools/translatorchanger.cpp @@ -27,13 +27,20 @@ void TranslatorChanger::initialize(QCoreApplication *app, QQmlApplicationEngine t->deleteLater(); } t = new QTranslator(this); + if (t->load(QString("lib_%1").arg(lang), dir)) { + m_translatorLib[lang] = t; + } else { + t->deleteLater(); + } + t = new QTranslator(this); if (t->load(QString("qt_%1").arg(lang), dir)) { m_translatorSys[lang] = t; } else { t->deleteLater(); } } - qDebug() << "Loaded languages" << m_translatorApp.keys(); + qDebug() << "Loaded languages" << m_translatorApp.keys() << m_translatorLib.keys() + << m_translatorSys.keys(); } void TranslatorChanger::connect() @@ -81,6 +88,9 @@ void TranslatorChanger::change(const QString &lang) if (m_translatorApp.contains(m_currentLang)) { m_app->removeTranslator(m_translatorApp[m_currentLang]); } + if (m_translatorLib.contains(m_currentLang)) { + m_app->removeTranslator(m_translatorLib[m_currentLang]); + } if (m_translatorSys.contains(m_currentLang)) { m_app->removeTranslator(m_translatorSys[m_currentLang]); } @@ -90,6 +100,9 @@ void TranslatorChanger::change(const QString &lang) if (m_translatorApp.contains(lang)) { m_app->installTranslator(m_translatorApp[lang]); } + if (m_translatorLib.contains(lang)) { + m_app->installTranslator(m_translatorLib[lang]); + } if (m_translatorSys.contains(lang)) { m_app->installTranslator(m_translatorSys[lang]); } diff --git a/app/tools/translatorchanger.h b/app/tools/translatorchanger.h index 982bf7cc..8a8be886 100644 --- a/app/tools/translatorchanger.h +++ b/app/tools/translatorchanger.h @@ -25,6 +25,7 @@ public slots: QCoreApplication *m_app; QQmlApplicationEngine *m_engine; QHash m_translatorApp; + QHash m_translatorLib; QHash m_translatorSys; QString m_currentLang; }; diff --git a/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.cpp b/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.cpp index b628ca2f..86ce548e 100644 --- a/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.cpp +++ b/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.cpp @@ -15,7 +15,8 @@ namespace AtProtocolInterface { -ComAtprotoSyncSubscribeReposEx::ComAtprotoSyncSubscribeReposEx(QObject *parent) : QObject { parent } +ComAtprotoSyncSubscribeReposEx::ComAtprotoSyncSubscribeReposEx(QObject *parent) + : QObject { parent }, m_subscribeMode { SubScribeMode::Firehose } { qDebug().noquote() << "ComAtprotoSyncSubscribeReposEx"; @@ -31,6 +32,8 @@ ComAtprotoSyncSubscribeReposEx::ComAtprotoSyncSubscribeReposEx(QObject *parent) &ComAtprotoSyncSubscribeReposEx::onConnected); connect(&m_webSocket, &QWebSocket::binaryMessageReceived, this, &ComAtprotoSyncSubscribeReposEx::onBinaryMessageReceived); + connect(&m_webSocket, &QWebSocket::textMessageReceived, this, + &ComAtprotoSyncSubscribeReposEx::onTextMessageReceived); connect(&m_webSocket, &QWebSocket::disconnected, this, &ComAtprotoSyncSubscribeReposEx::onDisconnected); @@ -56,9 +59,10 @@ ComAtprotoSyncSubscribeReposEx::ComAtprotoSyncSubscribeReposEx(QObject *parent) }); } -void ComAtprotoSyncSubscribeReposEx::open(const QUrl &url) +void ComAtprotoSyncSubscribeReposEx::open(const QUrl &url, SubScribeMode mode) { if (m_webSocket.state() == QAbstractSocket::UnconnectedState) { + m_subscribeMode = mode; m_webSocket.open(url); } } @@ -88,6 +92,20 @@ void ComAtprotoSyncSubscribeReposEx::onDisconnected() void ComAtprotoSyncSubscribeReposEx::onBinaryMessageReceived(const QByteArray &message) { // qDebug().noquote() << "onBinaryMessageReceived" << message.length(); + if (m_subscribeMode == SubScribeMode::Firehose) { + messageReceivedFromFirehose(message); + } +} + +void ComAtprotoSyncSubscribeReposEx::onTextMessageReceived(const QString &message) +{ + if (m_subscribeMode == SubScribeMode::JetStream) { + messageReceivedFromJetStream(message.toUtf8()); + } +} + +void ComAtprotoSyncSubscribeReposEx::messageReceivedFromFirehose(const QByteArray &message) +{ CarDecoder decoder(true); int offset = 0; @@ -135,4 +153,76 @@ void ComAtprotoSyncSubscribeReposEx::onBinaryMessageReceived(const QByteArray &m m_webSocket.close(); } } + +void ComAtprotoSyncSubscribeReposEx::messageReceivedFromJetStream(const QByteArray &message) +{ + + QString payload_type; + + QJsonDocument doc = QJsonDocument::fromJson(message); + QJsonObject json_src = doc.object(); + QHash commit_type_to; + commit_type_to["c"] = "create"; + commit_type_to["u"] = "update"; + commit_type_to["d"] = "delete"; + + if (doc.isNull() || json_src.isEmpty()) { + qDebug().noquote() << "Invalid data"; + qDebug().noquote() << "message:" << message; + emit errorOccured("InvalidData", "Unreadable JSON data."); + m_webSocket.close(); + } else if (!json_src.contains("type") || !json_src.contains("did") + || !json_src.contains("commit")) { + qDebug().noquote() << "Unsupport data:" << message; + // emit errorOccured("InvalidData", "Unanticipated data structure."); + // m_webSocket.close(); + } else { + payload_type = "#commit"; + + QJsonObject json_src_commit = json_src.value("commit").toObject(); + QJsonObject json_dest; + + json_dest.insert("repo", json_src.value("did").toString()); + json_dest.insert("rev", json_src_commit.value("rev").toString()); + json_dest.insert("time", + QDateTime::fromMSecsSinceEpoch( + static_cast(json_src.value("time_us").toDouble() / 1000)) + .toString(Qt::ISODateWithMs)); + + QJsonObject json_dest_commit; + json_dest_commit.insert("$link", json_src_commit.value("cid").toString()); + json_dest.insert("commit", json_dest_commit); + + QJsonObject json_dest_op; + QString commit_type = commit_type_to.value(json_src_commit.value("type").toString()); + json_dest_op.insert("action", commit_type); + json_dest_op.insert("path", + QString("%1/%2").arg(json_src_commit.value("collection").toString(), + json_src_commit.value("rkey").toString())); + if (commit_type == "delete") { + json_dest_op.insert("cid", QJsonValue()); + } else { + json_dest_op.insert("cid", json_dest_commit); + } + QJsonArray json_dest_ops; + json_dest_ops.append(json_dest_op); + json_dest.insert("ops", json_dest_ops); + + QJsonArray json_dest_blocks; + if (json_src_commit.contains("record")) { + QJsonObject json_dest_block; + json_dest_block.insert("cid", json_src_commit.value("cid").toString()); + json_dest_block.insert("uri", + QString("at://%1/%2/%3") + .arg(json_src.value("did").toString(), + json_src_commit.value("collection").toString(), + json_src_commit.value("rkey").toString())); + json_dest_block.insert("value", json_src_commit.value("record").toObject()); + json_dest_blocks.append(json_dest_block); + } + json_dest.insert("blocks", json_dest_blocks); + + emit received(payload_type, json_dest); + } +} } diff --git a/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.h b/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.h index 20b3109b..22c1c33f 100644 --- a/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.h +++ b/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.h @@ -12,9 +12,14 @@ class ComAtprotoSyncSubscribeReposEx : public QObject { Q_OBJECT public: + enum SubScribeMode : int { + Firehose, + JetStream, + }; + explicit ComAtprotoSyncSubscribeReposEx(QObject *parent = nullptr); - void open(const QUrl &url); + void open(const QUrl &url, SubScribeMode mode = SubScribeMode::Firehose); void close(); QAbstractSocket::SocketState state() const; @@ -28,10 +33,15 @@ public slots: void onConnected(); void onDisconnected(); void onBinaryMessageReceived(const QByteArray &message); + void onTextMessageReceived(const QString &message); private: + void messageReceivedFromFirehose(const QByteArray &message); + void messageReceivedFromJetStream(const QByteArray &message); + QWebSocket m_webSocket; QStringList m_payloadTypeList; + SubScribeMode m_subscribeMode; }; } diff --git a/lib/i18n/lib_ja.qm b/lib/i18n/lib_ja.qm new file mode 100644 index 00000000..830df881 Binary files /dev/null and b/lib/i18n/lib_ja.qm differ diff --git a/lib/i18n/lib_ja.ts b/lib/i18n/lib_ja.ts new file mode 100644 index 00000000..b4c5dc46 --- /dev/null +++ b/lib/i18n/lib_ja.ts @@ -0,0 +1,286 @@ + + + + + ConfigurableLabels + + + + Content hidden + 閲覧不可 + + + + + Moderator overrides for special cases. + 特別な場合のためのモデレーターによるオーバーライド。 + + + + + Content warning + 閲覧警告 + + + + + Legal + 法的 + + + + Content removed for legal reasons. + 法的な理由で削除されたコンテンツ。 + + + Explicit Sexual Images + 露骨な性的な画像 + + + i.e. pornography + 例えばポルノ + + + + Sexually Explicit + 露骨な性的表現 + + + Other Nudity + その他ヌード + + + Including non-sexual and artistic + ノンセクシャルや芸術的なものも含む + + + Nudity + ヌード + + + + Basic Moderation + 基本モデレーション + + + + Basic configuration independent of moderation services. + モデレーションサービスに依存しない基本設定。 + + + + Pornography + ポルノ + + + + Explicit sexual images. + 露骨な性的な画像。 + + + + + Sexually Suggestive + 性的な示唆 + + + + Does not include nudity + ヌードは含まない + + + + + Graphic Media + グラフィックメディア + + + + Explicit or potentially disturbing media. + 露骨な、あるいは不穏なメディア。 + + + + + Non-sexual Nudity + 性的でないヌード + + + + E.g. artistic nudes. + 例:芸術的なヌード + + + + Violent / Bloody + 暴力 / 流血 + + + + Gore, self-harm, torture + 血糊、自傷行為、拷問 + + + + Violence + 暴力 + + + + Hate Group Iconography + ヘイトグループの象徴 + + + + Images of terror groups, articles covering events, etc. + テロ集団の画像、事件を取り上げた記事など + + + + Hate Groups + ヘイトグループ + + + + + Spam + スパム + + + + Excessive unwanted interactions + 過剰な不要な干渉 + + + + Impersonation / Scam + なりすまし / 詐欺 + + + + Impersonation + なりすまし + + + + Accounts falsely claiming to be people or orgs + 個人や団体を偽ったアカウント + + + + LogAccess + + + + + Post + ポスト + + + + Total number of posts + 総ポスト数 + + + + Number of days posts + ポストした日数 + + + + Average number of daily posts + 1日あたりの平均ポスト数 + + + + + + Like + いいね + + + + Total number of likes + 総いいね数 + + + + Number of days likes + いいねした日数 + + + + Average number of daily likes + 1日あたりの平均いいね数 + + + + + + Repost + リポスト + + + + Total number of reposts + 総リポスト数 + + + + Number of days reposts + リポストした日数 + + + + Average number of daily reposts + 1日あたりの平均リポスト数 + + + + + List + リスト + + + + Total number of lists + リストの数 + + + + Total number of list items + リストに登録している総アカウント数 + + + + + + Other + その他 + + + + Total number of follows + フォロー数 + + + + Total number of who can reply + リプライ制限をした数 + + + + Total number of blocks + 総ブロック数 + + + + Number of registrations for list + リストに登録しているアカウント数 + + + Number of registrations for '%1' + '%1'に登録しているアカウント数 + + + diff --git a/lib/lib.pro b/lib/lib.pro index 9480d9d8..5b6763c6 100644 --- a/lib/lib.pro +++ b/lib/lib.pro @@ -13,6 +13,8 @@ unix:INCLUDEPATH += ../openssl/include DEFINES += CPPHTTPLIB_ZLIB_SUPPORT # zlib support for cpp-httplib +TRANSLATIONS += i18n/lib_ja.ts + SOURCES += \ $$PWD/atprotocol/accessatprotocol.cpp \ $$PWD/atprotocol/app/bsky/actor/appbskyactorgetpreferences.cpp \ diff --git a/lib/realtime/firehosereceiver.cpp b/lib/realtime/firehosereceiver.cpp index 158791f7..c474b238 100644 --- a/lib/realtime/firehosereceiver.cpp +++ b/lib/realtime/firehosereceiver.cpp @@ -5,6 +5,8 @@ // #include #include +#define USE_JETSTREAM + using AtProtocolInterface::ComAtprotoSyncSubscribeReposEx; namespace RealtimeFeed { @@ -18,7 +20,11 @@ FirehoseReceiver::FirehoseReceiver(QObject *parent) , m_status(FirehoseReceiverStatus::Disconnected) { +#ifdef USE_JETSTREAM + m_serviceEndpoint = "wss://jetstream2.us-west.bsky.network"; +#else m_serviceEndpoint = "wss://bsky.network"; +#endif m_wdgTimer.setInterval(60 * 1000); connect(&m_client, &ComAtprotoSyncSubscribeReposEx::errorOccured, @@ -97,9 +103,19 @@ void FirehoseReceiver::start() if (path.endsWith("/")) { path.resize(path.length() - 1); } +#ifdef USE_JETSTREAM + ComAtprotoSyncSubscribeReposEx::SubScribeMode mode = + ComAtprotoSyncSubscribeReposEx::SubScribeMode::JetStream; + QUrl url(path + + "/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed." + "repost&wantedCollections=app.bsky.graph.follow"); +#else + ComAtprotoSyncSubscribeReposEx::SubScribeMode mode = + ComAtprotoSyncSubscribeReposEx::SubScribeMode::Firehose; QUrl url(path + "/xrpc/com.atproto.sync.subscribeRepos"); +#endif qDebug().noquote() << "Connect to" << url.toString(); - m_client.open(url); + m_client.open(url, mode); m_wdgTimer.start(); } diff --git a/lib/tools/opengraphprotocol.cpp b/lib/tools/opengraphprotocol.cpp index e96e5aa5..ca97f36d 100644 --- a/lib/tools/opengraphprotocol.cpp +++ b/lib/tools/opengraphprotocol.cpp @@ -28,6 +28,16 @@ void OpenGraphProtocol::getData(const QString &url) m_thumb.clear(); QNetworkRequest request((QUrl(url))); + request.setRawHeader(QByteArray("accept"), QByteArray("*/*")); + request.setRawHeader(QByteArray("sec-ch-ua-platform"), QByteArray("Windows")); + request.setRawHeader(QByteArray("sec-fetch-dest"), QByteArray("document")); + request.setRawHeader(QByteArray("sec-fetch-mode"), QByteArray("navigate")); + request.setRawHeader(QByteArray("sec-fetch-site"), QByteArray("none")); + request.setRawHeader(QByteArray("sec-fetch-user"), QByteArray("?1")); + request.setHeader(QNetworkRequest::UserAgentHeader, + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, " + "like Gecko) Chrome/129.0.0.0 Safari/537.36"); + request.setTransferTimeout(60 * 1000); QNetworkAccessManager *manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, [=](QNetworkReply *reply) { @@ -54,6 +64,9 @@ void OpenGraphProtocol::getData(const QString &url) emit reply->redirectAllowed(); } }); + connect(reply, &QNetworkReply::errorOccurred, [=](QNetworkReply::NetworkError code) { + qDebug() << "Reply error:" << code << reply->request().url(); + }); } void OpenGraphProtocol::downloadThumb(const QString &path) @@ -172,7 +185,8 @@ bool OpenGraphProtocol::parse(const QByteArray &data, const QString &src_uri) QDomDocument doc; rebuildHtml(ts.readAll(), doc); #else - QTextCodec *codec = QTextCodec::codecForHtml(data, QTextCodec::codecForName("utf-8")); + QString charset = extractCharset(QString::fromUtf8(data)); + QTextCodec *codec = QTextCodec::codecForHtml(data, QTextCodec::codecForName(charset.toUtf8())); QDomDocument doc; rebuildHtml(codec->toUnicode(data), doc); #endif diff --git a/scripts/build.sh b/scripts/build.sh index e1d7e155..11787ce9 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -85,6 +85,7 @@ deploy_hagoromo(){ cp "zlib/lib/libz.so.1.3.1" ${work_dir}/lib cp "zlib/lib/libz.so.1" ${work_dir}/lib cp "app/i18n/app_ja.qm" ${work_dir}/bin/translations + cp "lib/i18n/lib_ja.qm" ${work_dir}/bin/translations cp ${QT_BIN_FOLDER}/../translations/qt_ja.qm ${work_dir}/bin/translations cat ${SCRIPT_FOLDER}/deploy/linux_lib.txt | xargs -i{} cp -P ${QT_BIN_FOLDER}/../lib/{} ${work_dir}/lib @@ -99,6 +100,7 @@ deploy_hagoromo(){ mkdir -p ${work_dir}/Hagoromo.app/Contents/MacOS/translations cp "app/i18n/app_ja.qm" ${work_dir}/Hagoromo.app/Contents/MacOS/translations + cp "lib/i18n/lib_ja.qm" ${work_dir}/Hagoromo.app/Contents/MacOS/translations cp ${QT_BIN_FOLDER}/../translations/qt_ja.qm ${work_dir}/Hagoromo.app/Contents/MacOS/translations cp -RL "zlib/lib/libz.1.dylib" ${work_dir}/Hagoromo.app/Contents/Frameworks fi diff --git a/tests/atprotocol_test/atprotocol_test.qrc b/tests/atprotocol_test/atprotocol_test.qrc index c6e8272a..ea69cbb8 100644 --- a/tests/atprotocol_test/atprotocol_test.qrc +++ b/tests/atprotocol_test/atprotocol_test.qrc @@ -74,5 +74,6 @@ response/labels/provider/2/xrpc/app.bsky.labeler.getServices data/postgate/1/xrpc/com.atproto.repo.createRecord data/postgate/2/xrpc/com.atproto.repo.createRecord + response/ogp/file7.html diff --git a/tests/atprotocol_test/response/ogp/file7.html b/tests/atprotocol_test/response/ogp/file7.html new file mode 100644 index 00000000..80363aa1 --- /dev/null +++ b/tests/atprotocol_test/response/ogp/file7.html @@ -0,0 +1,19 @@ + + + + + + + + +^Cg^O + + + + + + + + +file7 + diff --git a/tests/atprotocol_test/tst_atprotocol_test.cpp b/tests/atprotocol_test/tst_atprotocol_test.cpp index 154e400f..ea60273d 100644 --- a/tests/atprotocol_test/tst_atprotocol_test.cpp +++ b/tests/atprotocol_test/tst_atprotocol_test.cpp @@ -441,6 +441,28 @@ void atprotocol_test::test_OpenGraphProtocol() .arg(QString::number(m_listenPort)), ogp.thumb().toLocal8Bit()); } + + { + QSignalSpy spy(&ogp, SIGNAL(finished(bool))); + ogp.getData(m_service + "/ogp/file7.html"); + spy.wait(); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + + QList arguments = spy.takeFirst(); + QVERIFY(arguments.at(0).toBool()); + + QVERIFY2(ogp.uri() + == QString("http://localhost:%1/response/ogp/file7.html") + .arg(QString::number(m_listenPort)), + ogp.uri().toLocal8Bit()); + QVERIFY2(ogp.title() == QString("file7 TITLE"), ogp.title().toLocal8Bit()); + QVERIFY2(ogp.description() == QString("file7 ").append(QChar(0x8a73)).append(QChar(0x7d30)), + ogp.description().toLocal8Bit()); + QVERIFY2(ogp.thumb() + == QString("http://localhost:%1/response/ogp/images/file7.png") + .arg(QString::number(m_listenPort)), + ogp.thumb().toLocal8Bit()); + } } void atprotocol_test::test_ogpDecodeHtml() diff --git a/tools/firehosereader/main.cpp b/tools/firehosereader/main.cpp index 3a5f6e7d..f6ca7b5f 100644 --- a/tools/firehosereader/main.cpp +++ b/tools/firehosereader/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "comatprotosyncsubscribereposex.h" @@ -18,11 +19,22 @@ int main(int argc, char *argv[]) qWarning() << " usage: firehosereader URL STOPER_USER_DID"; return 1; } + ComAtprotoSyncSubscribeReposEx::SubScribeMode mode = + ComAtprotoSyncSubscribeReposEx::SubScribeMode::Firehose; QUrl url(a.arguments().at(1)); QString stopper_did = a.arguments().at(2); qDebug().noquote() << "url" << url.toString(); + qDebug().noquote() << " " << url.scheme(); + qDebug().noquote() << " " << url.host(); + qDebug().noquote() << " " << url.path(); + qDebug().noquote() << " " << url.query(); qDebug().noquote() << "Stopper" << stopper_did; // wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos + // wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.repost&wantedCollections=app.bsky.graph.follow + + if (url.host().startsWith("jetstream")) { + mode = ComAtprotoSyncSubscribeReposEx::SubScribeMode::JetStream; + } QElapsedTimer timer; timer.start(); @@ -105,7 +117,10 @@ int main(int argc, char *argv[]) QCoreApplication::quit(); } }); - client.open(url); + client.open(url, mode); + + QNetworkRequest req(url); + qDebug().noquote() << req.url().toString() << req.url().query(); return a.exec(); } diff --git a/web/content/docs/release-note.en.md b/web/content/docs/release-note.en.md index 1d7248d0..fcf8d593 100644 --- a/web/content/docs/release-note.en.md +++ b/web/content/docs/release-note.en.md @@ -8,6 +8,18 @@ description: This is a multi-column Bluesky client. ## 2024 +### v0.39.0 - 2024/10/12 + +- Add + - Add labels set for the account to the post thread + - Added a function to copy hashtags +- Update + - Change the display format of the profile label + - Reduces the amount of data transmitted in real-time feeds +- Fix + - Fix the layout when there is no thumbnail image on the link card + - Fix a case where the information on the link card could not be obtained + ### v0.38.0 - 2024/9/28 - Update diff --git a/web/content/docs/release-note.ja.md b/web/content/docs/release-note.ja.md index f1448318..378f76a9 100644 --- a/web/content/docs/release-note.ja.md +++ b/web/content/docs/release-note.ja.md @@ -8,6 +8,18 @@ description: マルチカラム対応Blueskyクライアント ## 2024 +### v0.39.0 - 2024/10/12 + +- 追加 + - ポストスレッドにアカウントに設定されたラベルを追加 + - ハッシュタグをコピーする機能を追加 +- 更新 + - プロフィールのラベルの表示方法を変更 + - リアルタイムフィードの通信量を軽減 +- 修正 + - リンクカードにサムネ画像がないときのレイアウトを修正 + - リンクカードの情報を取得できないケースを修正 + ### v0.38.0 - 2024/9/28 - 更新 diff --git a/web/layouts/shortcodes/download_link.html b/web/layouts/shortcodes/download_link.html index 490e5959..61421fac 100644 --- a/web/layouts/shortcodes/download_link.html +++ b/web/layouts/shortcodes/download_link.html @@ -1,9 +1,9 @@