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 cards
zio/stable
Paul Frazee 2023-01-19 12:30:28 -06:00 committed by GitHub
parent 0536a6afcf
commit 9230d52ff5
8 changed files with 123 additions and 23 deletions

View File

@ -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])
} }
}) })

View File

@ -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)
}) })

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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
} }

View File

@ -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,

View File

@ -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