diff --git a/assets/icons/codeBrackets_stroke2_corner0_rounded.svg b/assets/icons/codeBrackets_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..0cc239210e
--- /dev/null
+++ b/assets/icons/codeBrackets_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/bskyembed/src/screens/landing.tsx b/bskyembed/src/screens/landing.tsx
index 88e84ffb67..7c8ef28108 100644
--- a/bskyembed/src/screens/landing.tsx
+++ b/bskyembed/src/screens/landing.tsx
@@ -159,6 +159,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
return ''
}
+ const lang = record.langs && record.langs.length > 0 ? record.langs[0] : ''
const profileHref = toShareUrl(
['/profile', thread.post.author.did].join('/'),
)
@@ -167,10 +168,9 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
['/profile', thread.post.author.did, 'post', urip.rkey].join('/'),
)
- const lang = record.langs ? record.langs[0] : ''
-
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
- // DO NOT ADD ANY NEW INTERPOLATIOONS BELOW WITHOUT ESCAPING THEM!
+ // DO NOT ADD ANY NEW INTERPOLATIONS BELOW WITHOUT ESCAPING THEM!
+ // Also, keep this code synced with the app code in Embed.tsx.
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
return `
${escapeHtml(record.text)}${
+ record.embed
+ ? `
[image or embed]`
+ : ''
+ }
— ${escapeHtml(
+ postAuthor.displayName || postAuthor.handle,
+ )} (@${escapeHtml(
+ postAuthor.handle,
+ )}) ${escapeHtml(
+ niceDate(timestamp),
+ )}
`
+ }, [postUri, postCid, record, timestamp, postAuthor])
+
+ return (
+
+
+
+ Embed post
+
+
+
+ Embed this post in your website. Simply copy the following snippet
+ and paste it into the HTML code of your website.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+/**
+ * Based on a snippet of code from React, which itself was based on the escape-html library.
+ * Copyright (c) Meta Platforms, Inc. and affiliates
+ * Copyright (c) 2012-2013 TJ Holowaychuk
+ * Copyright (c) 2015 Andreas Lubbe
+ * Copyright (c) 2015 Tiancheng "Timothy" Gu
+ * Licensed as MIT.
+ */
+const matchHtmlRegExp = /["'&<>]/
+function escapeHtml(string: string) {
+ const str = String(string)
+ const match = matchHtmlRegExp.exec(str)
+ if (!match) {
+ return str
+ }
+ let escape
+ let html = ''
+ let index
+ let lastIndex = 0
+ for (index = match.index; index < str.length; index++) {
+ switch (str.charCodeAt(index)) {
+ case 34: // "
+ escape = '"'
+ break
+ case 38: // &
+ escape = '&'
+ break
+ case 39: // '
+ escape = '''
+ break
+ case 60: // <
+ escape = '<'
+ break
+ case 62: // >
+ escape = '>'
+ break
+ default:
+ continue
+ }
+ if (lastIndex !== index) {
+ html += str.slice(lastIndex, index)
+ }
+ lastIndex = index + 1
+ html += escape
+ }
+ return lastIndex !== index ? html + str.slice(lastIndex, index) : html
+}
diff --git a/src/components/icons/CodeBrackets.tsx b/src/components/icons/CodeBrackets.tsx
new file mode 100644
index 0000000000..59d5fca900
--- /dev/null
+++ b/src/components/icons/CodeBrackets.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const CodeBrackets_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M14.242 3.03a1 1 0 0 1 .728 1.213l-4 16a1 1 0 1 1-1.94-.485l4-16a1 1 0 0 1 1.213-.728ZM6.707 7.293a1 1 0 0 1 0 1.414L3.414 12l3.293 3.293a1 1 0 1 1-1.414 1.414l-4-4a1 1 0 0 1 0-1.414l4-4a1 1 0 0 1 1.414 0Zm10.586 0a1 1 0 0 1 1.414 0l4 4a1 1 0 0 1 0 1.414l-4 4a1 1 0 1 1-1.414-1.414L20.586 12l-3.293-3.293a1 1 0 0 1 0-1.414Z',
+})
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 401c39362b..bb49387c4c 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -7,6 +7,8 @@ export const BSKY_SERVICE = 'https://bsky.social'
export const DEFAULT_SERVICE = BSKY_SERVICE
const HELP_DESK_LANG = 'en-us'
export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}`
+export const EMBED_SERVICE = 'https://embed.bsky.app'
+export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
export function FEEDBACK_FORM_URL({
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index 04dfa203a1..31032396f3 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -28,12 +28,14 @@ import {getCurrentRoute} from 'lib/routes/helpers'
import {shareUrl} from 'lib/sharing'
import {toShareUrl} from 'lib/strings/url-helpers'
import {useTheme} from 'lib/ThemeContext'
-import {atoms as a, useTheme as useAlf} from '#/alf'
+import {atoms as a, useBreakpoints, useTheme as useAlf} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
+import {EmbedDialog} from '#/components/dialogs/Embed'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
+import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
@@ -55,6 +57,7 @@ let PostDropdownBtn = ({
richText,
style,
hitSlop,
+ timestamp,
}: {
testID: string
postAuthor: AppBskyActorDefs.ProfileViewBasic
@@ -64,10 +67,12 @@ let PostDropdownBtn = ({
richText: RichTextAPI
style?: StyleProp
hitSlop?: PressableProps['hitSlop']
+ timestamp: string
}): React.ReactNode => {
const {hasSession, currentAccount} = useSession()
const theme = useTheme()
const alf = useAlf()
+ const {gtMobile} = useBreakpoints()
const {_} = useLingui()
const defaultCtrlColor = theme.palette.default.postCtrl
const langPrefs = useLanguagePrefs()
@@ -83,6 +88,7 @@ let PostDropdownBtn = ({
const deletePromptControl = useDialogControl()
const hidePromptControl = useDialogControl()
const loggedOutWarningPromptControl = useDialogControl()
+ const embedPostControl = useDialogControl()
const rootUri = record.reply?.root?.uri || postUri
const isThreadMuted = mutedThreads.includes(rootUri)
@@ -177,6 +183,8 @@ let PostDropdownBtn = ({
shareUrl(url)
}, [href])
+ const canEmbed = isWeb && gtMobile && !shouldShowLoggedOutWarning
+
return (
@@ -238,6 +246,16 @@ let PostDropdownBtn = ({
+
+ {canEmbed && (
+
+ {_(msg`Embed post`)}
+
+
+ )}
{hasSession && (
@@ -350,6 +368,17 @@ let PostDropdownBtn = ({
onConfirm={onSharePost}
confirmButtonCta={_(msg`Share anyway`)}
/>
+
+ {canEmbed && (
+
+ )}
)
}
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index cd4a363730..cb50ee6dc3 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -264,6 +264,7 @@ let PostCtrls = ({
richText={richText}
style={styles.btnPad}
hitSlop={big ? HITSLOP_20 : HITSLOP_10}
+ timestamp={post.indexedAt}
/>