diff --git a/__tests__/lib/__mocks__/exampleComHtml.ts b/__tests__/lib/__mocks__/exampleComHtml.ts
new file mode 100644
index 0000000000..6633e40ca5
--- /dev/null
+++ b/__tests__/lib/__mocks__/exampleComHtml.ts
@@ -0,0 +1,47 @@
+export const exampleComHtml = `
+
+
+ Example Domain
+
+
+
+
+
+
+
+
+
+
+
Example Domain
+
This domain is for use in illustrative examples in documents. You may use this
+ domain in literature without prior coordination or asking for permission.
+
More information...
+
+
+`
diff --git a/__tests__/lib/__mocks__/youtubeHtml.ts b/__tests__/lib/__mocks__/youtubeHtml.ts
new file mode 100644
index 0000000000..7fd9f819da
--- /dev/null
+++ b/__tests__/lib/__mocks__/youtubeHtml.ts
@@ -0,0 +1,19 @@
+export const youtubeHTML = `
+
+YouTube
+
+`
diff --git a/__tests__/lib/extractMetaHtml.test.ts b/__tests__/lib/extractMetaHtml.test.ts
new file mode 100644
index 0000000000..4ac653db01
--- /dev/null
+++ b/__tests__/lib/extractMetaHtml.test.ts
@@ -0,0 +1,70 @@
+import {extractHtmlMeta} from '../../src/lib/extractHtmlMeta'
+import {exampleComHtml} from './__mocks__/exampleComHtml'
+import {youtubeHTML} from './__mocks__/youtubeHtml'
+
+describe('extractHtmlMeta', () => {
+ const cases = [
+ ['', {}],
+ ['nothing', {}],
+ ['title ', {title: 'title'}],
+ [' aSd!@#AC ', {title: 'aSd!@#AC'}],
+ ['\n title\n ', {title: 'title'}],
+ [' ', {title: 'meta title'}],
+ [
+ ' ',
+ {description: 'meta description'},
+ ],
+ [' ', {title: 'og title'}],
+ [
+ ' ',
+ {description: 'og description'},
+ ],
+ [
+ ' ',
+ {image: 'https://ogimage.com/foo.png'},
+ ],
+ [
+ ' ',
+ {title: 'twitter title'},
+ ],
+ [
+ ' ',
+ {description: 'twitter description'},
+ ],
+ [
+ ' ',
+ {image: 'https://twitterimage.com/foo.png'},
+ ],
+ [' ', {title: 'meta title'}],
+ ]
+
+ it.each(cases)(
+ 'given the html tag %p, returns %p',
+ (input, expectedResult) => {
+ const output = extractHtmlMeta(input)
+ expect(output).toEqual(expectedResult)
+ },
+ )
+
+ it('extracts title and description from a generic HTML page', () => {
+ const input = exampleComHtml
+ const expectedOutput = {
+ title: 'Example Domain',
+ description: 'An example website',
+ }
+ const output = extractHtmlMeta(input)
+ expect(output).toEqual(expectedOutput)
+ })
+
+ it('extracts title and description from a generic youtube page', () => {
+ const input = youtubeHTML
+ const expectedOutput = {
+ title: 'HD Video (1080p) with Relaxing Music of Native American Shamans',
+ description:
+ 'Stunning HD Video ( 1080p ) of Patagonian Nature with Relaxing Native American Shamanic Music. HD footage used from ',
+ image: 'https://i.ytimg.com/vi/x6UITRjhijI/sddefault.jpg',
+ }
+ const output = extractHtmlMeta(input)
+ expect(output).toEqual(expectedOutput)
+ })
+})
diff --git a/__tests__/lib/link-meta.test.ts b/__tests__/lib/link-meta.test.ts
index 5931a4ccba..eaf8732b2d 100644
--- a/__tests__/lib/link-meta.test.ts
+++ b/__tests__/lib/link-meta.test.ts
@@ -1,54 +1,7 @@
import {LikelyType, getLinkMeta, getLikelyType} from '../../src/lib/link-meta'
+import {exampleComHtml} from './__mocks__/exampleComHtml'
import {mockedRootStore} from '../../__mocks__/state-mock'
-const exampleComHtml = `
-
-
- Example Domain
-
-
-
-
-
-
-
-
-
-
-
Example Domain
-
This domain is for use in illustrative examples in documents. You may use this
- domain in literature without prior coordination or asking for permission.
-
More information...
-
-
-`
-
describe('getLinkMeta', () => {
const inputs = [
'',
diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts
index d8a56b36b6..0032ebf313 100644
--- a/__tests__/lib/string.test.ts
+++ b/__tests__/lib/string.test.ts
@@ -1,7 +1,6 @@
import {
extractEntities,
detectLinkables,
- extractHtmlMeta,
pluralize,
makeRecordUri,
ago,
@@ -286,48 +285,6 @@ describe('detectLinkables', () => {
})
})
-describe('extractHtmlMeta', () => {
- const inputs = [
- '',
- 'nothing',
- 'title ',
- ' aSd!@#AC ',
- '\n title\n ',
- ' ',
- ' ',
- ' ',
- ' ',
- ' ',
- ' ',
- ' ',
- ' ',
- ' ',
- ]
- const outputs = [
- {},
- {},
- {title: 'title'},
- {title: 'aSd!@#AC'},
- {title: 'title'},
- {title: 'meta title'},
- {description: 'meta description'},
- {title: 'og title'},
- {description: 'og description'},
- {image: 'https://ogimage.com/foo.png'},
- {title: 'twitter title'},
- {description: 'twitter description'},
- {image: 'https://twitterimage.com/foo.png'},
- {title: 'meta title'},
- ]
- it('correctly handles a set of text inputs', () => {
- for (let i = 0; i < inputs.length; i++) {
- const input = inputs[i]
- const output = extractHtmlMeta(input)
- expect(output).toEqual(outputs[i])
- }
- })
-})
-
describe('pluralize', () => {
const inputs: [number, string, string?][] = [
[1, 'follower'],
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 80249b61a5..50e15459fb 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -565,13 +565,13 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
- boost: a7c83b31436843459a1961bfd74b96033dc77234
+ boost: 57d2868c099736d80fcd648bf211b4431e51a558
BVLinearGradient: 34a999fda29036898a09c6a6b728b0b4189e1a44
- DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
+ DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 61839cba7a48c570b7ac3e1cd8a4d0948382202f
FBReactNativeSpec: 5a14398ccf5e27c1ca2d7109eb920594ce93c10d
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
- glog: 476ee3e89abb49e07f822b48323c51c57124b572
+ glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: f6e715aa6c8bd38de6c13bc85e07b0a337edaa89
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
diff --git a/package.json b/package.json
index 109fb32792..fd2f4b987c 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"start": "react-native start",
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
"test": "jest",
+ "test-watch": "jest --watchAll",
"test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "jest --coverage",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
@@ -114,6 +115,7 @@
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|rollbar-react-native|@fortawesome|@react-native|@react-navigation)"
],
+ "modulePathIgnorePatterns": ["__tests__\/.*\/__mocks__"],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/src/platform",
diff --git a/src/lib/extractHtmlMeta.ts b/src/lib/extractHtmlMeta.ts
new file mode 100644
index 0000000000..2517262bee
--- /dev/null
+++ b/src/lib/extractHtmlMeta.ts
@@ -0,0 +1,65 @@
+import {extractYoutubeMeta} from './extractYoutubeMeta'
+
+interface ExtractHtmlMetaInput {
+ html: string
+ hostname?: string
+}
+
+export const extractHtmlMeta = ({
+ html,
+ hostname,
+}: ExtractHtmlMetaInput): Record => {
+ const htmlTitleRegex = /([^<]+)<\/title>/i
+
+ let res: Record = {}
+
+ const match = htmlTitleRegex.exec(html)
+
+ if (match) {
+ res.title = match[1].trim()
+ }
+
+ let metaMatch
+ let propMatch
+ const metaRe = / ]+)>/gis
+ while ((metaMatch = metaRe.exec(html))) {
+ let propName
+ let propValue
+ const propRe = /(name|property|content)="([^"]+)"/gis
+ while ((propMatch = propRe.exec(metaMatch[1]))) {
+ if (propMatch[1] === 'content') {
+ propValue = propMatch[2]
+ } else {
+ propName = propMatch[2]
+ }
+ }
+ if (!propName || !propValue) {
+ continue
+ }
+ switch (propName?.trim()) {
+ case 'title':
+ case 'og:title':
+ case 'twitter:title':
+ res.title = propValue?.trim()
+ break
+ case 'description':
+ case 'og:description':
+ case 'twitter:description':
+ res.description = propValue?.trim()
+ break
+ case 'og:image':
+ case 'twitter:image':
+ res.image = propValue?.trim()
+ break
+ }
+ }
+
+ const isYoutubeUrl =
+ hostname?.includes('youtube.') || hostname?.includes('youtu.be')
+ if (isYoutubeUrl) {
+ // Workaround for Youtube not having a title in the meta tags
+ res = {...res, ...extractYoutubeMeta(html)}
+ }
+
+ return res
+}
diff --git a/src/lib/extractYoutubeMeta.ts b/src/lib/extractYoutubeMeta.ts
new file mode 100644
index 0000000000..566e3be46d
--- /dev/null
+++ b/src/lib/extractYoutubeMeta.ts
@@ -0,0 +1,26 @@
+export const extractYoutubeMeta = (html: string): Record => {
+ const res: Record = {}
+ const youtubeTitleRegex = /"videoDetails":.*"title":"([^"]*)"/i
+ const youtubeDescriptionRegex =
+ /"videoDetails":.*"shortDescription":"([^"]*)"/i
+ const youtubeThumbnailRegex = /"videoDetails":.*"url":"(.*)(default\.jpg)/i
+
+ const youtubeTitleMatch = youtubeTitleRegex.exec(html)
+ const youtubeDescriptionMatch = youtubeDescriptionRegex.exec(html)
+ const youtubeThumbnailMatch = youtubeThumbnailRegex.exec(html)
+
+ if (youtubeTitleMatch && youtubeTitleMatch.length >= 1) {
+ res.title = decodeURI(youtubeTitleMatch[1])
+ }
+ if (youtubeDescriptionMatch && youtubeDescriptionMatch.length >= 1) {
+ res.description = decodeURI(youtubeDescriptionMatch[1]).replace(
+ /\\n/g,
+ '\n',
+ )
+ }
+ if (youtubeThumbnailMatch && youtubeThumbnailMatch.length >= 2) {
+ res.image = youtubeThumbnailMatch[1] + 'default.jpg'
+ }
+
+ return res
+}
diff --git a/src/lib/link-meta.ts b/src/lib/link-meta.ts
index 9a0325c8f2..7e0964c17a 100644
--- a/src/lib/link-meta.ts
+++ b/src/lib/link-meta.ts
@@ -1,7 +1,8 @@
import he from 'he'
-import {extractHtmlMeta, isBskyAppUrl} from './strings'
+import {isBskyAppUrl} from './strings'
import {RootStoreModel} from '../state'
import {extractBskyMeta} from './extractBskyMeta'
+import {extractHtmlMeta} from './extractHtmlMeta'
export enum LikelyType {
HTML,
@@ -59,7 +60,10 @@ export async function getLinkMeta(
})
const httpResBody = await httpRes.text()
clearTimeout(to)
- const httpResMeta = extractHtmlMeta(httpResBody)
+ const httpResMeta = extractHtmlMeta({
+ html: httpResBody,
+ hostname: urlp?.hostname,
+ })
meta.title = httpResMeta.title ? he.decode(httpResMeta.title) : undefined
meta.description = httpResMeta.description
? he.decode(httpResMeta.description)
diff --git a/src/lib/strings.ts b/src/lib/strings.ts
index 04d8656f7a..77fe222e4b 100644
--- a/src/lib/strings.ts
+++ b/src/lib/strings.ts
@@ -265,54 +265,3 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
}
return url
}
-
-const htmlTitleRegex = /([^<]+)<\/title>/i
-export function extractHtmlMeta(html: string): Record {
- const res: Record = {}
-
- {
- const match = htmlTitleRegex.exec(html)
- if (match) {
- res.title = match[1].trim()
- }
- }
-
- {
- let metaMatch
- let propMatch
- const metaRe = / ]+)>/gis
- while ((metaMatch = metaRe.exec(html))) {
- let propName
- let propValue
- const propRe = /(name|property|content)="([^"]+)"/gis
- while ((propMatch = propRe.exec(metaMatch[1]))) {
- if (propMatch[1] === 'content') {
- propValue = propMatch[2]
- } else {
- propName = propMatch[2]
- }
- }
- if (!propName || !propValue) {
- continue
- }
- switch (propName?.trim()) {
- case 'title':
- case 'og:title':
- case 'twitter:title':
- res.title = propValue?.trim()
- break
- case 'description':
- case 'og:description':
- case 'twitter:description':
- res.description = propValue?.trim()
- break
- case 'og:image':
- case 'twitter:image':
- res.image = propValue?.trim()
- break
- }
- }
- }
-
- return res
-}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 54823d8443..4133c17d42 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -39,7 +39,9 @@ export const FeedItem = observer(function ({
const itemTitle = `Post by ${item.post.author.handle}`
const authorHref = `/profile/${item.post.author.handle}`
const replyAuthorDid = useMemo(() => {
- if (!record?.reply) return ''
+ if (!record?.reply) {
+ return ''
+ }
const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
return urip.hostname
}, [record?.reply])
@@ -196,7 +198,9 @@ export const FeedItem = observer(function ({
) : (
)}
-
+ {item.post.embed ? (
+
+ ) : null}