diff options
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/ThemeContext.tsx | 32 | ||||
-rw-r--r-- | src/lib/api/feed-manip.ts | 19 | ||||
-rw-r--r-- | src/lib/hooks/usePermissions.web.ts | 16 | ||||
-rw-r--r-- | src/lib/moderatePost_wrapped.ts | 58 | ||||
-rw-r--r-- | src/lib/moderation.ts | 7 | ||||
-rw-r--r-- | src/lib/strings/embed-player.ts | 152 | ||||
-rw-r--r-- | src/lib/strings/url-helpers.ts | 29 |
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. * |