Improve link meta fetching for bsky links (#54)
* Add share dropdown item to profiles * Temporary improvement for links to content on the network * Enlarge text slightly on embed cardszio/stable
parent
0536a6afcf
commit
9230d52ff5
|
@ -1,4 +1,5 @@
|
||||||
import {LikelyType, getLinkMeta, getLikelyType} from '../../src/lib/link-meta'
|
import {LikelyType, getLinkMeta, getLikelyType} from '../../src/lib/link-meta'
|
||||||
|
import {mockedRootStore} from '../../__mocks__/state-mock'
|
||||||
|
|
||||||
const exampleComHtml = `<!doctype html>
|
const exampleComHtml = `<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
@ -59,6 +60,7 @@ describe('getLinkMeta', () => {
|
||||||
'https://example.com/audio.ogg',
|
'https://example.com/audio.ogg',
|
||||||
'https://example.com/text.txt',
|
'https://example.com/text.txt',
|
||||||
'https://example.com/javascript.js',
|
'https://example.com/javascript.js',
|
||||||
|
'https://bsky.app/',
|
||||||
'https://bsky.app/index.html',
|
'https://bsky.app/index.html',
|
||||||
]
|
]
|
||||||
const outputs = [
|
const outputs = [
|
||||||
|
@ -104,6 +106,12 @@ describe('getLinkMeta', () => {
|
||||||
likelyType: LikelyType.Other,
|
likelyType: LikelyType.Other,
|
||||||
url: 'https://example.com/javascript.js',
|
url: 'https://example.com/javascript.js',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
likelyType: LikelyType.AtpData,
|
||||||
|
url: '/',
|
||||||
|
title: 'Bluesky',
|
||||||
|
description: 'A new kind of social network',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
likelyType: LikelyType.AtpData,
|
likelyType: LikelyType.AtpData,
|
||||||
url: '/index.html',
|
url: '/index.html',
|
||||||
|
@ -127,7 +135,7 @@ describe('getLinkMeta', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const input = inputs[i]
|
const input = inputs[i]
|
||||||
const output = await getLinkMeta(input)
|
const output = await getLinkMeta(mockedRootStore, input)
|
||||||
expect(output).toEqual(outputs[i])
|
expect(output).toEqual(outputs[i])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -43,7 +43,7 @@ describe('LinkMetasViewModel', () => {
|
||||||
|
|
||||||
const result = await viewModel.getLinkMeta(mockedMeta.url)
|
const result = await viewModel.getLinkMeta(mockedMeta.url)
|
||||||
|
|
||||||
expect(getLinkMetaMockSpy).toHaveBeenCalledWith(mockedMeta.url)
|
expect(getLinkMetaMockSpy).toHaveBeenCalledWith(rootStore, mockedMeta.url)
|
||||||
expect(result).toEqual(mockedMeta)
|
expect(result).toEqual(mockedMeta)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import {LikelyType, LinkMeta} from './link-meta'
|
||||||
|
import {match as matchRoute} from '../view/routes'
|
||||||
|
import {convertBskyAppUrlIfNeeded, makeRecordUri} from './strings'
|
||||||
|
import {RootStoreModel} from '../state'
|
||||||
|
import {PostThreadViewModel} from '../state/models/post-thread-view'
|
||||||
|
|
||||||
|
import {Home} from '../view/screens/Home'
|
||||||
|
import {Search} from '../view/screens/Search'
|
||||||
|
import {Notifications} from '../view/screens/Notifications'
|
||||||
|
import {PostThread} from '../view/screens/PostThread'
|
||||||
|
import {PostUpvotedBy} from '../view/screens/PostUpvotedBy'
|
||||||
|
import {PostRepostedBy} from '../view/screens/PostRepostedBy'
|
||||||
|
import {Profile} from '../view/screens/Profile'
|
||||||
|
import {ProfileFollowers} from '../view/screens/ProfileFollowers'
|
||||||
|
import {ProfileFollows} from '../view/screens/ProfileFollows'
|
||||||
|
|
||||||
|
// NOTE
|
||||||
|
// this is a hack around the lack of hosted social metadata
|
||||||
|
// remove once that's implemented
|
||||||
|
// -prf
|
||||||
|
export async function extractBskyMeta(
|
||||||
|
store: RootStoreModel,
|
||||||
|
url: string,
|
||||||
|
): Promise<LinkMeta> {
|
||||||
|
url = convertBskyAppUrlIfNeeded(url)
|
||||||
|
const route = matchRoute(url)
|
||||||
|
let meta: LinkMeta = {
|
||||||
|
likelyType: LikelyType.AtpData,
|
||||||
|
url,
|
||||||
|
title: route.defaultTitle,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.Com === Home) {
|
||||||
|
meta = {
|
||||||
|
...meta,
|
||||||
|
title: 'Bluesky',
|
||||||
|
description: 'A new kind of social network',
|
||||||
|
}
|
||||||
|
} else if (route.Com === Search) {
|
||||||
|
meta = {
|
||||||
|
...meta,
|
||||||
|
title: 'Search - Bluesky',
|
||||||
|
description: 'A new kind of social network',
|
||||||
|
}
|
||||||
|
} else if (route.Com === Notifications) {
|
||||||
|
meta = {
|
||||||
|
...meta,
|
||||||
|
title: 'Notifications - Bluesky',
|
||||||
|
description: 'A new kind of social network',
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
route.Com === PostThread ||
|
||||||
|
route.Com === PostUpvotedBy ||
|
||||||
|
route.Com === PostRepostedBy
|
||||||
|
) {
|
||||||
|
// post and post-related screens
|
||||||
|
const threadUri = makeRecordUri(
|
||||||
|
route.params.name,
|
||||||
|
'app.bsky.feed.post',
|
||||||
|
route.params.rkey,
|
||||||
|
)
|
||||||
|
const threadView = new PostThreadViewModel(store, {
|
||||||
|
uri: threadUri,
|
||||||
|
depth: 0,
|
||||||
|
})
|
||||||
|
await threadView.setup().catch(_err => undefined)
|
||||||
|
const title = [
|
||||||
|
route.Com === PostUpvotedBy
|
||||||
|
? 'Likes on a post by'
|
||||||
|
: route.Com === PostRepostedBy
|
||||||
|
? 'Reposts of a post by'
|
||||||
|
: 'Post by',
|
||||||
|
threadView.thread?.post.author.displayName ||
|
||||||
|
threadView.thread?.post.author.handle ||
|
||||||
|
'a bluesky user',
|
||||||
|
].join(' ')
|
||||||
|
meta = {
|
||||||
|
...meta,
|
||||||
|
title,
|
||||||
|
description: threadView.thread?.postRecord?.text,
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
route.Com === Profile ||
|
||||||
|
route.Com === ProfileFollowers ||
|
||||||
|
route.Com === ProfileFollows
|
||||||
|
) {
|
||||||
|
// profile and profile-related screens
|
||||||
|
const profile = await store.profiles.getProfile(route.params.name)
|
||||||
|
if (profile?.data) {
|
||||||
|
meta = {
|
||||||
|
...meta,
|
||||||
|
title: profile.data.displayName || profile.data.handle,
|
||||||
|
description: profile.data.description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
|
@ -1,10 +1,7 @@
|
||||||
import he from 'he'
|
import he from 'he'
|
||||||
import {
|
import {extractHtmlMeta, isBskyAppUrl} from './strings'
|
||||||
extractHtmlMeta,
|
import {RootStoreModel} from '../state'
|
||||||
isBskyAppUrl,
|
import {extractBskyMeta} from './extractBskyMeta'
|
||||||
convertBskyAppUrlIfNeeded,
|
|
||||||
} from './strings'
|
|
||||||
import {match as matchRoute} from '../view/routes'
|
|
||||||
|
|
||||||
export enum LikelyType {
|
export enum LikelyType {
|
||||||
HTML,
|
HTML,
|
||||||
|
@ -26,19 +23,12 @@ export interface LinkMeta {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLinkMeta(
|
export async function getLinkMeta(
|
||||||
|
store: RootStoreModel,
|
||||||
url: string,
|
url: string,
|
||||||
timeout = 5e3,
|
timeout = 5e3,
|
||||||
): Promise<LinkMeta> {
|
): Promise<LinkMeta> {
|
||||||
if (isBskyAppUrl(url)) {
|
if (isBskyAppUrl(url)) {
|
||||||
// TODO this could be better
|
return extractBskyMeta(store, url)
|
||||||
url = convertBskyAppUrlIfNeeded(url)
|
|
||||||
const route = matchRoute(url)
|
|
||||||
return {
|
|
||||||
likelyType: LikelyType.AtpData,
|
|
||||||
url,
|
|
||||||
title: route.defaultTitle,
|
|
||||||
// description: ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let urlp
|
let urlp
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class LinkMetasViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const promise = getLinkMeta(url)
|
const promise = getLinkMeta(this.rootStore, url)
|
||||||
this.cache.set(url, promise)
|
this.cache.set(url, promise)
|
||||||
const res = await promise
|
const res = await promise
|
||||||
this.cache.set(url, res)
|
this.cache.set(url, res)
|
||||||
|
|
|
@ -94,7 +94,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
return cleanup
|
return cleanup
|
||||||
}
|
}
|
||||||
if (!extLink.meta) {
|
if (!extLink.meta) {
|
||||||
getLinkMeta(extLink.uri).then(meta => {
|
getLinkMeta(store, extLink.uri).then(meta => {
|
||||||
if (aborted) {
|
if (aborted) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {
|
import {
|
||||||
|
Share,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
|
@ -16,7 +17,7 @@ import {
|
||||||
ReportAccountModal,
|
ReportAccountModal,
|
||||||
ProfileImageLightbox,
|
ProfileImageLightbox,
|
||||||
} from '../../../state/models/shell-ui'
|
} from '../../../state/models/shell-ui'
|
||||||
import {pluralize} from '../../../lib/strings'
|
import {pluralize, toShareUrl} from '../../../lib/strings'
|
||||||
import {s, gradients} from '../../lib/styles'
|
import {s, gradients} from '../../lib/styles'
|
||||||
import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton'
|
import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
|
@ -66,6 +67,9 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
const onPressFollows = () => {
|
const onPressFollows = () => {
|
||||||
store.nav.navigate(`/profile/${view.handle}/follows`)
|
store.nav.navigate(`/profile/${view.handle}/follows`)
|
||||||
}
|
}
|
||||||
|
const onPressShare = () => {
|
||||||
|
Share.share({url: toShareUrl(`/profile/${view.handle}`)})
|
||||||
|
}
|
||||||
const onPressMuteAccount = async () => {
|
const onPressMuteAccount = async () => {
|
||||||
try {
|
try {
|
||||||
await view.muteAccount()
|
await view.muteAccount()
|
||||||
|
@ -133,9 +137,8 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const isMe = store.me.did === view.did
|
const isMe = store.me.did === view.did
|
||||||
let dropdownItems: DropdownItem[] | undefined
|
let dropdownItems: DropdownItem[] = [{label: 'Share', onPress: onPressShare}]
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
dropdownItems = dropdownItems || []
|
|
||||||
dropdownItems.push({
|
dropdownItems.push({
|
||||||
label: view.myState.muted ? 'Unmute Account' : 'Mute Account',
|
label: view.myState.muted ? 'Unmute Account' : 'Mute Account',
|
||||||
onPress: view.myState.muted ? onPressUnmuteAccount : onPressMuteAccount,
|
onPress: view.myState.muted ? onPressUnmuteAccount : onPressMuteAccount,
|
||||||
|
|
|
@ -92,7 +92,7 @@ export function PostEmbeds({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<View style={styles.extInner}>
|
<View style={styles.extInner}>
|
||||||
<Text type="sm-bold" numberOfLines={2} style={[pal.text]}>
|
<Text type="md-bold" numberOfLines={2} style={[pal.text]}>
|
||||||
{link.title || link.uri}
|
{link.title || link.uri}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
|
|
Loading…
Reference in New Issue