about summary refs log tree commit diff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/ThemeContext.tsx32
-rw-r--r--src/lib/api/feed-manip.ts19
-rw-r--r--src/lib/hooks/usePermissions.web.ts16
-rw-r--r--src/lib/moderatePost_wrapped.ts58
-rw-r--r--src/lib/moderation.ts7
-rw-r--r--src/lib/strings/embed-player.ts152
-rw-r--r--src/lib/strings/url-helpers.ts29
7 files changed, 245 insertions, 68 deletions
diff --git a/src/lib/ThemeContext.tsx b/src/lib/ThemeContext.tsx
index 483c50c42..38bd199cb 100644
--- a/src/lib/ThemeContext.tsx
+++ b/src/lib/ThemeContext.tsx
@@ -1,9 +1,7 @@
-import {isWeb} from 'platform/detection'
 import React, {ReactNode, createContext, useContext} from 'react'
 import {
-  AppState,
   TextStyle,
-  useColorScheme as useColorScheme_BUGGY,
+  useColorScheme,
   ViewStyle,
   ColorSchemeName,
 } from 'react-native'
@@ -97,37 +95,11 @@ function getTheme(theme: ColorSchemeName) {
   return theme === 'dark' ? darkTheme : defaultTheme
 }
 
-/**
- * With RN iOS, we can only "trust" the color scheme reported while the app is
- * active. This is a workaround until the bug is fixed upstream.
- *
- * @see https://github.com/bluesky-social/social-app/pull/1417#issuecomment-1719868504
- * @see https://github.com/facebook/react-native/pull/39439
- */
-function useColorScheme_FIXED() {
-  const colorScheme = useColorScheme_BUGGY()
-  const [currentColorScheme, setCurrentColorScheme] =
-    React.useState<ColorSchemeName>(colorScheme)
-
-  React.useEffect(() => {
-    // we don't need to be updating state on web
-    if (isWeb) return
-    const subscription = AppState.addEventListener('change', state => {
-      const isActive = state === 'active'
-      if (!isActive) return
-      setCurrentColorScheme(colorScheme)
-    })
-    return () => subscription.remove()
-  }, [colorScheme])
-
-  return isWeb ? colorScheme : currentColorScheme
-}
-
 export const ThemeProvider: React.FC<ThemeProviderProps> = ({
   theme,
   children,
 }) => {
-  const colorScheme = useColorScheme_FIXED()
+  const colorScheme = useColorScheme()
   const themeValue = getTheme(theme === 'system' ? colorScheme : theme)
 
   return (
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts
index 9a050fd3e..c964693c4 100644
--- a/src/lib/api/feed-manip.ts
+++ b/src/lib/api/feed-manip.ts
@@ -117,11 +117,7 @@ export class FeedViewPostsSlice {
 }
 
 export class NoopFeedTuner {
-  private keyCounter = 0
-
-  reset() {
-    this.keyCounter = 0
-  }
+  reset() {}
   tune(
     feed: FeedViewPost[],
     _opts?: {dryRun: boolean; maintainOrder: boolean},
@@ -131,13 +127,13 @@ export class NoopFeedTuner {
 }
 
 export class FeedTuner {
-  private keyCounter = 0
+  seenKeys: Set<string> = new Set()
   seenUris: Set<string> = new Set()
 
   constructor(public tunerFns: FeedTunerFn[]) {}
 
   reset() {
-    this.keyCounter = 0
+    this.seenKeys.clear()
     this.seenUris.clear()
   }
 
@@ -218,11 +214,16 @@ export class FeedTuner {
     }
 
     if (!dryRun) {
-      for (const slice of slices) {
+      slices = slices.filter(slice => {
+        if (this.seenKeys.has(slice._reactKey)) {
+          return false
+        }
         for (const item of slice.items) {
           this.seenUris.add(item.post.uri)
         }
-      }
+        this.seenKeys.add(slice._reactKey)
+        return true
+      })
     }
 
     return slices
diff --git a/src/lib/hooks/usePermissions.web.ts b/src/lib/hooks/usePermissions.web.ts
new file mode 100644
index 000000000..c550a7d6d
--- /dev/null
+++ b/src/lib/hooks/usePermissions.web.ts
@@ -0,0 +1,16 @@
+export function usePhotoLibraryPermission() {
+  const requestPhotoAccessIfNeeded = async () => {
+    // On the, we use <input type="file"> to produce a filepicker
+    // This does not need any permission granting.
+    return true
+  }
+  return {requestPhotoAccessIfNeeded}
+}
+
+export function useCameraPermission() {
+  const requestCameraAccessIfNeeded = async () => {
+    return false
+  }
+
+  return {requestCameraAccessIfNeeded}
+}
diff --git a/src/lib/moderatePost_wrapped.ts b/src/lib/moderatePost_wrapped.ts
new file mode 100644
index 000000000..2195b2304
--- /dev/null
+++ b/src/lib/moderatePost_wrapped.ts
@@ -0,0 +1,58 @@
+import {
+  AppBskyEmbedRecord,
+  AppBskyEmbedRecordWithMedia,
+  moderatePost,
+} from '@atproto/api'
+
+type ModeratePost = typeof moderatePost
+type Options = Parameters<ModeratePost>[1] & {
+  hiddenPosts?: string[]
+}
+
+export function moderatePost_wrapped(
+  subject: Parameters<ModeratePost>[0],
+  opts: Options,
+) {
+  const {hiddenPosts = [], ...options} = opts
+  const moderations = moderatePost(subject, options)
+
+  if (hiddenPosts.includes(subject.uri)) {
+    moderations.content.filter = true
+    moderations.content.blur = true
+    if (!moderations.content.cause) {
+      moderations.content.cause = {
+        // @ts-ignore Temporary extension to the moderation system -prf
+        type: 'post-hidden',
+        source: {type: 'user'},
+        priority: 1,
+      }
+    }
+  }
+
+  if (subject.embed) {
+    let embedHidden = false
+    if (AppBskyEmbedRecord.isViewRecord(subject.embed.record)) {
+      embedHidden = hiddenPosts.includes(subject.embed.record.uri)
+    }
+    if (
+      AppBskyEmbedRecordWithMedia.isView(subject.embed) &&
+      AppBskyEmbedRecord.isViewRecord(subject.embed.record.record)
+    ) {
+      embedHidden = hiddenPosts.includes(subject.embed.record.record.uri)
+    }
+    if (embedHidden) {
+      moderations.embed.filter = true
+      moderations.embed.blur = true
+      if (!moderations.embed.cause) {
+        moderations.embed.cause = {
+          // @ts-ignore Temporary extension to the moderation system -prf
+          type: 'post-hidden',
+          source: {type: 'user'},
+          priority: 1,
+        }
+      }
+    }
+  }
+
+  return moderations
+}
diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts
index 8ba99128b..bf19c208a 100644
--- a/src/lib/moderation.ts
+++ b/src/lib/moderation.ts
@@ -60,6 +60,13 @@ export function describeModerationCause(
       }
     }
   }
+  // @ts-ignore Temporary extension to the moderation system -prf
+  if (cause.type === 'post-hidden') {
+    return {
+      name: 'Post Hidden by You',
+      description: 'You have hidden this post',
+    }
+  }
   return cause.labelDef.strings[context].en
 }
 
diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts
new file mode 100644
index 000000000..ec996dfa5
--- /dev/null
+++ b/src/lib/strings/embed-player.ts
@@ -0,0 +1,152 @@
+import {Platform} from 'react-native'
+
+export type EmbedPlayerParams =
+  | {type: 'youtube_video'; videoId: string; playerUri: string}
+  | {type: 'twitch_live'; channelId: string; playerUri: string}
+  | {type: 'spotify_album'; albumId: string; playerUri: string}
+  | {
+      type: 'spotify_playlist'
+      playlistId: string
+      playerUri: string
+    }
+  | {type: 'spotify_song'; songId: string; playerUri: string}
+  | {type: 'soundcloud_track'; user: string; track: string; playerUri: string}
+  | {type: 'soundcloud_set'; user: string; set: string; playerUri: string}
+
+export function parseEmbedPlayerFromUrl(
+  url: string,
+): EmbedPlayerParams | undefined {
+  let urlp
+  try {
+    urlp = new URL(url)
+  } catch (e) {
+    return undefined
+  }
+
+  // youtube
+  if (urlp.hostname === 'youtu.be') {
+    const videoId = urlp.pathname.split('/')[1]
+    if (videoId) {
+      return {
+        type: 'youtube_video',
+        videoId,
+        playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1`,
+      }
+    }
+  }
+  if (urlp.hostname === 'www.youtube.com' || urlp.hostname === 'youtube.com') {
+    const [_, page, shortVideoId] = urlp.pathname.split('/')
+    const videoId =
+      page === 'shorts' ? shortVideoId : (urlp.searchParams.get('v') as string)
+
+    if (videoId) {
+      return {
+        type: 'youtube_video',
+        videoId,
+        playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1`,
+      }
+    }
+  }
+
+  // twitch
+  if (urlp.hostname === 'twitch.tv' || urlp.hostname === 'www.twitch.tv') {
+    const parent =
+      Platform.OS === 'web' ? window.location.hostname : 'localhost'
+
+    const parts = urlp.pathname.split('/')
+    if (parts.length === 2 && parts[1]) {
+      return {
+        type: 'twitch_live',
+        channelId: parts[1],
+        playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=${parts[1]}&parent=${parent}`,
+      }
+    }
+  }
+
+  // spotify
+  if (urlp.hostname === 'open.spotify.com') {
+    const [_, type, id] = urlp.pathname.split('/')
+    if (type && id) {
+      if (type === 'playlist') {
+        return {
+          type: 'spotify_playlist',
+          playlistId: id,
+          playerUri: `https://open.spotify.com/embed/playlist/${id}`,
+        }
+      }
+      if (type === 'album') {
+        return {
+          type: 'spotify_album',
+          albumId: id,
+          playerUri: `https://open.spotify.com/embed/album/${id}`,
+        }
+      }
+      if (type === 'track') {
+        return {
+          type: 'spotify_song',
+          songId: id,
+          playerUri: `https://open.spotify.com/embed/track/${id}`,
+        }
+      }
+    }
+  }
+
+  // soundcloud
+  if (
+    urlp.hostname === 'soundcloud.com' ||
+    urlp.hostname === 'www.soundcloud.com'
+  ) {
+    const [_, user, trackOrSets, set] = urlp.pathname.split('/')
+
+    if (user && trackOrSets) {
+      if (trackOrSets === 'sets' && set) {
+        return {
+          type: 'soundcloud_set',
+          user,
+          set: set,
+          playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
+        }
+      }
+
+      return {
+        type: 'soundcloud_track',
+        user,
+        track: trackOrSets,
+        playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
+      }
+    }
+  }
+}
+
+export function getPlayerHeight({
+  type,
+  width,
+  hasThumb,
+}: {
+  type: EmbedPlayerParams['type']
+  width: number
+  hasThumb: boolean
+}) {
+  if (!hasThumb) return (width / 16) * 9
+
+  switch (type) {
+    case 'youtube_video':
+    case 'twitch_live':
+      return (width / 16) * 9
+    case 'spotify_album':
+      return 380
+    case 'spotify_playlist':
+      return 360
+    case 'spotify_song':
+      if (width <= 300) {
+        return 180
+      }
+      return 232
+    case 'soundcloud_track':
+      return 165
+    case 'soundcloud_set':
+      return 360
+    default:
+      return width
+  }
+}
diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
index e9bf4111d..8a71718c8 100644
--- a/src/lib/strings/url-helpers.ts
+++ b/src/lib/strings/url-helpers.ts
@@ -139,35 +139,6 @@ export function feedUriToHref(url: string): string {
   }
 }
 
-export function getYoutubeVideoId(link: string): string | undefined {
-  let url
-  try {
-    url = new URL(link)
-  } catch (e) {
-    return undefined
-  }
-
-  if (
-    url.hostname !== 'www.youtube.com' &&
-    url.hostname !== 'youtube.com' &&
-    url.hostname !== 'youtu.be'
-  ) {
-    return undefined
-  }
-  if (url.hostname === 'youtu.be') {
-    const videoId = url.pathname.split('/')[1]
-    if (!videoId) {
-      return undefined
-    }
-    return videoId
-  }
-  const videoId = url.searchParams.get('v') as string
-  if (!videoId) {
-    return undefined
-  }
-  return videoId
-}
-
 /**
  * Checks if the label in the post text matches the host of the link facet.
  *