about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/state/models/liked-by-view.ts141
-rw-r--r--src/state/models/reposted-by-view.ts141
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx139
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx141
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx22
-rw-r--r--src/view/lib/strings.ts14
-rw-r--r--src/view/routes/index.tsx16
-rw-r--r--src/view/routes/types.ts2
-rw-r--r--src/view/screens/content/PostLikedBy.tsx38
-rw-r--r--src/view/screens/content/PostRepostedBy.tsx41
-rw-r--r--src/view/screens/content/PostThread.tsx9
11 files changed, 695 insertions, 9 deletions
diff --git a/src/state/models/liked-by-view.ts b/src/state/models/liked-by-view.ts
new file mode 100644
index 000000000..e9548f27b
--- /dev/null
+++ b/src/state/models/liked-by-view.ts
@@ -0,0 +1,141 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {bsky, AdxUri} from '@adxp/mock-api'
+import {RootStoreModel} from './root-store'
+
+type LikedByItem = bsky.LikedByView.Response['likedBy'][number]
+
+export class LikedByViewItemModel implements LikedByItem {
+  // ui state
+  _reactKey: string = ''
+
+  // data
+  did: string = ''
+  name: string = ''
+  displayName: string = ''
+  createdAt?: string
+  indexedAt: string = ''
+
+  constructor(reactKey: string, v: LikedByItem) {
+    makeAutoObservable(this)
+    this._reactKey = reactKey
+    Object.assign(this, v)
+  }
+}
+
+export class LikedByViewModel implements bsky.LikedByView.Response {
+  // state
+  isLoading = false
+  isRefreshing = false
+  hasLoaded = false
+  error = ''
+  resolvedUri = ''
+  params: bsky.LikedByView.Params
+
+  // data
+  uri: string = ''
+  likedBy: LikedByViewItemModel[] = []
+
+  constructor(
+    public rootStore: RootStoreModel,
+    params: bsky.LikedByView.Params,
+  ) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+        params: false,
+      },
+      {autoBind: true},
+    )
+    this.params = params
+  }
+
+  get hasContent() {
+    return this.uri !== ''
+  }
+
+  get hasError() {
+    return this.error !== ''
+  }
+
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
+  }
+
+  // public api
+  // =
+
+  async setup() {
+    if (!this.resolvedUri) {
+      await this._resolveUri()
+    }
+    await this._fetch()
+  }
+
+  async refresh() {
+    await this._refresh()
+  }
+
+  // state transitions
+  // =
+
+  private _xLoading(isRefreshing = false) {
+    this.isLoading = true
+    this.isRefreshing = isRefreshing
+    this.error = ''
+  }
+
+  private _xIdle(err: string = '') {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = true
+    this.error = err
+  }
+
+  // loader functions
+  // =
+
+  private async _resolveUri() {
+    const urip = new AdxUri(this.params.uri)
+    if (!urip.host.startsWith('did:')) {
+      urip.host = await this.rootStore.resolveName(urip.host)
+    }
+    runInAction(() => {
+      this.resolvedUri = urip.toString()
+    })
+  }
+
+  private async _fetch() {
+    this._xLoading()
+    await new Promise(r => setTimeout(r, 250)) // DEBUG
+    try {
+      const res = (await this.rootStore.api.mainPds.view(
+        'blueskyweb.xyz:LikedByView',
+        Object.assign({}, this.params, {uri: this.resolvedUri}),
+      )) as bsky.LikedByView.Response
+      this._replaceAll(res)
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(`Failed to load feed: ${e.toString()}`)
+    }
+  }
+
+  private async _refresh() {
+    this._xLoading(true)
+    // TODO: refetch and update items
+    await new Promise(r => setTimeout(r, 250)) // DEBUG
+    this._xIdle()
+  }
+
+  private _replaceAll(res: bsky.LikedByView.Response) {
+    this.likedBy.length = 0
+    let counter = 0
+    for (const item of res.likedBy) {
+      this._append(counter++, item)
+    }
+  }
+
+  private _append(keyId: number, item: LikedByItem) {
+    this.likedBy.push(new LikedByViewItemModel(`item-${keyId}`, item))
+  }
+}
diff --git a/src/state/models/reposted-by-view.ts b/src/state/models/reposted-by-view.ts
new file mode 100644
index 000000000..a5f6cce25
--- /dev/null
+++ b/src/state/models/reposted-by-view.ts
@@ -0,0 +1,141 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {bsky, AdxUri} from '@adxp/mock-api'
+import {RootStoreModel} from './root-store'
+
+type RepostedByItem = bsky.RepostedByView.Response['repostedBy'][number]
+
+export class RepostedByViewItemModel implements RepostedByItem {
+  // ui state
+  _reactKey: string = ''
+
+  // data
+  did: string = ''
+  name: string = ''
+  displayName: string = ''
+  createdAt?: string
+  indexedAt: string = ''
+
+  constructor(reactKey: string, v: RepostedByItem) {
+    makeAutoObservable(this)
+    this._reactKey = reactKey
+    Object.assign(this, v)
+  }
+}
+
+export class RepostedByViewModel implements bsky.RepostedByView.Response {
+  // state
+  isLoading = false
+  isRefreshing = false
+  hasLoaded = false
+  error = ''
+  resolvedUri = ''
+  params: bsky.RepostedByView.Params
+
+  // data
+  uri: string = ''
+  repostedBy: RepostedByViewItemModel[] = []
+
+  constructor(
+    public rootStore: RootStoreModel,
+    params: bsky.RepostedByView.Params,
+  ) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+        params: false,
+      },
+      {autoBind: true},
+    )
+    this.params = params
+  }
+
+  get hasContent() {
+    return this.uri !== ''
+  }
+
+  get hasError() {
+    return this.error !== ''
+  }
+
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
+  }
+
+  // public api
+  // =
+
+  async setup() {
+    if (!this.resolvedUri) {
+      await this._resolveUri()
+    }
+    await this._fetch()
+  }
+
+  async refresh() {
+    await this._refresh()
+  }
+
+  // state transitions
+  // =
+
+  private _xLoading(isRefreshing = false) {
+    this.isLoading = true
+    this.isRefreshing = isRefreshing
+    this.error = ''
+  }
+
+  private _xIdle(err: string = '') {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = true
+    this.error = err
+  }
+
+  // loader functions
+  // =
+
+  private async _resolveUri() {
+    const urip = new AdxUri(this.params.uri)
+    if (!urip.host.startsWith('did:')) {
+      urip.host = await this.rootStore.resolveName(urip.host)
+    }
+    runInAction(() => {
+      this.resolvedUri = urip.toString()
+    })
+  }
+
+  private async _fetch() {
+    this._xLoading()
+    await new Promise(r => setTimeout(r, 250)) // DEBUG
+    try {
+      const res = (await this.rootStore.api.mainPds.view(
+        'blueskyweb.xyz:RepostedByView',
+        Object.assign({}, this.params, {uri: this.resolvedUri}),
+      )) as bsky.RepostedByView.Response
+      this._replaceAll(res)
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(`Failed to load feed: ${e.toString()}`)
+    }
+  }
+
+  private async _refresh() {
+    this._xLoading(true)
+    // TODO: refetch and update items
+    await new Promise(r => setTimeout(r, 250)) // DEBUG
+    this._xIdle()
+  }
+
+  private _replaceAll(res: bsky.RepostedByView.Response) {
+    this.repostedBy.length = 0
+    let counter = 0
+    for (const item of res.repostedBy) {
+      this._append(counter++, item)
+    }
+  }
+
+  private _append(keyId: number, item: RepostedByItem) {
+    this.repostedBy.push(new RepostedByViewItemModel(`item-${keyId}`, item))
+  }
+}
diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
new file mode 100644
index 000000000..678e069f6
--- /dev/null
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -0,0 +1,139 @@
+import React, {useState, useEffect} from 'react'
+import {observer} from 'mobx-react-lite'
+import {
+  ActivityIndicator,
+  FlatList,
+  Image,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {OnNavigateContent} from '../../routes/types'
+import {
+  LikedByViewModel,
+  LikedByViewItemModel,
+} from '../../../state/models/liked-by-view'
+import {useStores} from '../../../state'
+import {s} from '../../lib/styles'
+import {AVIS} from '../../lib/assets'
+
+export const PostLikedBy = observer(function PostLikedBy({
+  uri,
+  onNavigateContent,
+}: {
+  uri: string
+  onNavigateContent: OnNavigateContent
+}) {
+  const store = useStores()
+  const [view, setView] = useState<LikedByViewModel | undefined>()
+
+  useEffect(() => {
+    if (view?.params.uri === uri) {
+      console.log('Liked by doing nothing')
+      return // no change needed? or trigger refresh?
+    }
+    console.log('Fetching Liked by', uri)
+    const newView = new LikedByViewModel(store, {uri})
+    setView(newView)
+    newView.setup().catch(err => console.error('Failed to fetch liked by', err))
+  }, [uri, view?.params.uri, store])
+
+  // loading
+  // =
+  if (
+    !view ||
+    (view.isLoading && !view.isRefreshing) ||
+    view.params.uri !== uri
+  ) {
+    return (
+      <View>
+        <ActivityIndicator />
+      </View>
+    )
+  }
+
+  // error
+  // =
+  if (view.hasError) {
+    return (
+      <View>
+        <Text>{view.error}</Text>
+      </View>
+    )
+  }
+
+  // loaded
+  // =
+  const renderItem = ({item}: {item: LikedByViewItemModel}) => (
+    <LikedByItem item={item} onNavigateContent={onNavigateContent} />
+  )
+  return (
+    <View>
+      <FlatList
+        data={view.likedBy}
+        keyExtractor={item => item._reactKey}
+        renderItem={renderItem}
+      />
+    </View>
+  )
+})
+
+const LikedByItem = ({
+  item,
+  onNavigateContent,
+}: {
+  item: LikedByViewItemModel
+  onNavigateContent: OnNavigateContent
+}) => {
+  const onPressOuter = () => {
+    onNavigateContent('Profile', {
+      name: item.name,
+    })
+  }
+  return (
+    <TouchableOpacity style={styles.outer} onPress={onPressOuter}>
+      <View style={styles.layout}>
+        <View style={styles.layoutAvi}>
+          <Image
+            style={styles.avi}
+            source={AVIS[item.name] || AVIS['alice.com']}
+          />
+        </View>
+        <View style={styles.layoutContent}>
+          <Text style={[s.f15, s.bold]}>{item.displayName}</Text>
+          <Text style={[s.f14, s.gray]}>@{item.name}</Text>
+        </View>
+      </View>
+    </TouchableOpacity>
+  )
+}
+
+const styles = StyleSheet.create({
+  outer: {
+    borderTopWidth: 1,
+    borderTopColor: '#e8e8e8',
+    backgroundColor: '#fff',
+  },
+  layout: {
+    flexDirection: 'row',
+  },
+  layoutAvi: {
+    width: 60,
+    paddingLeft: 10,
+    paddingTop: 10,
+    paddingBottom: 10,
+  },
+  avi: {
+    width: 40,
+    height: 40,
+    borderRadius: 30,
+    resizeMode: 'cover',
+  },
+  layoutContent: {
+    flex: 1,
+    paddingRight: 10,
+    paddingTop: 10,
+    paddingBottom: 10,
+  },
+})
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
new file mode 100644
index 000000000..98c24ef86
--- /dev/null
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -0,0 +1,141 @@
+import React, {useState, useEffect} from 'react'
+import {observer} from 'mobx-react-lite'
+import {
+  ActivityIndicator,
+  FlatList,
+  Image,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
+} from 'react-native'
+import {OnNavigateContent} from '../../routes/types'
+import {
+  RepostedByViewModel,
+  RepostedByViewItemModel,
+} from '../../../state/models/reposted-by-view'
+import {useStores} from '../../../state'
+import {s} from '../../lib/styles'
+import {AVIS} from '../../lib/assets'
+
+export const PostRepostedBy = observer(function PostRepostedBy({
+  uri,
+  onNavigateContent,
+}: {
+  uri: string
+  onNavigateContent: OnNavigateContent
+}) {
+  const store = useStores()
+  const [view, setView] = useState<RepostedByViewModel | undefined>()
+
+  useEffect(() => {
+    if (view?.params.uri === uri) {
+      console.log('Reposted by doing nothing')
+      return // no change needed? or trigger refresh?
+    }
+    console.log('Fetching Reposted by', uri)
+    const newView = new RepostedByViewModel(store, {uri})
+    setView(newView)
+    newView
+      .setup()
+      .catch(err => console.error('Failed to fetch reposted by', err))
+  }, [uri, view?.params.uri, store])
+
+  // loading
+  // =
+  if (
+    !view ||
+    (view.isLoading && !view.isRefreshing) ||
+    view.params.uri !== uri
+  ) {
+    return (
+      <View>
+        <ActivityIndicator />
+      </View>
+    )
+  }
+
+  // error
+  // =
+  if (view.hasError) {
+    return (
+      <View>
+        <Text>{view.error}</Text>
+      </View>
+    )
+  }
+
+  // loaded
+  // =
+  const renderItem = ({item}: {item: RepostedByViewItemModel}) => (
+    <RepostedByItem item={item} onNavigateContent={onNavigateContent} />
+  )
+  return (
+    <View>
+      <FlatList
+        data={view.repostedBy}
+        keyExtractor={item => item._reactKey}
+        renderItem={renderItem}
+      />
+    </View>
+  )
+})
+
+const RepostedByItem = ({
+  item,
+  onNavigateContent,
+}: {
+  item: RepostedByViewItemModel
+  onNavigateContent: OnNavigateContent
+}) => {
+  const onPressOuter = () => {
+    onNavigateContent('Profile', {
+      name: item.name,
+    })
+  }
+  return (
+    <TouchableOpacity style={styles.outer} onPress={onPressOuter}>
+      <View style={styles.layout}>
+        <View style={styles.layoutAvi}>
+          <Image
+            style={styles.avi}
+            source={AVIS[item.name] || AVIS['alice.com']}
+          />
+        </View>
+        <View style={styles.layoutContent}>
+          <Text style={[s.f15, s.bold]}>{item.displayName}</Text>
+          <Text style={[s.f14, s.gray]}>@{item.name}</Text>
+        </View>
+      </View>
+    </TouchableOpacity>
+  )
+}
+
+const styles = StyleSheet.create({
+  outer: {
+    borderTopWidth: 1,
+    borderTopColor: '#e8e8e8',
+    backgroundColor: '#fff',
+  },
+  layout: {
+    flexDirection: 'row',
+  },
+  layoutAvi: {
+    width: 60,
+    paddingLeft: 10,
+    paddingTop: 10,
+    paddingBottom: 10,
+  },
+  avi: {
+    width: 40,
+    height: 40,
+    borderRadius: 30,
+    resizeMode: 'cover',
+  },
+  layoutContent: {
+    flex: 1,
+    paddingRight: 10,
+    paddingTop: 10,
+    paddingBottom: 10,
+  },
+})
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 896eab89f..7263c61b3 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -40,6 +40,20 @@ export const PostThreadItem = observer(function PostThreadItem({
       name: item.author.name,
     })
   }
+  const onPressLikes = () => {
+    const urip = new AdxUri(item.uri)
+    onNavigateContent('PostLikedBy', {
+      name: item.author.name,
+      recordKey: urip.recordKey,
+    })
+  }
+  const onPressReposts = () => {
+    const urip = new AdxUri(item.uri)
+    onNavigateContent('PostRepostedBy', {
+      name: item.author.name,
+      recordKey: urip.recordKey,
+    })
+  }
   const onPressToggleRepost = () => {
     item
       .toggleRepost()
@@ -91,7 +105,9 @@ export const PostThreadItem = observer(function PostThreadItem({
           {item._isHighlightedPost && hasEngagement ? (
             <View style={styles.expandedInfo}>
               {item.repostCount ? (
-                <Text style={[styles.expandedInfoItem, s.gray, s.semiBold]}>
+                <Text
+                  style={[styles.expandedInfoItem, s.gray, s.semiBold]}
+                  onPress={onPressReposts}>
                   <Text style={[s.bold, s.black]}>{item.repostCount}</Text>{' '}
                   {pluralize(item.repostCount, 'repost')}
                 </Text>
@@ -99,7 +115,9 @@ export const PostThreadItem = observer(function PostThreadItem({
                 <></>
               )}
               {item.likeCount ? (
-                <Text style={[styles.expandedInfoItem, s.gray, s.semiBold]}>
+                <Text
+                  style={[styles.expandedInfoItem, s.gray, s.semiBold]}
+                  onPress={onPressLikes}>
                   <Text style={[s.bold, s.black]}>{item.likeCount}</Text>{' '}
                   {pluralize(item.likeCount, 'like')}
                 </Text>
diff --git a/src/view/lib/strings.ts b/src/view/lib/strings.ts
index 1be1112b1..3ef707dd8 100644
--- a/src/view/lib/strings.ts
+++ b/src/view/lib/strings.ts
@@ -1,3 +1,5 @@
+import {AdxUri} from '@adxp/mock-api'
+
 export function pluralize(n: number, base: string, plural?: string): string {
   if (n === 1) {
     return base
@@ -7,3 +9,15 @@ export function pluralize(n: number, base: string, plural?: string): string {
   }
   return base + 's'
 }
+
+export function makeRecordUri(
+  didOrName: string,
+  collection: string,
+  recordKey: string,
+) {
+  const urip = new AdxUri(`adx://host/`)
+  urip.host = didOrName
+  urip.collection = collection
+  urip.recordKey = recordKey
+  return urip.toString()
+}
diff --git a/src/view/routes/index.tsx b/src/view/routes/index.tsx
index c44076a6e..989fda470 100644
--- a/src/view/routes/index.tsx
+++ b/src/view/routes/index.tsx
@@ -19,6 +19,8 @@ import {Notifications} from '../screens/Notifications'
 import {Menu} from '../screens/Menu'
 import {Profile} from '../screens/content/Profile'
 import {PostThread} from '../screens/content/PostThread'
+import {PostLikedBy} from '../screens/content/PostLikedBy'
+import {PostRepostedBy} from '../screens/content/PostRepostedBy'
 import {Login} from '../screens/Login'
 import {Signup} from '../screens/Signup'
 import {NotFound} from '../screens/NotFound'
@@ -38,6 +40,8 @@ const linking: LinkingOptions<RootTabsParamList> = {
       MenuTab: 'menu',
       Profile: 'profile/:name',
       PostThread: 'profile/:name/post/:recordKey',
+      PostLikedBy: 'profile/:name/post/:recordKey/liked-by',
+      PostRepostedBy: 'profile/:name/post/:recordKey/reposted-by',
       Login: 'login',
       Signup: 'signup',
       NotFound: '*',
@@ -87,6 +91,8 @@ function HomeStackCom() {
       <HomeTabStack.Screen name="Home" component={Home} options={HIDE_HEADER} />
       <HomeTabStack.Screen name="Profile" component={Profile} />
       <HomeTabStack.Screen name="PostThread" component={PostThread} />
+      <HomeTabStack.Screen name="PostLikedBy" component={PostLikedBy} />
+      <HomeTabStack.Screen name="PostRepostedBy" component={PostRepostedBy} />
     </HomeTabStack.Navigator>
   )
 }
@@ -101,6 +107,8 @@ function SearchStackCom() {
       />
       <SearchTabStack.Screen name="Profile" component={Profile} />
       <SearchTabStack.Screen name="PostThread" component={PostThread} />
+      <SearchTabStack.Screen name="PostLikedBy" component={PostLikedBy} />
+      <SearchTabStack.Screen name="PostRepostedBy" component={PostRepostedBy} />
     </SearchTabStack.Navigator>
   )
 }
@@ -114,6 +122,14 @@ function NotificationsStackCom() {
       />
       <NotificationsTabStack.Screen name="Profile" component={Profile} />
       <NotificationsTabStack.Screen name="PostThread" component={PostThread} />
+      <NotificationsTabStack.Screen
+        name="PostLikedBy"
+        component={PostLikedBy}
+      />
+      <NotificationsTabStack.Screen
+        name="PostRepostedBy"
+        component={PostRepostedBy}
+      />
     </NotificationsTabStack.Navigator>
   )
 }
diff --git a/src/view/routes/types.ts b/src/view/routes/types.ts
index 0b4bbc5d7..fd58a7666 100644
--- a/src/view/routes/types.ts
+++ b/src/view/routes/types.ts
@@ -7,6 +7,8 @@ export type RootTabsParamList = {
   MenuTab: undefined
   Profile: {name: string}
   PostThread: {name: string; recordKey: string}
+  PostLikedBy: {name: string; recordKey: string}
+  PostRepostedBy: {name: string; recordKey: string}
   Login: undefined
   Signup: undefined
   NotFound: undefined
diff --git a/src/view/screens/content/PostLikedBy.tsx b/src/view/screens/content/PostLikedBy.tsx
new file mode 100644
index 000000000..f12990141
--- /dev/null
+++ b/src/view/screens/content/PostLikedBy.tsx
@@ -0,0 +1,38 @@
+import React, {useLayoutEffect} from 'react'
+import {TouchableOpacity} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {makeRecordUri} from '../../lib/strings'
+import {Shell} from '../../shell'
+import type {RootTabsScreenProps} from '../../routes/types'
+import {PostLikedBy as PostLikedByComponent} from '../../com/post-thread/PostLikedBy'
+
+export const PostLikedBy = ({
+  navigation,
+  route,
+}: RootTabsScreenProps<'PostLikedBy'>) => {
+  const {name, recordKey} = route.params
+  const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
+
+  useLayoutEffect(() => {
+    navigation.setOptions({
+      headerShown: true,
+      headerTitle: 'Liked By',
+      headerLeft: () => (
+        <TouchableOpacity onPress={() => navigation.goBack()}>
+          <FontAwesomeIcon icon="arrow-left" />
+        </TouchableOpacity>
+      ),
+    })
+  }, [navigation])
+
+  const onNavigateContent = (screen: string, props: Record<string, string>) => {
+    // @ts-ignore it's up to the callers to supply correct params -prf
+    navigation.push(screen, props)
+  }
+
+  return (
+    <Shell>
+      <PostLikedByComponent uri={uri} onNavigateContent={onNavigateContent} />
+    </Shell>
+  )
+}
diff --git a/src/view/screens/content/PostRepostedBy.tsx b/src/view/screens/content/PostRepostedBy.tsx
new file mode 100644
index 000000000..000c1a7fc
--- /dev/null
+++ b/src/view/screens/content/PostRepostedBy.tsx
@@ -0,0 +1,41 @@
+import React, {useLayoutEffect} from 'react'
+import {TouchableOpacity} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {makeRecordUri} from '../../lib/strings'
+import {Shell} from '../../shell'
+import type {RootTabsScreenProps} from '../../routes/types'
+import {PostRepostedBy as PostRepostedByComponent} from '../../com/post-thread/PostRepostedBy'
+
+export const PostRepostedBy = ({
+  navigation,
+  route,
+}: RootTabsScreenProps<'PostRepostedBy'>) => {
+  const {name, recordKey} = route.params
+  const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
+
+  useLayoutEffect(() => {
+    navigation.setOptions({
+      headerShown: true,
+      headerTitle: 'Reposted By',
+      headerLeft: () => (
+        <TouchableOpacity onPress={() => navigation.goBack()}>
+          <FontAwesomeIcon icon="arrow-left" />
+        </TouchableOpacity>
+      ),
+    })
+  }, [navigation])
+
+  const onNavigateContent = (screen: string, props: Record<string, string>) => {
+    // @ts-ignore it's up to the callers to supply correct params -prf
+    navigation.push(screen, props)
+  }
+
+  return (
+    <Shell>
+      <PostRepostedByComponent
+        uri={uri}
+        onNavigateContent={onNavigateContent}
+      />
+    </Shell>
+  )
+}
diff --git a/src/view/screens/content/PostThread.tsx b/src/view/screens/content/PostThread.tsx
index fde74e778..485a2e49a 100644
--- a/src/view/screens/content/PostThread.tsx
+++ b/src/view/screens/content/PostThread.tsx
@@ -1,7 +1,7 @@
 import React, {useLayoutEffect} from 'react'
 import {TouchableOpacity} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {AdxUri} from '@adxp/mock-api'
+import {makeRecordUri} from '../../lib/strings'
 import {Shell} from '../../shell'
 import type {RootTabsScreenProps} from '../../routes/types'
 import {PostThread as PostThreadComponent} from '../../com/post-thread/PostThread'
@@ -11,12 +11,7 @@ export const PostThread = ({
   route,
 }: RootTabsScreenProps<'PostThread'>) => {
   const {name, recordKey} = route.params
-
-  const urip = new AdxUri(`adx://todo/`)
-  urip.host = name
-  urip.collection = 'blueskyweb.xyz:Posts'
-  urip.recordKey = recordKey
-  const uri = urip.toString()
+  const uri = makeRecordUri(name, 'blueskyweb.xyz:Posts', recordKey)
 
   useLayoutEffect(() => {
     navigation.setOptions({