diff --git a/lang/default.json b/lang/default.json index cfe5799b8f..cb0003209f 100644 --- a/lang/default.json +++ b/lang/default.json @@ -113,6 +113,10 @@ "/IMR+8": { "defaultMessage": "Top Supporters" }, + "/LjM9M": { + "defaultMessage": "Quote card", + "description": "src/components/TextSelectionPopover/index.tsx" + }, "/MS+jK": { "defaultMessage": "Collection", "description": "src/components/Editor/PreviewDialog/Collections/index.tsx" @@ -146,6 +150,9 @@ "defaultMessage": "Public", "description": "src/views/Circle/Analytics/ContentAnalytics/ContentTabs/index.tsx" }, + "/uJdnC": { + "defaultMessage": "Sky" + }, "/wKyxw": { "defaultMessage": "Failed to republish" }, @@ -235,6 +242,9 @@ "defaultMessage": "Failed", "description": "src/components/Transaction/State/index.tsx" }, + "1DQn89": { + "defaultMessage": "Share Quote" + }, "1EYCdR": { "defaultMessage": "Tags" }, @@ -400,6 +410,9 @@ "3YAasP": { "defaultMessage": "What is Liker ID?" }, + "3cxMQp": { + "defaultMessage": "Violet" + }, "3kbIhS": { "defaultMessage": "Untitled" }, @@ -514,6 +527,9 @@ "5mu8HJ": { "defaultMessage": "Moment deleted" }, + "5q3qC0": { + "defaultMessage": "Download" + }, "5rxHb7": { "defaultMessage": "May contain pornography, violence, gore, etc. Click here to expand all.", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -576,6 +592,9 @@ "defaultMessage": "subscribers_empty", "description": "src/views/Circle/Analytics/SubscriberAnalytics/index.tsx" }, + "6RAJ7U": { + "defaultMessage": "IG / FB story · Threads" + }, "6Sj2lN": { "defaultMessage": "Claim" }, @@ -665,6 +684,9 @@ "defaultMessage": "No results", "description": "src/components/Dialogs/AddArticlesCollectionDialog/SearchingDialogContent.tsx" }, + "7mL9QE": { + "defaultMessage": "Style" + }, "7oytv9": { "defaultMessage": "(edited)" }, @@ -817,6 +839,9 @@ "A4P0al": { "defaultMessage": "Channel suggestion" }, + "A6dqhl": { + "defaultMessage": "IG / FB post" + }, "A6ozr9": { "defaultMessage": "Collection name" }, @@ -987,6 +1012,9 @@ "defaultMessage": "mentioned you in a comment", "description": "src/components/Notice/CommentNotice/CommentMentionedYouNotice.tsx" }, + "CZciVV": { + "defaultMessage": "Story 9:16" + }, "CbRvzm": { "defaultMessage": "Schedule publish" }, @@ -1476,6 +1504,9 @@ "K2ec8y": { "defaultMessage": "Please verify email" }, + "K3+ihp": { + "defaultMessage": "Pine" + }, "K3r6DQ": { "defaultMessage": "Delete" }, @@ -1739,6 +1770,9 @@ "defaultMessage": "Your schedule article {articleTitle} published successfully, but failed to submit because the selection event has ended", "description": "src/components/Notice/ArticleNotice/ScheduledArticlePublishedNotice.tsx" }, + "Oawtbo": { + "defaultMessage": "IG post · most eye-catching" + }, "OhSg5a": { "defaultMessage": "Sensitive by author" }, @@ -1760,6 +1794,9 @@ "OwMuXW": { "defaultMessage": "Cancel schedule" }, + "OwO+Nr": { + "defaultMessage": "Mint" + }, "OwtCWk": { "defaultMessage": "Must be between {MIN_CIRCLE_DISPLAY_NAME_LENGTH}-{MAX_CIRCLE_DISPLAY_NAME_LENGTH} characters long." }, @@ -1853,6 +1890,9 @@ "defaultMessage": "Billboard is an open and rentable on-chain NFT advertising protocol. Once a rental is completed, the content can be displayed for 14 days. The rental fee is calculated based on the concept of the Harberger tax, and the generated rental income is distributed to the community creators through quadratic funding.", "description": "src/components/Dialogs/BillboardDialog/Content.tsx" }, + "QWkEED": { + "defaultMessage": "Ink" + }, "QXJQ5G": { "defaultMessage": "Please log in again.", "description": "TOKEN_INVALID" @@ -2463,6 +2503,9 @@ "afLdf2": { "defaultMessage": "Moment" }, + "agOXPD": { + "defaultMessage": "Size" + }, "ai7kS4": { "defaultMessage": "My Works" }, @@ -2513,6 +2556,9 @@ "defaultMessage": "Unblock", "description": "src/components/BlockUser/Button/index.tsx" }, + "bGxO22": { + "defaultMessage": "Coral" + }, "bQ5vZC": { "defaultMessage": "This article has been marked as restricted content by the {actor}.", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -2691,6 +2737,9 @@ "e3qUqn": { "defaultMessage": "Pornography involving minors" }, + "e4AtHq": { + "defaultMessage": "You selected {original} characters; only the first {max} are shown. A concise quote works best." + }, "eIlMHB": { "defaultMessage": "Allow readers to respond to this article (can NOT be disabled afterwards)" }, @@ -2719,6 +2768,9 @@ "eZ0m39": { "defaultMessage": "Enter the amount" }, + "em7860": { + "defaultMessage": "Portrait 4:5" + }, "enMIYK": { "defaultMessage": "My Page" }, @@ -2942,9 +2994,15 @@ "iEJeQH": { "defaultMessage": "Liker ID" }, + "iII6Ry": { + "defaultMessage": "Square 1:1" + }, "iIitRg": { "defaultMessage": "Tag not bookmarked yet" }, + "iLKG5w": { + "defaultMessage": "Slate" + }, "iNZdM/": { "defaultMessage": "Switch to support creators with the Optimism network {br} Make support more convenient and affordable", "description": "src/components/Forms/PaymentForm/SwitchNetwork/index.tsx" @@ -3129,6 +3187,9 @@ "lTleCS": { "defaultMessage": "Checking" }, + "lY48xg": { + "defaultMessage": "Cream" + }, "lYVn31": { "defaultMessage": "This work has been added to the schedule. Please go to the \"My Works\" page to confirm" }, @@ -3624,6 +3685,10 @@ "defaultMessage": "Comment has been deleted", "description": "Moment" }, + "uDdAD+": { + "defaultMessage": "Quote", + "description": "src/components/TextSelectionPopover/index.tsx" + }, "uM5qZr": { "defaultMessage": "Likes Given" }, diff --git a/lang/en.json b/lang/en.json index a05a08c846..c6b88a1273 100644 --- a/lang/en.json +++ b/lang/en.json @@ -113,6 +113,10 @@ "/IMR+8": { "defaultMessage": "Top Supporters" }, + "/LjM9M": { + "defaultMessage": "Quote card", + "description": "src/components/TextSelectionPopover/index.tsx" + }, "/MS+jK": { "defaultMessage": "Collection", "description": "src/components/Editor/PreviewDialog/Collections/index.tsx" @@ -146,6 +150,9 @@ "defaultMessage": "Public", "description": "src/views/Circle/Analytics/ContentAnalytics/ContentTabs/index.tsx" }, + "/uJdnC": { + "defaultMessage": "Sky" + }, "/wKyxw": { "defaultMessage": "Failed to republish" }, @@ -235,6 +242,9 @@ "defaultMessage": "Failed", "description": "src/components/Transaction/State/index.tsx" }, + "1DQn89": { + "defaultMessage": "Share Quote" + }, "1EYCdR": { "defaultMessage": "Tags" }, @@ -400,6 +410,9 @@ "3YAasP": { "defaultMessage": "What is Liker ID?" }, + "3cxMQp": { + "defaultMessage": "Violet" + }, "3kbIhS": { "defaultMessage": "Untitled" }, @@ -514,6 +527,9 @@ "5mu8HJ": { "defaultMessage": "Moment deleted" }, + "5q3qC0": { + "defaultMessage": "Download" + }, "5rxHb7": { "defaultMessage": "May contain pornography, violence, gore, etc. Click here to expand all.", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -576,6 +592,9 @@ "defaultMessage": "subscribers_empty", "description": "src/views/Circle/Analytics/SubscriberAnalytics/index.tsx" }, + "6RAJ7U": { + "defaultMessage": "IG / FB story · Threads" + }, "6Sj2lN": { "defaultMessage": "Claim" }, @@ -665,6 +684,9 @@ "defaultMessage": "No results", "description": "src/components/Dialogs/AddArticlesCollectionDialog/SearchingDialogContent.tsx" }, + "7mL9QE": { + "defaultMessage": "Style" + }, "7oytv9": { "defaultMessage": " (edited) " }, @@ -817,6 +839,9 @@ "A4P0al": { "defaultMessage": "Channel suggestion" }, + "A6dqhl": { + "defaultMessage": "IG / FB post" + }, "A6ozr9": { "defaultMessage": "Collection name" }, @@ -987,6 +1012,9 @@ "defaultMessage": "mentioned you in a comment", "description": "src/components/Notice/CommentNotice/CommentMentionedYouNotice.tsx" }, + "CZciVV": { + "defaultMessage": "Story 9:16" + }, "CbRvzm": { "defaultMessage": "Schedule publish" }, @@ -1476,6 +1504,9 @@ "K2ec8y": { "defaultMessage": "Please verify email" }, + "K3+ihp": { + "defaultMessage": "Pine" + }, "K3r6DQ": { "defaultMessage": "Delete" }, @@ -1739,6 +1770,9 @@ "defaultMessage": "Your schedule article {articleTitle} published successfully, but failed to submit because the selection event has ended", "description": "src/components/Notice/ArticleNotice/ScheduledArticlePublishedNotice.tsx" }, + "Oawtbo": { + "defaultMessage": "IG post · most eye-catching" + }, "OhSg5a": { "defaultMessage": "Sensitive by author" }, @@ -1760,6 +1794,9 @@ "OwMuXW": { "defaultMessage": "Cancel schedule" }, + "OwO+Nr": { + "defaultMessage": "Mint" + }, "OwtCWk": { "defaultMessage": "Must be between {MIN_CIRCLE_DISPLAY_NAME_LENGTH}-{MAX_CIRCLE_DISPLAY_NAME_LENGTH} characters long." }, @@ -1853,6 +1890,9 @@ "defaultMessage": "Billboard is an open and rentable on-chain NFT advertising protocol. Once a rental is completed, the content can be displayed for 14 days. The rental fee is calculated based on the concept of the Harberger tax, and the generated rental income is distributed to the community creators through quadratic funding.", "description": "src/components/Dialogs/BillboardDialog/Content.tsx" }, + "QWkEED": { + "defaultMessage": "Ink" + }, "QXJQ5G": { "defaultMessage": "Please log in again.", "description": "TOKEN_INVALID" @@ -2463,6 +2503,9 @@ "afLdf2": { "defaultMessage": "Moment" }, + "agOXPD": { + "defaultMessage": "Size" + }, "ai7kS4": { "defaultMessage": "My Works" }, @@ -2513,6 +2556,9 @@ "defaultMessage": "Unblock", "description": "src/components/BlockUser/Button/index.tsx" }, + "bGxO22": { + "defaultMessage": "Coral" + }, "bQ5vZC": { "defaultMessage": "This article has been marked as restricted content by the {actor}.", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -2691,6 +2737,9 @@ "e3qUqn": { "defaultMessage": "Pornography involving minors" }, + "e4AtHq": { + "defaultMessage": "You selected {original} characters; only the first {max} are shown. A concise quote works best." + }, "eIlMHB": { "defaultMessage": "Allow readers to respond to this article (can NOT be disabled afterwards)" }, @@ -2719,6 +2768,9 @@ "eZ0m39": { "defaultMessage": "Enter the amount" }, + "em7860": { + "defaultMessage": "Portrait 4:5" + }, "enMIYK": { "defaultMessage": "My Page" }, @@ -2942,9 +2994,15 @@ "iEJeQH": { "defaultMessage": "Liker ID" }, + "iII6Ry": { + "defaultMessage": "Square 1:1" + }, "iIitRg": { "defaultMessage": "Tag not bookmarked yet" }, + "iLKG5w": { + "defaultMessage": "Slate" + }, "iNZdM/": { "defaultMessage": "Switch to support creators with the Optimism network {br} Make support more convenient and affordable", "description": "src/components/Forms/PaymentForm/SwitchNetwork/index.tsx" @@ -3129,6 +3187,9 @@ "lTleCS": { "defaultMessage": "Checking" }, + "lY48xg": { + "defaultMessage": "Cream" + }, "lYVn31": { "defaultMessage": "This work has been added to the schedule. Please go to the \"My Works\" page to confirm" }, @@ -3624,6 +3685,10 @@ "defaultMessage": "Comment has been deleted", "description": "Moment" }, + "uDdAD+": { + "defaultMessage": "Quote", + "description": "src/components/TextSelectionPopover/index.tsx" + }, "uM5qZr": { "defaultMessage": "Likes Given" }, diff --git a/lang/zh-Hans.json b/lang/zh-Hans.json index 1a328447a2..2cc439435f 100644 --- a/lang/zh-Hans.json +++ b/lang/zh-Hans.json @@ -113,6 +113,10 @@ "/IMR+8": { "defaultMessage": "支持排行榜" }, + "/LjM9M": { + "defaultMessage": "Quote card", + "description": "src/components/TextSelectionPopover/index.tsx" + }, "/MS+jK": { "defaultMessage": "选集", "description": "src/components/Editor/PreviewDialog/Collections/index.tsx" @@ -146,6 +150,9 @@ "defaultMessage": "公开", "description": "src/views/Circle/Analytics/ContentAnalytics/ContentTabs/index.tsx" }, + "/uJdnC": { + "defaultMessage": "Sky" + }, "/wKyxw": { "defaultMessage": "发布失败" }, @@ -235,6 +242,9 @@ "defaultMessage": "失败", "description": "src/components/Transaction/State/index.tsx" }, + "1DQn89": { + "defaultMessage": "Share Quote" + }, "1EYCdR": { "defaultMessage": "标签" }, @@ -400,6 +410,9 @@ "3YAasP": { "defaultMessage": "什么是 Liker ID?" }, + "3cxMQp": { + "defaultMessage": "Violet" + }, "3kbIhS": { "defaultMessage": "未命名" }, @@ -514,6 +527,9 @@ "5mu8HJ": { "defaultMessage": "动态已删除" }, + "5q3qC0": { + "defaultMessage": "Download" + }, "5rxHb7": { "defaultMessage": "可能包含色情、暴力、血腥等,点此展开全部", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -576,6 +592,9 @@ "defaultMessage": "目前总订阅人数", "description": "src/views/Circle/Analytics/SubscriberAnalytics/index.tsx" }, + "6RAJ7U": { + "defaultMessage": "IG / FB story · Threads" + }, "6Sj2lN": { "defaultMessage": "提领支持" }, @@ -665,6 +684,9 @@ "defaultMessage": "无结果", "description": "src/components/Dialogs/AddArticlesCollectionDialog/SearchingDialogContent.tsx" }, + "7mL9QE": { + "defaultMessage": "Style" + }, "7oytv9": { "defaultMessage": "(修改过)" }, @@ -817,6 +839,9 @@ "A4P0al": { "defaultMessage": "建议频道" }, + "A6dqhl": { + "defaultMessage": "IG / FB post" + }, "A6ozr9": { "defaultMessage": "选集名称" }, @@ -987,6 +1012,9 @@ "defaultMessage": "在动态留言中提及你", "description": "src/components/Notice/CommentNotice/CommentMentionedYouNotice.tsx" }, + "CZciVV": { + "defaultMessage": "Story 9:16" + }, "CbRvzm": { "defaultMessage": "定时发布" }, @@ -1476,6 +1504,9 @@ "K2ec8y": { "defaultMessage": "请先验证邮箱" }, + "K3+ihp": { + "defaultMessage": "Pine" + }, "K3r6DQ": { "defaultMessage": "刪除" }, @@ -1739,6 +1770,9 @@ "defaultMessage": "你的定时发布作品 {articleTitle} 已发布成功,由于选择活动已结束,投稿未能成功", "description": "src/components/Notice/ArticleNotice/ScheduledArticlePublishedNotice.tsx" }, + "Oawtbo": { + "defaultMessage": "IG post · most eye-catching" + }, "OhSg5a": { "defaultMessage": "限制级内容" }, @@ -1760,6 +1794,9 @@ "OwMuXW": { "defaultMessage": "取消定时发布" }, + "OwO+Nr": { + "defaultMessage": "Mint" + }, "OwtCWk": { "defaultMessage": "仅供输入 {MIN_CIRCLE_DISPLAY_NAME_LENGTH}-{MAX_CIRCLE_DISPLAY_NAME_LENGTH} 個字符" }, @@ -1853,6 +1890,9 @@ "defaultMessage": "Billboard 是一个公开且可付费租借的链上 NFT 广告协议。租借成功后,可进行 14 天的内容投放。租借费用基于哈伯格税概念计算,产生的租借收入将以二次方配捐方式回馈给社区创作者。", "description": "src/components/Dialogs/BillboardDialog/Content.tsx" }, + "QWkEED": { + "defaultMessage": "Ink" + }, "QXJQ5G": { "defaultMessage": "授权信息已失效,请重新登入", "description": "TOKEN_INVALID" @@ -2463,6 +2503,9 @@ "afLdf2": { "defaultMessage": "发动态" }, + "agOXPD": { + "defaultMessage": "Size" + }, "ai7kS4": { "defaultMessage": "我的创作" }, @@ -2513,6 +2556,9 @@ "defaultMessage": "取消屏蔽", "description": "src/components/BlockUser/Button/index.tsx" }, + "bGxO22": { + "defaultMessage": "Coral" + }, "bQ5vZC": { "defaultMessage": "此文已被{actor}标示为限制级内容", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -2691,6 +2737,9 @@ "e3qUqn": { "defaultMessage": "涉及未成年人的色情" }, + "e4AtHq": { + "defaultMessage": "You selected {original} characters; only the first {max} are shown. A concise quote works best." + }, "eIlMHB": { "defaultMessage": "允许读者评论本文(开启后无法关闭)" }, @@ -2719,6 +2768,9 @@ "eZ0m39": { "defaultMessage": "或输入自定义金额" }, + "em7860": { + "defaultMessage": "Portrait 4:5" + }, "enMIYK": { "defaultMessage": "我的" }, @@ -2942,9 +2994,15 @@ "iEJeQH": { "defaultMessage": "设置 Liker ID" }, + "iII6Ry": { + "defaultMessage": "Square 1:1" + }, "iIitRg": { "defaultMessage": "尚未收藏标签" }, + "iLKG5w": { + "defaultMessage": "Slate" + }, "iNZdM/": { "defaultMessage": "切换后即可支持创作者,采用 Optimism 网络{br}让支持更方便且费用低廉", "description": "src/components/Forms/PaymentForm/SwitchNetwork/index.tsx" @@ -3129,6 +3187,9 @@ "lTleCS": { "defaultMessage": "Checking" }, + "lY48xg": { + "defaultMessage": "Cream" + }, "lYVn31": { "defaultMessage": "此作品已加入定时发布,请前往「我的创作」页确认" }, @@ -3624,6 +3685,10 @@ "defaultMessage": "留言已刪除", "description": "Moment" }, + "uDdAD+": { + "defaultMessage": "Quote", + "description": "src/components/TextSelectionPopover/index.tsx" + }, "uM5qZr": { "defaultMessage": "我赞赏的" }, diff --git a/lang/zh-Hant.json b/lang/zh-Hant.json index f477b8e5a6..d7e62d67f1 100644 --- a/lang/zh-Hant.json +++ b/lang/zh-Hant.json @@ -113,6 +113,10 @@ "/IMR+8": { "defaultMessage": "支持排行榜" }, + "/LjM9M": { + "defaultMessage": "Quote card", + "description": "src/components/TextSelectionPopover/index.tsx" + }, "/MS+jK": { "defaultMessage": "選集", "description": "src/components/Editor/PreviewDialog/Collections/index.tsx" @@ -146,6 +150,9 @@ "defaultMessage": "公開", "description": "src/views/Circle/Analytics/ContentAnalytics/ContentTabs/index.tsx" }, + "/uJdnC": { + "defaultMessage": "Sky" + }, "/wKyxw": { "defaultMessage": "發布失敗" }, @@ -235,6 +242,9 @@ "defaultMessage": "失敗", "description": "src/components/Transaction/State/index.tsx" }, + "1DQn89": { + "defaultMessage": "Share Quote" + }, "1EYCdR": { "defaultMessage": "標籤" }, @@ -400,6 +410,9 @@ "3YAasP": { "defaultMessage": "什麼是 Liker ID?" }, + "3cxMQp": { + "defaultMessage": "Violet" + }, "3kbIhS": { "defaultMessage": "未命名" }, @@ -514,6 +527,9 @@ "5mu8HJ": { "defaultMessage": "動態已刪除" }, + "5q3qC0": { + "defaultMessage": "Download" + }, "5rxHb7": { "defaultMessage": "可能包含色情、暴力、血腥等,點此展開全部", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -576,6 +592,9 @@ "defaultMessage": "目前總訂閱人數", "description": "src/views/Circle/Analytics/SubscriberAnalytics/index.tsx" }, + "6RAJ7U": { + "defaultMessage": "IG / FB story · Threads" + }, "6Sj2lN": { "defaultMessage": "提領支持" }, @@ -665,6 +684,9 @@ "defaultMessage": "無結果", "description": "src/components/Dialogs/AddArticlesCollectionDialog/SearchingDialogContent.tsx" }, + "7mL9QE": { + "defaultMessage": "Style" + }, "7oytv9": { "defaultMessage": "(修改過)" }, @@ -817,6 +839,9 @@ "A4P0al": { "defaultMessage": "建議頻道" }, + "A6dqhl": { + "defaultMessage": "IG / FB post" + }, "A6ozr9": { "defaultMessage": "選集名稱" }, @@ -987,6 +1012,9 @@ "defaultMessage": "在動態留言中提及你", "description": "src/components/Notice/CommentNotice/CommentMentionedYouNotice.tsx" }, + "CZciVV": { + "defaultMessage": "Story 9:16" + }, "CbRvzm": { "defaultMessage": "排程發布" }, @@ -1476,6 +1504,9 @@ "K2ec8y": { "defaultMessage": "請先驗證電子郵件" }, + "K3+ihp": { + "defaultMessage": "Pine" + }, "K3r6DQ": { "defaultMessage": "刪除" }, @@ -1739,6 +1770,9 @@ "defaultMessage": "你的排程作品 {articleTitle} 已發布成功,由於選擇活動已結束,投稿未能成功", "description": "src/components/Notice/ArticleNotice/ScheduledArticlePublishedNotice.tsx" }, + "Oawtbo": { + "defaultMessage": "IG post · most eye-catching" + }, "OhSg5a": { "defaultMessage": "限制級內容" }, @@ -1760,6 +1794,9 @@ "OwMuXW": { "defaultMessage": "取消排程" }, + "OwO+Nr": { + "defaultMessage": "Mint" + }, "OwtCWk": { "defaultMessage": "僅供輸入 {MIN_CIRCLE_DISPLAY_NAME_LENGTH}-{MAX_CIRCLE_DISPLAY_NAME_LENGTH} 個字元" }, @@ -1853,6 +1890,9 @@ "defaultMessage": "Billboard 是一個公開且可付費租借的鏈上 NFT 廣告協議。租借成功後,可進行 14 天的內容投放。租借費用基於哈柏格稅概念計算,產生的租借收入將以二次方配捐方式回饋給社區創作者。", "description": "src/components/Dialogs/BillboardDialog/Content.tsx" }, + "QWkEED": { + "defaultMessage": "Ink" + }, "QXJQ5G": { "defaultMessage": "授權信息已失效,請重新登入", "description": "TOKEN_INVALID" @@ -2463,6 +2503,9 @@ "afLdf2": { "defaultMessage": "發動態" }, + "agOXPD": { + "defaultMessage": "Size" + }, "ai7kS4": { "defaultMessage": "我的創作" }, @@ -2513,6 +2556,9 @@ "defaultMessage": "取消封鎖", "description": "src/components/BlockUser/Button/index.tsx" }, + "bGxO22": { + "defaultMessage": "Coral" + }, "bQ5vZC": { "defaultMessage": "此文已被{actor}標示為限制級內容", "description": "src/views/ArticleDetail/Wall/Sensitive/index.tsx" @@ -2691,6 +2737,9 @@ "e3qUqn": { "defaultMessage": "涉及未成年人的色情" }, + "e4AtHq": { + "defaultMessage": "You selected {original} characters; only the first {max} are shown. A concise quote works best." + }, "eIlMHB": { "defaultMessage": "允許讀者評論本文(開啟後無法關閉)" }, @@ -2719,6 +2768,9 @@ "eZ0m39": { "defaultMessage": "或輸入自定義金額" }, + "em7860": { + "defaultMessage": "Portrait 4:5" + }, "enMIYK": { "defaultMessage": "我的" }, @@ -2942,9 +2994,15 @@ "iEJeQH": { "defaultMessage": "設置 Liker ID" }, + "iII6Ry": { + "defaultMessage": "Square 1:1" + }, "iIitRg": { "defaultMessage": "尚未收藏標籤" }, + "iLKG5w": { + "defaultMessage": "Slate" + }, "iNZdM/": { "defaultMessage": "切換後即可支持創作者,採用 Optimism 網路{br}讓支持更方便且費用低廉", "description": "src/components/Forms/PaymentForm/SwitchNetwork/index.tsx" @@ -3129,6 +3187,9 @@ "lTleCS": { "defaultMessage": "Checking" }, + "lY48xg": { + "defaultMessage": "Cream" + }, "lYVn31": { "defaultMessage": "此作品已加入排程,請前往「我的創作」頁面確認" }, @@ -3624,6 +3685,10 @@ "defaultMessage": "留言已刪除", "description": "Moment" }, + "uDdAD+": { + "defaultMessage": "Quote", + "description": "src/components/TextSelectionPopover/index.tsx" + }, "uM5qZr": { "defaultMessage": "我讚賞的" }, diff --git a/package-lock.json b/package-lock.json index 9310f52953..8569df9d60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "matters-web", - "version": "6.7.0", + "version": "6.11.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "matters-web", - "version": "6.7.0", + "version": "6.11.1", "license": "Apache-2.0", "dependencies": { "@apollo/client": "^3.13.8", @@ -47,6 +47,7 @@ "formik": "^2.4.6", "graphql": "^16.11.0", "graphql-tag": "^2.12.6", + "html-to-image": "^1.11.13", "husky": "^9.1.7", "js-base64": "^3.7.7", "js-cookie": "^3.0.5", @@ -61,6 +62,7 @@ "number-precision": "^1.6.0", "path-to-regexp": "^8.2.0", "photoswipe": "^5.4.4", + "qrcode": "^1.5.4", "react": "^18.3.1", "react-aria-components": "^1.10.1", "react-beautiful-dnd": "^13.1.1", @@ -109,6 +111,7 @@ "@types/jump.js": "^1.0.6", "@types/lodash": "^4.17.17", "@types/nprogress": "0.2.3", + "@types/qrcode": "^1.5.5", "@types/react": "^18.3.22", "@types/react-beautiful-dnd": "^13.1.8", "@types/react-dom": "^18.3.7", @@ -12200,6 +12203,149 @@ "qrcode": "1.5.3" } }, + "node_modules/@reown/appkit-ui/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@reown/appkit-ui/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/@reown/appkit-ui/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@reown/appkit-ui/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@reown/appkit-ui/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@reown/appkit-ui/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@reown/appkit-ui/node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@reown/appkit-ui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@reown/appkit-ui/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/@reown/appkit-ui/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@reown/appkit-ui/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@reown/appkit-utils": { "version": "1.7.8", "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.7.8.tgz", @@ -16572,6 +16718,16 @@ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "18.3.23", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", @@ -27351,6 +27507,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -35637,13 +35799,12 @@ } }, "node_modules/qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "license": "MIT", "dependencies": { "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, @@ -50934,6 +51095,107 @@ "@reown/appkit-wallet": "1.7.8", "lit": "3.3.0", "qrcode": "1.5.3" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "requires": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } } }, "@reown/appkit-utils": { @@ -53424,6 +53686,15 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" }, + "@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/react": { "version": "18.3.23", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", @@ -60545,6 +60816,11 @@ "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "dev": true }, + "html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==" + }, "html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -65822,12 +66098,11 @@ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==" }, "qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "requires": { "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, diff --git a/package.json b/package.json index d2c462d426..043f09a8a3 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "formik": "^2.4.6", "graphql": "^16.11.0", "graphql-tag": "^2.12.6", + "html-to-image": "^1.11.13", "husky": "^9.1.7", "js-base64": "^3.7.7", "js-cookie": "^3.0.5", @@ -95,6 +96,7 @@ "number-precision": "^1.6.0", "path-to-regexp": "^8.2.0", "photoswipe": "^5.4.4", + "qrcode": "^1.5.4", "react": "^18.3.1", "react-aria-components": "^1.10.1", "react-beautiful-dnd": "^13.1.1", @@ -143,6 +145,7 @@ "@types/jump.js": "^1.0.6", "@types/lodash": "^4.17.17", "@types/nprogress": "0.2.3", + "@types/qrcode": "^1.5.5", "@types/react": "^18.3.22", "@types/react-beautiful-dnd": "^13.1.8", "@types/react-dom": "^18.3.7", diff --git a/public/static/images/seven-day-book-logo-dark.svg b/public/static/images/seven-day-book-logo-dark.svg new file mode 100644 index 0000000000..96e18fa5c5 --- /dev/null +++ b/public/static/images/seven-day-book-logo-dark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/static/images/seven-day-book-logo-white.svg b/public/static/images/seven-day-book-logo-white.svg new file mode 100644 index 0000000000..65f5a9c033 --- /dev/null +++ b/public/static/images/seven-day-book-logo-white.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/common/utils/analytics.ts b/src/common/utils/analytics.ts index a29187e50a..dafa666254 100644 --- a/src/common/utils/analytics.ts +++ b/src/common/utils/analytics.ts @@ -90,6 +90,9 @@ export interface ClickButtonProp { | 'edited' | 'appreciate' | 'article_content_quote' + | 'article_content_quote_image' + | 'quote_image_download' + | 'quote_image_share' | 'comment_open' | 'comment_close' | 'comment_placeholder' diff --git a/src/components/QuoteImageDialog/Card.tsx b/src/components/QuoteImageDialog/Card.tsx new file mode 100644 index 0000000000..2acd2d0ede --- /dev/null +++ b/src/components/QuoteImageDialog/Card.tsx @@ -0,0 +1,105 @@ +import { forwardRef } from 'react' + +import SevenDayBookLogoDark from '@/public/static/images/seven-day-book-logo-dark.svg' +import SevenDayBookLogoWhite from '@/public/static/images/seven-day-book-logo-white.svg' + +import { + clampQuote, + fitFontSize, + type QuoteSize, + type QuoteStyle, +} from './presets' +import styles from './styles.module.css' + +const FONT_SERIF = "'Noto Serif TC','Songti TC','Songti SC','NSimSun',serif" +const FONT_SANS = + "'PingFang TC','PingFang SC','Microsoft JhengHei','Noto Sans TC',sans-serif" + +export type QuoteCardProps = { + quote: string + author: string + title: string + /** QR Code 的 data URL(由 Content 以 qrcode 產生) */ + qrDataUrl: string + style: QuoteStyle + size: QuoteSize + isSevenDayBook: boolean +} + +/** + * 金句卡片本體。固定以 1080px 寬度渲染(size.w/size.h), + * 由 Content 以 CSS transform 縮放預覽、以 html-to-image 原尺寸截圖。 + */ +export const QuoteCard = forwardRef( + ( + { quote, author, title, qrDataUrl, style: s, size, isSevenDayBook }, + ref + ) => { + const { text } = clampQuote(quote) + const fontFamily = s.font === 'serif' ? FONT_SERIF : FONT_SANS + const letterSpacing = s.wide + ? '0.06em' + : s.font === 'serif' + ? '0.02em' + : 'normal' + const lineHeight = s.airy ? 1.8 : 1.65 + const fontSize = fitFontSize(text.length, s.airy) + + const Logo = + s.logo === 'white' ? SevenDayBookLogoWhite : SevenDayBookLogoDark + + return ( +
+
+
+ “ +
+
+ {text} +
+
+ + {author} +
+
+ +
+
+
+ 原文 + {title} +
+ {isSevenDayBook ? ( +
+ +
+ ) : ( +
+ Matters · matters.town +
+ )} +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + QR code +
+
+
+ ) + } +) + +QuoteCard.displayName = 'QuoteCard' diff --git a/src/components/QuoteImageDialog/Content.tsx b/src/components/QuoteImageDialog/Content.tsx new file mode 100644 index 0000000000..d8609f55e6 --- /dev/null +++ b/src/components/QuoteImageDialog/Content.tsx @@ -0,0 +1,252 @@ +import { toJpeg } from 'html-to-image' +import QRCode from 'qrcode' +import { useEffect, useMemo, useRef, useState } from 'react' +import { defineMessages, FormattedMessage, useIntl } from 'react-intl' + +import { analytics, isMobile } from '~/common/utils' +import { Dialog } from '~/components' + +import { QuoteCard } from './Card' +import { clampQuote, MAX_QUOTE_LEN, QUOTE_SIZES, QUOTE_STYLES } from './presets' +import styles from './styles.module.css' + +const PREVIEW_WIDTH = 320 // 預覽寬度(卡片以此等比縮放顯示) + +const styleNames = defineMessages({ + warm: { defaultMessage: 'Cream', id: 'lY48xg' }, + sky: { defaultMessage: 'Sky', id: '/uJdnC' }, + coral: { defaultMessage: 'Coral', id: 'bGxO22' }, + ink: { defaultMessage: 'Ink', id: 'QWkEED' }, + pine: { defaultMessage: 'Pine', id: 'K3+ihp' }, + mint: { defaultMessage: 'Mint', id: 'OwO+Nr' }, + violet: { defaultMessage: 'Violet', id: '3cxMQp' }, + slate: { defaultMessage: 'Slate', id: 'iLKG5w' }, +}) +const sizeNames = defineMessages({ + square: { defaultMessage: 'Square 1:1', id: 'iII6Ry' }, + portrait: { defaultMessage: 'Portrait 4:5', id: 'em7860' }, + story: { defaultMessage: 'Story 9:16', id: 'CZciVV' }, +}) +const sizeNotes = defineMessages({ + square: { defaultMessage: 'IG / FB post', id: 'A6dqhl' }, + portrait: { defaultMessage: 'IG post · most eye-catching', id: 'Oawtbo' }, + story: { defaultMessage: 'IG / FB story · Threads', id: '6RAJ7U' }, +}) + +export type QuoteImageDialogContentProps = { + closeDialog: () => void + quote: string + author: string + title: string + /** 文章連結,用於產生 QR Code */ + shareLink: string + isSevenDayBook: boolean +} + +const QuoteImageDialogContent: React.FC = ({ + closeDialog, + quote, + author, + title, + shareLink, + isSevenDayBook, +}) => { + const intl = useIntl() + const cardRef = useRef(null) + + const [styleId, setStyleId] = useState('pine') + const [sizeId, setSizeId] = useState('portrait') + const [qrDataUrl, setQrDataUrl] = useState('') + + const style = QUOTE_STYLES.find((s) => s.id === styleId) || QUOTE_STYLES[0] + const size = QUOTE_SIZES.find((s) => s.id === sizeId) || QUOTE_SIZES[1] + const { truncated, original } = useMemo(() => clampQuote(quote), [quote]) + + // 依風格 / 連結重新產生 QR Code(顏色跟著風格走) + useEffect(() => { + let active = true + QRCode.toDataURL(shareLink || ' ', { + margin: 0, + width: 300, + color: { dark: style.qrDark, light: style.qrLight }, + }) + .then((url) => active && setQrDataUrl(url)) + .catch(() => active && setQrDataUrl('')) + return () => { + active = false + } + }, [shareLink, style.qrDark, style.qrLight]) + + const previewScale = PREVIEW_WIDTH / size.w + + const generate = async () => { + if (!cardRef.current) return null + return toJpeg(cardRef.current, { + quality: 0.95, + pixelRatio: 2, + width: size.w, + height: size.h, + style: { transform: 'none' }, + }) + } + + const onDownload = async () => { + const url = await generate() + if (!url) return + analytics.trackEvent('click_button', { + type: 'quote_image_download', + pageType: 'article_detail', + }) + const a = document.createElement('a') + a.href = url + a.download = `matters-quote-${style.id}-${size.id}.jpg` + a.click() + } + + const onShare = async () => { + const url = await generate() + if (!url) return + analytics.trackEvent('click_button', { + type: 'quote_image_share', + pageType: 'article_detail', + }) + try { + const blob = await (await fetch(url)).blob() + const file = new File([blob], 'matters-quote.jpg', { type: 'image/jpeg' }) + if (isMobile() && navigator.canShare?.({ files: [file] })) { + await navigator.share({ files: [file] }) + return + } + } catch { + // fall through to download + } + await onDownload() + } + + return ( + <> + } + closeDialog={closeDialog} + /> + + +
+
+ +
+
+ + {truncated && ( +

+ +

+ )} + +
+ + + +
+ {QUOTE_STYLES.map((s) => ( + + ))} +
+
+ +
+ + + +
+ {QUOTE_SIZES.map((s) => ( + + ))} +
+
+
+ + + } + color="green" + onClick={onDownload} + /> + } + color="greyDarker" + onClick={onShare} + /> + + } + smUpBtns={ + <> + } + color="green" + onClick={onDownload} + /> + } + color="greyDarker" + onClick={onShare} + /> + + } + /> + + ) +} + +export default QuoteImageDialogContent diff --git a/src/components/QuoteImageDialog/gql.ts b/src/components/QuoteImageDialog/gql.ts new file mode 100644 index 0000000000..b3d525cb31 --- /dev/null +++ b/src/components/QuoteImageDialog/gql.ts @@ -0,0 +1,54 @@ +import gql from 'graphql-tag' + +import { QuoteImageArticleFragment } from '~/gql/graphql' + +/** + * 金句卡片需要的文章欄位。 + * campaigns 用來判斷文章是否屬於「七日書」活動(WritingChallenge)。 + */ +export const fragments = { + article: gql` + fragment QuoteImageArticle on Article { + id + title + shortHash + # 版權 gate:「作者保留所有權利」(ARR) 時僅作者本人可生成金句卡片 + license + author { + id + displayName + userName + } + campaigns { + campaign { + id + shortHash + ... on WritingChallenge { + id + nameZhHant: name(input: { language: zh_hant }) + nameZhHans: name(input: { language: zh_hans }) + nameEn: name(input: { language: en }) + } + } + } + } + `, +} + +/** + * 判斷文章是否屬於「七日書」活動。 + * + * 目前以活動名稱包含「七日書」為準(涵蓋歷屆七日書)。 + * TODO: 若需更精確,可改為比對一份設定好的七日書 campaign shortHash 清單 + * (例如放在 src/common/enums 或環境變數),避免名稱被更動時誤判。 + */ +export const isSevenDayBookArticle = ( + article?: QuoteImageArticleFragment | null +): boolean => { + if (!article?.campaigns?.length) return false + return article.campaigns.some(({ campaign }) => { + if (campaign?.__typename !== 'WritingChallenge') return false + const names = [campaign.nameZhHant, campaign.nameZhHans] + return names.some((n) => !!n && n.includes('七日書')) + }) +} diff --git a/src/components/QuoteImageDialog/index.tsx b/src/components/QuoteImageDialog/index.tsx new file mode 100644 index 0000000000..77971153e9 --- /dev/null +++ b/src/components/QuoteImageDialog/index.tsx @@ -0,0 +1,38 @@ +import dynamic from 'next/dynamic' + +import { Dialog, SpinnerBlock, useDialogSwitch } from '~/components' + +import { type QuoteImageDialogContentProps } from './Content' + +export type QuoteImageDialogProps = { + children: ({ openDialog }: { openDialog: () => void }) => React.ReactNode +} & Omit + +const DynamicContent = dynamic(() => import('./Content'), { + ssr: false, + loading: () => , +}) + +const BaseDialog = ({ children, ...props }: QuoteImageDialogProps) => { + const { show, openDialog, closeDialog } = useDialogSwitch(true) + + return ( + <> + {children({ openDialog })} + + + + + + ) +} + +export const QuoteImageDialog = (props: QuoteImageDialogProps) => { + return ( + }> + {({ openDialog }) => <>{props.children({ openDialog })}} + + ) +} + +export { fragments, isSevenDayBookArticle } from './gql' diff --git a/src/components/QuoteImageDialog/presets.ts b/src/components/QuoteImageDialog/presets.ts new file mode 100644 index 0000000000..0a47966781 --- /dev/null +++ b/src/components/QuoteImageDialog/presets.ts @@ -0,0 +1,172 @@ +/** + * 金句卡片的風格與尺寸設定。 + * 顏色取自 thematters/design-system token;命名採中性色調。 + */ + +export type QuoteStyle = { + id: string + name: string // i18n key 後綴,對應 lang 檔的 styleXxx + swatch: string // 風格選擇器上的色塊 + bg: string + quoteColor: string + accent: string + sub: string + font: 'serif' | 'sans' + weight?: number + airy?: boolean + wide?: boolean + /** 七日書 logo 版本:深底用 white,淺底用 dark */ + logo?: 'dark' | 'white' + qrDark: string + qrLight: string +} + +export type QuoteSize = { + id: string + name: string // i18n key 後綴 + w: number + h: number +} + +export const QUOTE_STYLES: QuoteStyle[] = [ + { + id: 'warm', + name: 'Cream', + swatch: '#c0a46b', + bg: '#faf7f0', + quoteColor: '#333333', + accent: '#c0a46b', + sub: '#c58463', + font: 'serif', + logo: 'dark', + qrDark: '#c0a46b', + qrLight: '#faf7f0', + }, + { + id: 'sky', + name: 'Sky', + swatch: '#85d8ff', + bg: '#F0F9FE', + quoteColor: '#045898', + accent: '#1999D0', + sub: '#1999D0', + font: 'sans', + weight: 300, + airy: true, + logo: 'dark', + qrDark: '#1999D0', + qrLight: '#F0F9FE', + }, + { + id: 'coral', + name: 'Coral', + swatch: '#dc7871', + bg: '#ffe8e8', + quoteColor: '#333333', + accent: '#dc7871', + sub: '#d577aa', + font: 'sans', + weight: 600, + logo: 'dark', + qrDark: '#dc7871', + qrLight: '#ffe8e8', + }, + { + id: 'ink', + name: 'Ink', + swatch: '#000000', + bg: '#000000', + quoteColor: '#ffffff', + accent: '#c0a46b', + sub: '#c0a46b', + font: 'serif', + wide: true, + logo: 'white', + qrDark: '#c0a46b', + qrLight: '#000000', + }, + { + id: 'pine', + name: 'Pine', + swatch: '#0d6763', + bg: '#0d6763', + quoteColor: '#faf7f0', + accent: '#40bfa5', + sub: '#a9d9cf', + font: 'serif', + logo: 'white', + qrDark: '#faf7f0', + qrLight: '#0d6763', + }, + { + id: 'mint', + name: 'Mint', + swatch: '#70b388', + bg: 'linear-gradient(160deg,#f2faf7 0%,#f2fbd9 100%)', + quoteColor: '#246802', + accent: '#70b388', + sub: '#70b388', + font: 'serif', + logo: 'dark', + qrDark: '#246802', + qrLight: '#f7fbef', + }, + { + id: 'violet', + name: 'Violet', + swatch: '#5b5080', + bg: '#514775', + quoteColor: '#f4f1fb', + accent: '#c3b6e8', + sub: '#d6cdee', + font: 'sans', + weight: 500, + logo: 'white', + qrDark: '#f4f1fb', + qrLight: '#514775', + }, + { + id: 'slate', + name: 'Slate', + swatch: '#c9cace', + bg: '#e7e8ec', + quoteColor: '#2b2b2e', + accent: '#7f7f88', + sub: '#6f6f78', + font: 'sans', + weight: 400, + logo: 'dark', + qrDark: '#333333', + qrLight: '#e7e8ec', + }, +] + +export const QUOTE_SIZES: QuoteSize[] = [ + { id: 'square', name: 'Square', w: 1080, h: 1080 }, + { id: 'portrait', name: 'Portrait', w: 1080, h: 1350 }, + { id: 'story', name: 'Story', w: 1080, h: 1920 }, +] + +/** 金句字數上限(超過自動截斷,金句以精煉為佳) */ +export const MAX_QUOTE_LEN = 80 + +/** 依字數自動縮放字級,確保長句也塞得進安全區、不壓到頁尾 */ +export const fitFontSize = (len: number, airy?: boolean): number => { + if (len <= 16) return airy ? 72 : 78 + if (len <= 30) return airy ? 60 : 66 + if (len <= 48) return 56 + if (len <= 64) return 48 + return 42 +} + +export const clampQuote = (raw: string) => { + const text = (raw || '').trim().replace(/\s+/g, ' ') + if (text.length > MAX_QUOTE_LEN) { + return { + text: text.slice(0, MAX_QUOTE_LEN) + '…', + truncated: true, + original: text.length, + } + } + return { text, truncated: false, original: text.length } +} diff --git a/src/components/QuoteImageDialog/styles.module.css b/src/components/QuoteImageDialog/styles.module.css new file mode 100644 index 0000000000..57448d622f --- /dev/null +++ b/src/components/QuoteImageDialog/styles.module.css @@ -0,0 +1,177 @@ +/* === 金句卡片本體(固定 1080px 設計尺寸) === */ +.card { + position: relative; + display: block; + overflow: hidden; +} + +.inner { + position: absolute; + inset: 118px 110px 300px; + display: flex; + flex-direction: column; + justify-content: center; + overflow: hidden; +} + +.mark { + margin-bottom: 18px; + font-family: 'Noto Serif TC', 'Songti TC', serif; + font-size: 120px; + line-height: 0.6; + opacity: 0.9; +} + +.quote { + /* font-size / color / line-height 由元件 inline 指定 */ +} + +.author { + margin-top: 44px; + font-size: 40px; + font-weight: 600; +} + +.author .dash { + margin-right: 14px; + opacity: 0.7; +} + +.foot { + position: absolute; + right: 110px; + bottom: 90px; + left: 110px; + display: flex; + gap: 32px; + align-items: flex-end; + justify-content: space-between; +} + +.title { + display: -webkit-box; + max-width: 640px; + overflow: hidden; + -webkit-line-clamp: 2; + font-family: 'PingFang TC', sans-serif; + font-size: 30px; + line-height: 1.5; + opacity: 0.95; + -webkit-box-orient: vertical; +} + +.title .label { + display: block; + margin-bottom: 10px; + font-size: 22px; + letter-spacing: 0.2em; + opacity: 0.6; +} + +.brand { + margin-top: 14px; + font-family: 'PingFang TC', sans-serif; + font-size: 24px; + opacity: 0.7; +} + +.brandLogo { + margin-top: 16px; +} + +.brandLogo svg { + display: block; + width: auto; + height: 60px; +} + +.qr { + flex: 0 0 auto; + width: 150px; + height: 150px; + padding: 12px; + border-radius: 14px; + box-shadow: 0 4px 16px rgb(0 0 0 / 12%); +} + +.qr img { + display: block; + width: 100%; + height: 100%; +} + +/* === 對話框內容 === */ +.preview { + display: flex; + justify-content: center; + padding: var(--sp24, 24px); + background: var(--color-grey-lighter); + border-radius: var(--sp12, 12px); +} + +.stage { + transform-origin: top left; +} + +.truncHint { + margin: var(--sp12, 12px) 0 0; + font-size: var(--text13, 13px); + color: var(--color-function-warn, #dba34f); +} + +.group { + margin-top: var(--sp16, 16px); +} + +.groupLabel { + display: block; + margin-bottom: var(--sp8, 8px); + font-size: var(--text13, 13px); + font-weight: var(--font-semibold, 600); + color: var(--color-grey-darker); +} + +.opts { + display: flex; + flex-wrap: wrap; + gap: var(--sp8, 8px); +} + +.opt { + display: flex; + gap: 6px; + align-items: center; + padding: 7px 12px; + font-size: var(--text14, 14px); + cursor: pointer; + background: var(--color-white); + border: 1.5px solid var(--color-grey-light); + border-radius: 9px; +} + +.opt.active { + font-weight: var(--font-semibold, 600); + color: var(--color-brand-legacy-green, #0d6763); + background: var(--color-brand-legacy-green-lighter, #f2faf7); + border-color: var(--color-brand-legacy-green, #0d6763); +} + +.swatch { + display: inline-block; + width: 14px; + height: 14px; + border: 1px solid rgb(0 0 0 / 10%); + border-radius: 4px; +} + +.sizeName { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.sizeNote { + font-size: 11px; + font-weight: 400; + color: var(--color-grey-dark); +} diff --git a/src/components/TextSelectionPopover/index.tsx b/src/components/TextSelectionPopover/index.tsx index 6fa256a5c7..3b8e03b14d 100644 --- a/src/components/TextSelectionPopover/index.tsx +++ b/src/components/TextSelectionPopover/index.tsx @@ -1,16 +1,30 @@ import classNames from 'classnames' -import { useEffect, useRef, useState } from 'react' +import { useContext, useEffect, useRef, useState } from 'react' +import { FormattedMessage } from 'react-intl' import IconComment from '@/public/static/icons/24px/comment.svg' +import IconImage from '@/public/static/icons/24px/image.svg' import { OPEN_COMMENT_LIST_DRAWER } from '~/common/enums' import { isElementInViewport } from '~/common/utils' import { analytics } from '~/common/utils' -import { Icon, useCommentEditorContext } from '~/components' +import { + Icon, + isSevenDayBookArticle, + QuoteImageDialog, + useCommentEditorContext, + ViewerContext, +} from '~/components' +import { ArticleLicenseType, QuoteImageArticleFragment } from '~/gql/graphql' import styles from './styles.module.css' interface TextSelectionPopoverProps { targetElement: HTMLElement + article?: QuoteImageArticleFragment | null + // 'popover': floating bubble above the selection (desktop). + // 'bottomBar': fixed action bar at the bottom of the screen (mobile) — a + // floating bubble would fight with the iOS/Android native selection menu. + variant?: 'popover' | 'bottomBar' } const isSelectionCrossingParagraphs = (selection: Selection): boolean => { @@ -53,12 +67,22 @@ const isValidSelection = ( export const TextSelectionPopover = ({ targetElement, + article, + variant = 'popover', }: TextSelectionPopoverProps) => { const [selection, setSelection] = useState() + const [selectionText, setSelectionText] = useState('') const [position, setPosition] = useState>() // { x, y } const ref = useRef(null) const { fallbackEditor, getCurrentEditor } = useCommentEditorContext() const [quote, setQuote] = useState(null) + const viewer = useContext(ViewerContext) + const isBottomBar = variant === 'bottomBar' + + // 版權 gate:「作者保留所有權利」(ARR) 時僅作者本人可生成金句卡片; + // CC 系列授權允許(卡片本身已忠實引用+標註作者+帶原文連結) + const isAuthor = !!viewer.id && viewer.id === article?.author?.id + const canQuoteImage = isAuthor || article?.license !== ArticleLicenseType.Arr useEffect(() => { const editor = getCurrentEditor?.() @@ -118,6 +142,14 @@ export const TextSelectionPopover = ({ setSelection(activeSelection?.toString() || '') } + // 金句卡片用純文字 + setSelectionText(activeSelection?.toString() || '') + + // fixed bottom bar needs no positioning + if (isBottomBar) { + return + } + const rect = (activeSelection as Selection) .getRangeAt(0) .getBoundingClientRect() @@ -133,6 +165,26 @@ export const TextSelectionPopover = ({ } useEffect(() => { + // mobile: selection is made by long-press and adjusted by dragging + // handles; there is no reliable "selection ended" event, so debounce + // selectionchange instead of listening to mouseup + if (isBottomBar) { + let timer: ReturnType + const onDebouncedSelectionChange = () => { + clearTimeout(timer) + timer = setTimeout(onSelectEnd, 350) + } + + document.addEventListener('selectionchange', onDebouncedSelectionChange) + return () => { + clearTimeout(timer) + document.removeEventListener( + 'selectionchange', + onDebouncedSelectionChange + ) + } + } + document.addEventListener('selectstart', onSelectStart) document.addEventListener('mouseup', onSelectEnd) document.addEventListener('selectionchange', onSelectChange) @@ -174,6 +226,73 @@ export const TextSelectionPopover = ({ [styles.triangle]: true, }) + const quoteImageDialog = (children: React.ReactNode, className: string) => ( + + {({ openDialog }) => ( + + )} + + ) + + if (isBottomBar) { + return ( +
+ {selection && ( +
+ + + {canQuoteImage && ( + <> + + {quoteImageDialog( + <> + + + , + styles.barButton + )} + + )} +
+ )} +
+ ) + } + return (
{selection && position && ( @@ -186,10 +305,16 @@ export const TextSelectionPopover = ({ - {/* - */} + + {canQuoteImage && ( + <> + + {quoteImageDialog( + , + styles.shareButton + )} + + )}
)} diff --git a/src/components/TextSelectionPopover/styles.module.css b/src/components/TextSelectionPopover/styles.module.css index fe75f1fc50..f2484360d0 100644 --- a/src/components/TextSelectionPopover/styles.module.css +++ b/src/components/TextSelectionPopover/styles.module.css @@ -60,3 +60,42 @@ } } } + +/* mobile: fixed action bar at the bottom of the screen, so it never fights + with the iOS/Android native selection menu near the selected text */ +.bottomBar { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 100; + display: flex; + align-items: stretch; + justify-content: space-around; + padding-bottom: env(safe-area-inset-bottom); + background: var(--color-black); + + & .divider { + width: 1px; + margin: var(--sp12) 0; + background-color: var(--color-grey-darker); + } + + & .barButton { + @mixin transition; + + display: flex; + flex: 1; + gap: var(--sp8); + align-items: center; + justify-content: center; + padding: var(--sp12) var(--sp16); + font-size: var(--text14); + color: var(--color-white); + transition-property: opacity; + + &:active { + opacity: 0.6; + } + } +} diff --git a/src/components/index.tsx b/src/components/index.tsx index 4297e37892..063ca4de65 100644 --- a/src/components/index.tsx +++ b/src/components/index.tsx @@ -76,6 +76,7 @@ export * from './Interaction' export * from './Layout' export * from './Notice' export * from './Protected' +export * from './QuoteImageDialog' export * from './ReCaptcha' export * from './Search' export * from './TagDigest' diff --git a/src/views/ArticleDetail/Content/index.tsx b/src/views/ArticleDetail/Content/index.tsx index adc6172781..67c86f1a6d 100644 --- a/src/views/ArticleDetail/Content/index.tsx +++ b/src/views/ArticleDetail/Content/index.tsx @@ -13,7 +13,7 @@ import { ViewerContext, } from '~/components' import { useReadTimer } from '~/components/Hook' -import { ReadArticleMutation } from '~/gql/graphql' +import { QuoteImageArticleFragment, ReadArticleMutation } from '~/gql/graphql' import styles from './styles.module.css' @@ -27,10 +27,12 @@ const READ_ARTICLE = gql` const Content = ({ articleId, + article, content, indentFirstLine, }: { articleId: string + article?: QuoteImageArticleFragment | null content: string indentFirstLine: boolean }) => { @@ -151,6 +153,20 @@ const Content = ({ contentContainer.current && ( + )} + + {/* mobile: a floating bubble would fight with the native selection + menu, so render a fixed bottom action bar instead */} + + {!isInArticleDetailHistory && + isContentMounted && + contentContainer.current && ( + )} diff --git a/src/views/ArticleDetail/gql.ts b/src/views/ArticleDetail/gql.ts index 278cd5e3b9..cbf3d2b0a7 100644 --- a/src/views/ArticleDetail/gql.ts +++ b/src/views/ArticleDetail/gql.ts @@ -1,5 +1,6 @@ import gql from 'graphql-tag' +import { fragments as quoteImageFragments } from '~/components/QuoteImageDialog/gql' import { UserDigest } from '~/components/UserDigest' import { AuthorSidebar } from './AuthorSidebar' @@ -77,7 +78,9 @@ const articlePublicFragment = gql` ...SupportWidgetArticlePrivate ...ChannelArticlePublic ...ChannelArticlePrivate + ...QuoteImageArticle } + ${quoteImageFragments.article} ${Header.fragments.article} ${AuthorSidebar.fragments.article} ${MetaInfo.fragments.article} diff --git a/src/views/ArticleDetail/index.tsx b/src/views/ArticleDetail/index.tsx index 49af917aca..22c07e0349 100644 --- a/src/views/ArticleDetail/index.tsx +++ b/src/views/ArticleDetail/index.tsx @@ -355,6 +355,7 @@ const BaseArticleDetail = ({ <>