Compare commits

...

5 Commits

Author SHA1 Message Date
hailey 70ec4767a5 improve og profile card (#9051) 2025-09-22 18:38:41 -07:00
Caidan f0d6a74921 improve: enhance post OpenGraph metadata with engagement data and auth handling (#9046) 2025-09-17 14:31:06 -07:00
Samuel Newman 0a6eddb1ab Add missing passive feed interactions (#9043) 2025-09-15 03:03:31 -07:00
hailey 1c855603d2 ensure mod service header present for appeals (#9025) 2025-09-11 00:32:06 -07:00
Samuel Newman 6a6c13d147 remove link from saved count (#9010)
(cherry picked from commit d8413b09f8)
2025-09-08 15:17:18 -05:00
7 changed files with 124 additions and 94 deletions
+17 -7
View File
@@ -394,6 +394,7 @@ func (srv *Server) Shutdown() error {
func (srv *Server) NewTemplateContext() pongo2.Context {
return pongo2.Context{
"staticCDNHost": srv.cfg.staticCDNHost,
"favicon": fmt.Sprintf("%s/static/favicon.png", srv.cfg.staticCDNHost),
}
}
@@ -488,20 +489,26 @@ func (srv *Server) WebPost(c echo.Context) error {
}
}
req := c.Request()
if !unauthedViewingOkay {
// Provide minimal OpenGraph data for auth-required posts
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
data["requiresAuth"] = true
data["profileHandle"] = pv.Handle
if pv.DisplayName != nil {
data["profileDisplayName"] = *pv.DisplayName
}
return c.Render(http.StatusOK, "post.html", data)
}
did := pv.Did
data["did"] = did
// then fetch the post thread (with extra context)
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", pv.Did, rkey)
tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 0, uri)
if err != nil {
log.Warnf("failed to fetch post: %s\t%v", uri, err)
return c.Render(http.StatusOK, "post.html", data)
}
req := c.Request()
postView := tpv.Thread.FeedDefs_ThreadViewPost.Post
data["postView"] = postView
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
@@ -593,13 +600,16 @@ func (srv *Server) WebProfile(c echo.Context) error {
unauthedViewingOkay = false
}
}
if !unauthedViewingOkay {
return c.Render(http.StatusOK, "profile.html", data)
}
req := c.Request()
data["profileView"] = pv
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
data["requestHost"] = req.Host
if !unauthedViewingOkay {
data["requiresAuth"] = true
}
return c.Render(http.StatusOK, "profile.html", data)
}
+2 -1
View File
@@ -94,7 +94,8 @@
<meta name="theme-color">
<meta name="application-name" content="Bluesky">
<meta name="generator" content="bskyweb">
<meta property="og:site_name" content="Bluesky Social" />
<meta property="og:site_name" content="Bluesky Social">
<meta property="og:logo" content="{{ favicon }}">
<meta name="twitter:site" content="@bluesky" />
<link type="application/activity+json" href="" />
+40 -5
View File
@@ -3,6 +3,8 @@
{% block head_title %}
{%- if postView -%}
@{{ postView.Author.Handle }} on Bluesky
{%- elif requiresAuth and profileHandle -%}
@{{ profileHandle }} on Bluesky
{%- else -%}
Bluesky
{%- endif -%}
@@ -11,7 +13,7 @@
{% block html_head_extra -%}
{%- if postView -%}
<meta property="og:type" content="article">
<meta property="profile:username" content="{{ profileView.Handle }}">
<meta property="profile:username" content="{{ postView.Author.Handle }}">
{%- if requestURI %}
<meta property="og:url" content="{{ requestURI }}">
<link rel="canonical" href="{{ requestURI|canonicalize_url }}" />
@@ -32,17 +34,44 @@
<meta property="twitter:image" content="{{ imgThumbUrl }}">
{% endfor %}
<meta name="twitter:card" content="summary_large_image">
{%- elif postView.Author.Avatar %}
<meta name="twitter:card" content="summary">
{% else %}
<meta property="og:image" content="{{ postView.Author.Avatar }}">
<meta property="twitter:image" content="{{ postView.Author.Avatar }}">
<meta name="twitter:card" content="summary">
{% endif %}
<meta name="twitter:label1" content="Posted At">
<meta name="twitter:value1" content="{{ postView.IndexedAt }}">
<meta name="article:published_time" content="{{ postView.IndexedAt }}">
<meta name="article:published_time" content="{{ postView.IndexedAt }}">
{%- if postView.LikeCount %}
<meta name="twitter:label2" content="Likes">
<meta name="twitter:value2" content="{{ postView.LikeCount }}">
{% endif -%}
{%- if postView.ReplyCount %}
<meta name="twitter:label3" content="Replies">
<meta name="twitter:value3" content="{{ postView.ReplyCount }}">
{% endif -%}
{%- if postView.RepostCount %}
<meta name="twitter:label4" content="Reposts">
<meta name="twitter:value4" content="{{ postView.RepostCount }}">
{% endif -%}
<meta property="article:published_time" content="{{ postView.IndexedAt }}">
<link rel="alternate" type="application/json+oembed" href="https://embed.bsky.app/oembed?format=json&url={{ postView.Uri | urlencode }}" />
<link rel="alternate" href="{{ postView.Uri }}" />
{%- elif requiresAuth and profileHandle -%}
<meta property="og:type" content="article">
<meta property="profile:username" content="{{ profileHandle }}">
{%- if requestURI %}
<meta property="og:url" content="{{ requestURI }}">
<link rel="canonical" href="{{ requestURI|canonicalize_url }}" />
{% endif -%}
{%- if profileDisplayName %}
<meta property="og:title" content="{{ profileDisplayName }} (@{{ profileHandle }})">
{% else %}
<meta property="og:title" content="@{{ profileHandle }}">
{% endif -%}
<meta name="description" content="This post requires authentication to view.">
<meta property="og:description" content="This post requires authentication to view.">
<meta property="twitter:description" content="This post requires authentication to view.">
<meta name="twitter:card" content="summary">
{% endif -%}
{%- endblock %}
@@ -56,5 +85,11 @@
<p id="bsky_post_text">{{ postText }}</p>
<p id="bsky_post_indexedat">{{ postView.IndexedAt }}</p>
</div>
{%- elif requiresAuth and profileHandle -%}
<div id="bsky_post_summary">
<h3>Post</h3>
<p id="bsky_handle">{{ profileHandle }}</p>
<p id="bsky_post_text">This post requires authentication to view.</p>
</div>
{% endif -%}
{%- endblock %}
+28 -16
View File
@@ -9,36 +9,48 @@
{% endblock %}
{% block html_head_extra -%}
{%- if profileView -%}
<meta property="og:site_name" content="Bluesky Social">
<meta property="og:type" content="profile">
<meta property="profile:username" content="{{ profileView.Handle }}">
{%- if requestURI %}
{%- if requestURI %}
<meta property="og:url" content="{{ requestURI }}">
<link rel="canonical" href="{{ requestURI|canonicalize_url }}" />
{% endif -%}
{% endif -%}
{%- if profileView -%}
<meta property="profile:username" content="{{ profileView.Handle }}">
{%- if profileView.DisplayName %}
<meta property="og:title" content="{{ profileView.DisplayName }} (@{{ profileView.Handle }})">
{% else %}
<meta property="og:title" content="{{ profileView.Handle }}">
{% endif -%}
{%- if profileView.Description %}
<meta name="description" content="{{ profileView.Description }}">
<meta property="og:description" content="{{ profileView.Description }}">
{% endif -%}
{%- if profileView.Banner %}
<meta property="og:image" content="{{ profileView.Banner }}">
<meta name="twitter:card" content="summary_large_image">
{%- elif profileView.Avatar -%}
{# Don't use avatar image in cards; usually looks bad #}
<meta name="twitter:card" content="summary">
{% endif %}
<link rel="alternate" href="at://{{ profileView.Did }}/app.bsky.actor.profile/self" />
<meta name="twitter:label1" content="Account DID">
<meta name="twitter:value1" content="{{ profileView.Did }}">
{%- if requestHost %}
<link rel="alternate" type="application/rss+xml" href="https://{{ requestHost }}/profile/{{ profileView.Did }}/rss">
{% endif %}
<link rel="alternate" href="at://{{ profileView.Did }}/app.bsky.actor.profile/self" />
{# Only show details if auth isn't required #}
{% if not requiresAuth %}
{%- if profileView.Description %}
<meta name="description" content="{{ profileView.Description }}">
<meta property="og:description" content="{{ profileView.Description }}">
{% endif -%}
{%- if profileView.Banner %}
<meta property="og:image" content="{{ profileView.Banner }}">
<meta name="twitter:card" content="summary_large_image">
{%- elif profileView.Avatar -%}
{# Don't use avatar image in cards; usually looks bad #}
<meta name="twitter:card" content="summary">
{% endif %}
{% else %}
<meta name="description" content="This profile requires authentication to view.">
<meta property="og:description" content="This profile requires authentication to view.">
<meta property="twitter:description" content="This profile requires authentication to view.">
{% endif %}
{% endif -%}
{%- endblock %}
@@ -481,20 +481,14 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
</Link>
) : null}
{post.bookmarkCount != null && post.bookmarkCount !== 0 ? (
<Link to={likesHref} label={_(msg`Saves of this post`)}>
<Text
testID="bookmarkCount-expanded"
style={[a.text_md, t.atoms.text_contrast_medium]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{formatPostStatCount(post.bookmarkCount)}
</Text>{' '}
<Plural
value={post.bookmarkCount}
one="save"
other="saves"
/>
</Text>
</Link>
<Text
testID="bookmarkCount-expanded"
style={[a.text_md, t.atoms.text_contrast_medium]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{formatPostStatCount(post.bookmarkCount)}
</Text>{' '}
<Plural value={post.bookmarkCount} one="save" other="saves" />
</Text>
) : null}
</View>
) : null}
+18 -9
View File
@@ -9,7 +9,10 @@ import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
import Graphemer from 'graphemer'
import {MAX_REPORT_REASON_GRAPHEME_LENGTH} from '#/lib/constants'
import {
BLUESKY_MOD_SERVICE_HEADERS,
MAX_REPORT_REASON_GRAPHEME_LENGTH,
} from '#/lib/constants'
import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController'
import {cleanError} from '#/lib/strings/errors'
import {isIOS, isWeb} from '#/platform/detection'
@@ -49,14 +52,20 @@ export function Takendown() {
} = useMutation({
mutationFn: async (appealText: string) => {
if (!currentAccount) throw new Error('No session')
await agent.com.atproto.moderation.createReport({
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: currentAccount.did,
} satisfies ComAtprotoAdminDefs.RepoRef,
reason: appealText,
})
await agent.com.atproto.moderation.createReport(
{
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: currentAccount.did,
} satisfies ComAtprotoAdminDefs.RepoRef,
reason: appealText,
},
{
encoding: 'application/json',
headers: BLUESKY_MOD_SERVICE_HEADERS,
},
)
},
onSuccess: () => setReason(''),
})
+11 -42
View File
@@ -28,39 +28,9 @@ import {useAgent} from './session'
export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
export const PASSIVE_FEEDBACK_INTERACTIONS = [
'app.bsky.feed.defs#clickthroughItem',
'app.bsky.feed.defs#clickthroughAuthor',
'app.bsky.feed.defs#clickthroughReposter',
'app.bsky.feed.defs#clickthroughEmbed',
'app.bsky.feed.defs#interactionSeen',
] as const
export type PassiveFeedbackInteraction =
(typeof PASSIVE_FEEDBACK_INTERACTIONS)[number]
export const DIRECT_FEEDBACK_INTERACTIONS = [
'app.bsky.feed.defs#requestLess',
'app.bsky.feed.defs#requestMore',
] as const
export type DirectFeedbackInteraction =
(typeof DIRECT_FEEDBACK_INTERACTIONS)[number]
export const ALL_FEEDBACK_INTERACTIONS = [
...PASSIVE_FEEDBACK_INTERACTIONS,
...DIRECT_FEEDBACK_INTERACTIONS,
] as const
export type FeedbackInteraction = (typeof ALL_FEEDBACK_INTERACTIONS)[number]
export function isFeedbackInteraction(
interactionEvent: string,
): interactionEvent is FeedbackInteraction {
return ALL_FEEDBACK_INTERACTIONS.includes(
interactionEvent as FeedbackInteraction,
)
}
export const DIRECT_FEEDBACK_INTERACTIONS = new Set<
AppBskyFeedDefs.Interaction['event']
>(['app.bsky.feed.defs#requestLess', 'app.bsky.feed.defs#requestMore'])
const logger = Logger.create(Logger.Context.FeedFeedback)
@@ -97,7 +67,6 @@ export function useFeedFeedback(
const proxyDid = feed?.view?.did
const enabled =
Boolean(feed) && Boolean(proxyDid) && acceptsInteractions && hasSession
const enabledInteractions = getEnabledInteractions(enabled, feed, isDiscover)
const queue = useRef<Set<string>>(new Set())
const history = useRef<
@@ -123,8 +92,7 @@ export function useFeedFeedback(
const interactionsToSend = interactions.filter(
interaction =>
interaction.event &&
isFeedbackInteraction(interaction.event) &&
enabledInteractions.includes(interaction.event),
isInteractionAllowed(enabled, feed, interaction.event),
)
if (interactionsToSend.length === 0) {
@@ -158,7 +126,7 @@ export function useFeedFeedback(
)
throttledFlushAggregatedStats()
logger.debug('flushed')
}, [agent, throttledFlushAggregatedStats, proxyDid, enabledInteractions])
}, [agent, throttledFlushAggregatedStats, proxyDid, enabled, feed])
const sendToFeed = useMemo(
() =>
@@ -251,15 +219,16 @@ export function isDiscoverFeed(feed?: FeedDescriptor) {
return !!feed && FEEDBACK_FEEDS.includes(feed)
}
function getEnabledInteractions(
function isInteractionAllowed(
enabled: boolean,
feed: FeedSourceFeedInfo | undefined,
isDiscover: boolean,
): readonly FeedbackInteraction[] {
interaction: AppBskyFeedDefs.Interaction['event'],
) {
if (!enabled || !feed) {
return []
return false
}
return isDiscover ? ALL_FEEDBACK_INTERACTIONS : DIRECT_FEEDBACK_INTERACTIONS
const isDiscover = isDiscoverFeed(feed.feedDescriptor)
return isDiscover ? true : DIRECT_FEEDBACK_INTERACTIONS.has(interaction)
}
function toString(interaction: AppBskyFeedDefs.Interaction): string {