Merge branch 'main' into localization-german

This commit is contained in:
Ansh
2023-12-22 23:12:14 +05:30
committed by GitHub
30 changed files with 760 additions and 331 deletions
+140 -27
View File
@@ -1,6 +1,5 @@
import {RichText} from '@atproto/api' import {RichText} from '@atproto/api'
import { import {
getYoutubeVideoId,
makeRecordUri, makeRecordUri,
toNiceDomain, toNiceDomain,
toShortUrl, toShortUrl,
@@ -12,6 +11,7 @@ import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
import {shortenLinks} from '../../src/lib/strings/rich-text-manip' import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles' import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
import {cleanError} from '../../src/lib/strings/errors' import {cleanError} from '../../src/lib/strings/errors'
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
describe('detectLinkables', () => { describe('detectLinkables', () => {
const inputs = [ const inputs = [
@@ -335,32 +335,6 @@ describe('toShareUrl', () => {
}) })
}) })
describe('getYoutubeVideoId', () => {
it(' should return undefined for invalid youtube links', () => {
expect(getYoutubeVideoId('')).toBeUndefined()
expect(getYoutubeVideoId('https://www.google.com')).toBeUndefined()
expect(getYoutubeVideoId('https://www.youtube.com')).toBeUndefined()
expect(
getYoutubeVideoId('https://www.youtube.com/channelName'),
).toBeUndefined()
expect(
getYoutubeVideoId('https://www.youtube.com/channel/channelName'),
).toBeUndefined()
})
it('getYoutubeVideoId should return video id for valid youtube links', () => {
expect(getYoutubeVideoId('https://www.youtube.com/watch?v=videoId')).toBe(
'videoId',
)
expect(
getYoutubeVideoId(
'https://www.youtube.com/watch?v=videoId&feature=share',
),
).toBe('videoId')
expect(getYoutubeVideoId('https://youtu.be/videoId')).toBe('videoId')
})
})
describe('shortenLinks', () => { describe('shortenLinks', () => {
const inputs = [ const inputs = [
'start https://middle.com/foo/bar?baz=bux#hash end', 'start https://middle.com/foo/bar?baz=bux#hash end',
@@ -396,6 +370,7 @@ describe('shortenLinks', () => {
], ],
], ],
] ]
it('correctly shortens rich text while preserving facet URIs', () => { it('correctly shortens rich text while preserving facet URIs', () => {
for (let i = 0; i < inputs.length; i++) { for (let i = 0; i < inputs.length; i++) {
const input = inputs[i] const input = inputs[i]
@@ -410,3 +385,141 @@ describe('shortenLinks', () => {
} }
}) })
}) })
describe('parseEmbedPlayerFromUrl', () => {
const inputs = [
'https://youtu.be/videoId',
'https://www.youtube.com/watch?v=videoId',
'https://www.youtube.com/watch?v=videoId&feature=share',
'https://youtube.com/watch?v=videoId',
'https://youtube.com/watch?v=videoId&feature=share',
'https://youtube.com/shorts/videoId',
'https://youtube.com/shorts/',
'https://youtube.com/',
'https://youtube.com/random',
'https://twitch.tv/channelName',
'https://www.twitch.tv/channelName',
'https://open.spotify.com/playlist/playlistId',
'https://open.spotify.com/playlist/playlistId?param=value',
'https://open.spotify.com/track/songId',
'https://open.spotify.com/track/songId?param=value',
'https://open.spotify.com/album/albumId',
'https://open.spotify.com/album/albumId?param=value',
'https://soundcloud.com/user/track',
'https://soundcloud.com/user/sets/set',
'https://soundcloud.com/user/',
]
const outputs = [
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
undefined,
undefined,
undefined,
{
type: 'twitch_live',
channelId: 'channelName',
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
},
{
type: 'twitch_live',
channelId: 'channelName',
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
},
{
type: 'spotify_playlist',
playlistId: 'playlistId',
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
},
{
type: 'spotify_playlist',
playlistId: 'playlistId',
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
},
{
type: 'spotify_song',
songId: 'songId',
playerUri: `https://open.spotify.com/embed/track/songId`,
},
{
type: 'spotify_song',
songId: 'songId',
playerUri: `https://open.spotify.com/embed/track/songId`,
},
{
type: 'spotify_album',
albumId: 'albumId',
playerUri: `https://open.spotify.com/embed/album/albumId`,
},
{
type: 'spotify_album',
albumId: 'albumId',
playerUri: `https://open.spotify.com/embed/album/albumId`,
},
{
type: 'soundcloud_track',
user: 'user',
track: 'track',
playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/track&auto_play=true&visual=false&hide_related=true`,
},
{
type: 'soundcloud_set',
user: 'user',
set: 'set',
playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/sets/set&auto_play=true&visual=false&hide_related=true`,
},
undefined,
]
it('correctly grabs the correct id from uri', () => {
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i]
const output = outputs[i]
const res = parseEmbedPlayerFromUrl(input)
console.log(input)
expect(res).toEqual(output)
}
})
})
+19 -5
View File
@@ -1,8 +1,10 @@
package main package main
import ( import (
"encoding/xml"
"fmt" "fmt"
"net/http" "net/http"
"time"
appbsky "github.com/bluesky-social/indigo/api/bsky" appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/atproto/syntax"
@@ -10,6 +12,12 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ItemGUID struct {
XMLName xml.Name `xml:"guid"`
Value string `xml:",chardata"`
IsPerma bool `xml:"isPermalink,attr"`
}
// We don't actually populate the title for "posts". // We don't actually populate the title for "posts".
// Some background: https://book.micro.blog/rss-for-microblogs/ // Some background: https://book.micro.blog/rss-for-microblogs/
type Item struct { type Item struct {
@@ -17,8 +25,7 @@ type Item struct {
Link string `xml:"link,omitempty"` Link string `xml:"link,omitempty"`
Description string `xml:"description,omitempty"` Description string `xml:"description,omitempty"`
PubDate string `xml:"pubDate,omitempty"` PubDate string `xml:"pubDate,omitempty"`
Author string `xml:"author,omitempty"` GUID ItemGUID
GUID string `xml:"guid,omitempty"`
} }
type rss struct { type rss struct {
@@ -71,12 +78,19 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
if rec.Reply != nil { if rec.Reply != nil {
continue continue
} }
pubDate := ""
createdAt, err := syntax.ParseDatetimeLenient(rec.CreatedAt)
if nil == err {
pubDate = createdAt.Time().Format(time.RFC822Z)
}
posts = append(posts, Item{ posts = append(posts, Item{
Link: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", pv.Handle, aturi.RecordKey().String()), Link: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", pv.Handle, aturi.RecordKey().String()),
Description: rec.Text, Description: rec.Text,
PubDate: rec.CreatedAt, PubDate: pubDate,
Author: "@" + pv.Handle, GUID: ItemGUID{
GUID: aturi.String(), Value: aturi.String(),
IsPerma: false,
},
}) })
} }
+1 -1
View File
@@ -32,7 +32,7 @@ import { Text } from "react-native";
const text = "Hello World"; const text = "Hello World";
<Text accessibilityLabel="Label is here">{text}</Text> <Text accessibilityLabel="Label is here">{text}</Text>
``` ```
In this case, you cannot use the `useLingui()` hook: In this case, you can use the `useLingui()` hook:
```jsx ```jsx
import { msg } from "@lingui/macro"; import { msg } from "@lingui/macro";
import { useLingui } from "@lingui/react"; import { useLingui } from "@lingui/react";
+4 -1
View File
@@ -137,7 +137,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-native": "0.72.5", "react-native": "0.72.5",
"react-native-appstate-hook": "^1.0.6", "react-native-appstate-hook": "^1.0.6",
"react-native-drawer-layout": "^3.2.0", "react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-fs": "^2.20.0", "react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.12.1", "react-native-gesture-handler": "^2.12.1",
"react-native-get-random-values": "^1.8.0", "react-native-get-random-values": "^1.8.0",
@@ -160,6 +160,9 @@
"react-native-version-number": "^0.3.6", "react-native-version-number": "^0.3.6",
"react-native-web": "~0.19.6", "react-native-web": "~0.19.6",
"react-native-web-linear-gradient": "^1.1.2", "react-native-web-linear-gradient": "^1.1.2",
"react-native-web-webview": "^1.0.2",
"react-native-webview": "^13.6.2",
"react-native-youtube-iframe": "^2.3.0",
"react-responsive": "^9.0.2", "react-responsive": "^9.0.2",
"rn-fetch-blob": "^0.12.0", "rn-fetch-blob": "^0.12.0",
"sentry-expo": "~7.0.1", "sentry-expo": "~7.0.1",
+147
View File
@@ -0,0 +1,147 @@
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 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=localhost`,
}
}
}
// 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
}
}
-29
View File
@@ -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. * Checks if the label in the post text matches the host of the link facet.
* *
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7 -5
View File
@@ -287,15 +287,11 @@ export class Logger {
metadata: Metadata = {}, metadata: Metadata = {},
) { ) {
if (!this.enabled) return if (!this.enabled) return
if (!enabledLogLevels[this.level].includes(level)) return
const timestamp = Date.now() const timestamp = Date.now()
const meta = metadata || {} const meta = metadata || {}
for (const transport of this.transports) { // send every log to syslog
transport(level, message, meta, timestamp)
}
add({ add({
id: nanoid(), id: nanoid(),
timestamp, timestamp,
@@ -303,6 +299,12 @@ export class Logger {
message, message,
metadata: meta, metadata: meta,
}) })
if (!enabledLogLevels[this.level].includes(level)) return
for (const transport of this.transports) {
transport(level, message, meta, timestamp)
}
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ let entries: ConsoleTransportEntry[] = []
export function add(entry: ConsoleTransportEntry) { export function add(entry: ConsoleTransportEntry) {
entries.unshift(entry) entries.unshift(entry)
entries = entries.slice(0, 50) entries = entries.slice(0, 500)
} }
export function getEntries() { export function getEntries() {
+4 -19
View File
@@ -1,12 +1,5 @@
import React from 'react' import React from 'react'
import { import {Dimensions, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
Dimensions,
RefreshControl,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {List, ListRef} from '../util/List' import {List, ListRef} from '../util/List'
import {FeedSourceCardLoaded} from './FeedSourceCard' import {FeedSourceCardLoaded} from './FeedSourceCard'
@@ -180,22 +173,14 @@ export const ProfileFeedgens = React.forwardRef<
data={items} data={items}
keyExtractor={(item: any) => item._reactKey || item.uri} keyExtractor={(item: any) => item._reactKey || item.uri}
renderItem={renderItemInner} renderItem={renderItemInner}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onRefresh}
refreshing={isPTRing} headerOffset={headerOffset}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={{ contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
onEndReached={onEndReached} onEndReached={onEndReached}
+3 -14
View File
@@ -2,7 +2,6 @@ import React from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
Dimensions, Dimensions,
RefreshControl,
StyleProp, StyleProp,
View, View,
ViewStyle, ViewStyle,
@@ -15,7 +14,6 @@ import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {ProfileCard} from '../profile/ProfileCard' import {ProfileCard} from '../profile/ProfileCard'
import {Button} from '../util/forms/Button' import {Button} from '../util/forms/Button'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useListMembersQuery} from '#/state/queries/list-members' import {useListMembersQuery} from '#/state/queries/list-members'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -51,7 +49,6 @@ export function ListMembers({
headerOffset?: number headerOffset?: number
desktopFixedHeightOffset?: number desktopFixedHeightOffset?: number
}) { }) {
const pal = usePalette('default')
const {track} = useAnalytics() const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false) const [isRefreshing, setIsRefreshing] = React.useState(false)
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
@@ -215,24 +212,16 @@ export function ListMembers({
renderItem={renderItem} renderItem={renderItem}
ListHeaderComponent={renderHeader} ListHeaderComponent={renderHeader}
ListFooterComponent={Footer} ListFooterComponent={Footer}
refreshControl={ refreshing={isRefreshing}
<RefreshControl onRefresh={onRefresh}
refreshing={isRefreshing} headerOffset={headerOffset}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={{ contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}}
onScrolledDownChange={onScrolledDownChange} onScrolledDownChange={onScrolledDownChange}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={0.6} onEndReachedThreshold={0.6}
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight={desktopFixedHeightOffset || true} desktopFixedHeight={desktopFixedHeightOffset || true}
/> />
+45 -25
View File
@@ -119,31 +119,51 @@ export function MyLists({
[error, onRefresh, renderItem, pal], [error, onRefresh, renderItem, pal],
) )
const FlatListCom = inline ? RNFlatList : List if (inline) {
return ( return (
<View testID={testID} style={style}> <View testID={testID} style={style}>
{items.length > 0 && ( {items.length > 0 && (
<FlatListCom <RNFlatList
testID={testID ? `${testID}-flatlist` : undefined} testID={testID ? `${testID}-flatlist` : undefined}
data={items} data={items}
keyExtractor={item => (item.uri ? item.uri : item._reactKey)} keyExtractor={item => (item.uri ? item.uri : item._reactKey)}
renderItem={renderItemInner} renderItem={renderItemInner}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text} tintColor={pal.colors.text}
titleColor={pal.colors.text} titleColor={pal.colors.text}
/> />
} }
contentContainerStyle={[s.contentContainer]} contentContainerStyle={[s.contentContainer]}
removeClippedSubviews={true} removeClippedSubviews={true}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
/> />
)} )}
</View> </View>
) )
} else {
return (
<View testID={testID} style={style}>
{items.length > 0 && (
<List
testID={testID ? `${testID}-flatlist` : undefined}
data={items}
keyExtractor={item => (item.uri ? item.uri : item._reactKey)}
renderItem={renderItemInner}
refreshing={isPTRing}
onRefresh={onRefresh}
contentContainerStyle={[s.contentContainer]}
removeClippedSubviews={true}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
)}
</View>
)
}
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
+4 -19
View File
@@ -1,12 +1,5 @@
import React from 'react' import React from 'react'
import { import {Dimensions, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
Dimensions,
RefreshControl,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {List, ListRef} from '../util/List' import {List, ListRef} from '../util/List'
import {ListCard} from './ListCard' import {ListCard} from './ListCard'
@@ -182,22 +175,14 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
data={items} data={items}
keyExtractor={(item: any) => item._reactKey} keyExtractor={(item: any) => item._reactKey}
renderItem={renderItemInner} renderItem={renderItemInner}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onRefresh}
refreshing={isPTRing} headerOffset={headerOffset}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={{ contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
onEndReached={onEndReached} onEndReached={onEndReached}
+3 -11
View File
@@ -1,13 +1,12 @@
import React from 'react' import React from 'react'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {EmptyState} from '../util/EmptyState' import {EmptyState} from '../util/EmptyState'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread' import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -30,7 +29,6 @@ export function Feed({
onScrolledDownChange: (isScrolledDown: boolean) => void onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element ListHeaderComponent?: () => JSX.Element
}) { }) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
@@ -152,14 +150,8 @@ export function Feed({
renderItem={renderItem} renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent} ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={FeedFooter} ListFooterComponent={FeedFooter}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onRefresh}
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={0.6} onEndReachedThreshold={0.6}
onScrolledDownChange={onScrolledDownChange} onScrolledDownChange={onScrolledDownChange}
+3 -11
View File
@@ -1,18 +1,16 @@
import React, {useCallback, useMemo, useState} from 'react' import React, {useCallback, useMemo, useState} from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {List} from '../util/List' import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {usePostLikedByQuery} from '#/state/queries/post-liked-by' import {usePostLikedByQuery} from '#/state/queries/post-liked-by'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
export function PostLikedBy({uri}: {uri: string}) { export function PostLikedBy({uri}: {uri: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = useState(false) const [isPTRing, setIsPTRing] = useState(false)
const { const {
data: resolvedUri, data: resolvedUri,
@@ -88,14 +86,8 @@ export function PostLikedBy({uri}: {uri: string}) {
<List <List
data={likes} data={likes}
keyExtractor={item => item.actor.did} keyExtractor={item => item.actor.did}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onRefresh}
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
+3 -11
View File
@@ -1,18 +1,16 @@
import React, {useMemo, useCallback, useState} from 'react' import React, {useMemo, useCallback, useState} from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {List} from '../util/List' import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {usePostRepostedByQuery} from '#/state/queries/post-reposted-by' import {usePostRepostedByQuery} from '#/state/queries/post-reposted-by'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
export function PostRepostedBy({uri}: {uri: string}) { export function PostRepostedBy({uri}: {uri: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = useState(false) const [isPTRing, setIsPTRing] = useState(false)
const { const {
data: resolvedUri, data: resolvedUri,
@@ -89,14 +87,8 @@ export function PostRepostedBy({uri}: {uri: string}) {
<List <List
data={repostedBy} data={repostedBy}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onRefresh}
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
+2 -9
View File
@@ -2,7 +2,6 @@ import React, {useEffect, useRef} from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
Pressable, Pressable,
RefreshControl,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
@@ -349,14 +348,8 @@ function PostThreadLoaded({
} }
keyExtractor={item => item._reactKey} keyExtractor={item => item._reactKey}
renderItem={renderItem} renderItem={renderItem}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onPTR}
refreshing={isPTRing}
onRefresh={onPTR}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onContentSizeChange={onContentSizeChange} onContentSizeChange={onContentSizeChange}
style={s.hContentRegion} style={s.hContentRegion}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
+3 -14
View File
@@ -3,7 +3,6 @@ import {
ActivityIndicator, ActivityIndicator,
AppState, AppState,
Dimensions, Dimensions,
RefreshControl,
StyleProp, StyleProp,
StyleSheet, StyleSheet,
View, View,
@@ -16,7 +15,6 @@ import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedSlice} from './FeedSlice' import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {logger} from '#/logger' import {logger} from '#/logger'
import { import {
@@ -74,7 +72,6 @@ let Feed = ({
ListHeaderComponent?: () => JSX.Element ListHeaderComponent?: () => JSX.Element
extraData?: any extraData?: any
}): React.ReactNode => { }): React.ReactNode => {
const pal = usePalette('default')
const theme = useTheme() const theme = useTheme()
const {track} = useAnalytics() const {track} = useAnalytics()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -294,25 +291,17 @@ let Feed = ({
renderItem={renderItem} renderItem={renderItem}
ListFooterComponent={FeedFooter} ListFooterComponent={FeedFooter}
ListHeaderComponent={ListHeaderComponent} ListHeaderComponent={ListHeaderComponent}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onRefresh}
refreshing={isPTRing} headerOffset={headerOffset}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={{ contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}}
onScrolledDownChange={onScrolledDownChange} onScrolledDownChange={onScrolledDownChange}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={2} // number of posts left to trigger load more onEndReachedThreshold={2} // number of posts left to trigger load more
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
extraData={extraData} extraData={extraData}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight={ desktopFixedHeight={
+3 -11
View File
@@ -1,18 +1,16 @@
import React from 'react' import React from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {List} from '../util/List' import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard' import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFollowersQuery} from '#/state/queries/profile-followers' import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger' import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
export function ProfileFollowers({name}: {name: string}) { export function ProfileFollowers({name}: {name: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const { const {
data: resolvedDid, data: resolvedDid,
@@ -90,14 +88,8 @@ export function ProfileFollowers({name}: {name: string}) {
<List <List
data={followers} data={followers}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onRefresh}
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
+3 -11
View File
@@ -1,18 +1,16 @@
import React from 'react' import React from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {List} from '../util/List' import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard' import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger' import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
export function ProfileFollows({name}: {name: string}) { export function ProfileFollows({name}: {name: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const { const {
data: resolvedDid, data: resolvedDid,
@@ -90,14 +88,8 @@ export function ProfileFollows({name}: {name: string}) {
<List <List
data={follows} data={follows}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onRefresh}
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
+42 -3
View File
@@ -1,27 +1,42 @@
import React, {memo, startTransition} from 'react' import React, {memo, startTransition} from 'react'
import {FlatListProps} from 'react-native' import {FlatListProps, RefreshControl} from 'react-native'
import {FlatList_INTERNAL} from './Views' import {FlatList_INTERNAL} from './Views'
import {addStyle} from 'lib/styles'
import {useScrollHandlers} from '#/lib/ScrollContext' import {useScrollHandlers} from '#/lib/ScrollContext'
import {runOnJS, useSharedValue} from 'react-native-reanimated' import {runOnJS, useSharedValue} from 'react-native-reanimated'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {usePalette} from '#/lib/hooks/usePalette'
export type ListMethods = FlatList_INTERNAL export type ListMethods = FlatList_INTERNAL
export type ListProps<ItemT> = Omit< export type ListProps<ItemT> = Omit<
FlatListProps<ItemT>, FlatListProps<ItemT>,
'onScroll' // Use ScrollContext instead. | 'onScroll' // Use ScrollContext instead.
| 'refreshControl' // Pass refreshing and/or onRefresh instead.
| 'contentOffset' // Pass headerOffset instead.
> & { > & {
onScrolledDownChange?: (isScrolledDown: boolean) => void onScrolledDownChange?: (isScrolledDown: boolean) => void
headerOffset?: number
refreshing?: boolean
onRefresh?: () => void
} }
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null> export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
const SCROLLED_DOWN_LIMIT = 200 const SCROLLED_DOWN_LIMIT = 200
function ListImpl<ItemT>( function ListImpl<ItemT>(
{onScrolledDownChange, ...props}: ListProps<ItemT>, {
onScrolledDownChange,
refreshing,
onRefresh,
headerOffset,
style,
...props
}: ListProps<ItemT>,
ref: React.Ref<ListMethods>, ref: React.Ref<ListMethods>,
) { ) {
const isScrolledDown = useSharedValue(false) const isScrolledDown = useSharedValue(false)
const contextScrollHandlers = useScrollHandlers() const contextScrollHandlers = useScrollHandlers()
const pal = usePalette('default')
function handleScrolledDownChange(didScrollDown: boolean) { function handleScrolledDownChange(didScrollDown: boolean) {
startTransition(() => { startTransition(() => {
@@ -49,12 +64,36 @@ function ListImpl<ItemT>(
}, },
}) })
let refreshControl
if (refreshing !== undefined || onRefresh !== undefined) {
refreshControl = (
<RefreshControl
refreshing={refreshing ?? false}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
)
}
let contentOffset
if (headerOffset != null) {
style = addStyle(style, {
paddingTop: headerOffset,
})
contentOffset = {x: 0, y: headerOffset * -1}
}
return ( return (
<FlatList_INTERNAL <FlatList_INTERNAL
{...props} {...props}
scrollIndicatorInsets={{right: 1}} scrollIndicatorInsets={{right: 1}}
contentOffset={contentOffset}
refreshControl={refreshControl}
onScroll={scrollHandler} onScroll={scrollHandler}
scrollEventThrottle={1} scrollEventThrottle={1}
style={style}
ref={ref} ref={ref}
/> />
) )
@@ -6,22 +6,28 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {AppBskyEmbedExternal} from '@atproto/api' import {AppBskyEmbedExternal} from '@atproto/api'
import {toNiceDomain} from 'lib/strings/url-helpers' import {toNiceDomain} from 'lib/strings/url-helpers'
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed'
export const ExternalLinkEmbed = ({ export const ExternalLinkEmbed = ({
link, link,
imageChild,
}: { }: {
link: AppBskyEmbedExternal.ViewExternal link: AppBskyEmbedExternal.ViewExternal
imageChild?: React.ReactNode
}) => { }) => {
const pal = usePalette('default') const pal = usePalette('default')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const embedPlayerParams = React.useMemo(
() => parseEmbedPlayerFromUrl(link.uri),
[link.uri],
)
return ( return (
<View <View
style={{ style={{
flexDirection: isMobile ? 'column' : 'row', flexDirection: !isMobile && !embedPlayerParams ? 'row' : 'column',
}}> }}>
{link.thumb ? ( {link.thumb && !embedPlayerParams ? (
<View <View
style={ style={
!isMobile !isMobile
@@ -45,9 +51,11 @@ export const ExternalLinkEmbed = ({
source={{uri: link.thumb}} source={{uri: link.thumb}}
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
{imageChild}
</View> </View>
) : undefined} ) : undefined}
{embedPlayerParams && (
<ExternalPlayer link={link} params={embedPlayerParams} />
)}
<View <View
style={{ style={{
paddingHorizontal: isMobile ? 10 : 14, paddingHorizontal: isMobile ? 10 : 14,
@@ -0,0 +1,251 @@
import React from 'react'
import {
ActivityIndicator,
Dimensions,
GestureResponderEvent,
Pressable,
StyleSheet,
View,
} from 'react-native'
import {Image} from 'expo-image'
import {WebView} from 'react-native-webview'
import YoutubePlayer from 'react-native-youtube-iframe'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {EmbedPlayerParams, getPlayerHeight} from 'lib/strings/embed-player'
import {EventStopper} from '../EventStopper'
import {AppBskyEmbedExternal} from '@atproto/api'
import {isNative} from 'platform/detection'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
interface ShouldStartLoadRequest {
url: string
}
// This renders the overlay when the player is either inactive or loading as a separate layer
function PlaceholderOverlay({
isLoading,
isPlayerActive,
onPress,
}: {
isLoading: boolean
isPlayerActive: boolean
onPress: (event: GestureResponderEvent) => void
}) {
// If the player is active and not loading, we don't want to show the overlay.
if (isPlayerActive && !isLoading) return null
return (
<View style={[styles.layer, styles.overlayLayer]}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Play Video"
accessibilityHint=""
onPress={onPress}
style={[styles.overlayContainer, styles.topRadius]}>
{!isPlayerActive ? (
<FontAwesomeIcon icon="play" size={42} color="white" />
) : (
<ActivityIndicator size="large" color="white" />
)}
</Pressable>
</View>
)
}
// This renders the webview/youtube player as a separate layer
function Player({
height,
params,
onLoad,
isPlayerActive,
}: {
isPlayerActive: boolean
params: EmbedPlayerParams
height: number
onLoad: () => void
}) {
// ensures we only load what's requested
const onShouldStartLoadWithRequest = React.useCallback(
(event: ShouldStartLoadRequest) => event.url === params.playerUri,
[params.playerUri],
)
// Don't show the player until it is active
if (!isPlayerActive) return null
return (
<View style={[styles.layer, styles.playerLayer]}>
<EventStopper>
{isNative && params.type === 'youtube_video' ? (
<YoutubePlayer
videoId={params.videoId}
play
height={height}
onReady={onLoad}
webViewStyle={[styles.webview, styles.topRadius]}
/>
) : (
<View style={{height, width: '100%'}}>
<WebView
javaScriptEnabled={true}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
mediaPlaybackRequiresUserAction={false}
allowsInlineMediaPlayback
bounces={false}
allowsFullscreenVideo
nestedScrollEnabled
source={{uri: params.playerUri}}
onLoad={onLoad}
setSupportMultipleWindows={false} // Prevent any redirects from opening a new window (ads)
style={[styles.webview, styles.topRadius]}
/>
</View>
)}
</EventStopper>
</View>
)
}
// This renders the player area and handles the logic for when to show the player and when to show the overlay
export function ExternalPlayer({
link,
params,
}: {
link: AppBskyEmbedExternal.ViewExternal
params: EmbedPlayerParams
}) {
const navigation = useNavigation<NavigationProp>()
const [isPlayerActive, setPlayerActive] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(true)
const [dim, setDim] = React.useState({
width: 0,
height: 0,
})
const viewRef = React.useRef<View>(null)
// watch for leaving the viewport due to scrolling
React.useEffect(() => {
// Interval for scrolling works in most cases, However, for twitch embeds, if we navigate away from the screen the webview will
// continue playing. We need to watch for the blur event
const unsubscribe = navigation.addListener('blur', () => {
setPlayerActive(false)
})
const interval = setInterval(() => {
viewRef.current?.measure((x, y, w, h, pageX, pageY) => {
const window = Dimensions.get('window')
const top = pageY
const bot = pageY + h
const isVisible = isNative
? top >= 0 && bot <= window.height
: !(top >= window.height || bot <= 0)
if (!isVisible) {
setPlayerActive(false)
}
})
}, 1e3)
return () => {
unsubscribe()
clearInterval(interval)
}
}, [viewRef, navigation])
// calculate height for the player and the screen size
const height = React.useMemo(
() =>
getPlayerHeight({
type: params.type,
width: dim.width,
hasThumb: !!link.thumb,
}),
[params.type, dim.width, link.thumb],
)
const onLoad = React.useCallback(() => {
setIsLoading(false)
}, [])
const onPlayPress = React.useCallback((event: GestureResponderEvent) => {
// Prevent this from propagating upward on web
event.preventDefault()
setPlayerActive(true)
}, [])
// measure the layout to set sizing
const onLayout = React.useCallback(
(event: {nativeEvent: {layout: {width: any; height: any}}}) => {
setDim({
width: event.nativeEvent.layout.width,
height: event.nativeEvent.layout.height,
})
},
[],
)
return (
<View
ref={viewRef}
style={{height}}
collapsable={false}
onLayout={onLayout}>
{link.thumb && (!isPlayerActive || isLoading) && (
<Image
style={[
{
width: dim.width,
height,
},
styles.topRadius,
]}
source={{uri: link.thumb}}
accessibilityIgnoresInvertColors
/>
)}
<PlaceholderOverlay
isLoading={isLoading}
isPlayerActive={isPlayerActive}
onPress={onPlayPress}
/>
<Player
isPlayerActive={isPlayerActive}
params={params}
height={height}
onLoad={onLoad}
/>
</View>
)
}
const styles = StyleSheet.create({
topRadius: {
borderTopLeftRadius: 6,
borderTopRightRadius: 6,
},
layer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
overlayContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
},
overlayLayer: {
zIndex: 2,
},
playerLayer: {
zIndex: 3,
},
webview: {
backgroundColor: 'transparent',
},
})
@@ -1,55 +0,0 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {AppBskyEmbedExternal} from '@atproto/api'
import {Link} from '../Link'
export const YoutubeEmbed = ({
link,
style,
}: {
link: AppBskyEmbedExternal.ViewExternal
style?: StyleProp<ViewStyle>
}) => {
const pal = usePalette('default')
const imageChild = (
<View style={styles.playButton}>
<FontAwesomeIcon icon="play" size={24} color="white" />
</View>
)
return (
<Link
asAnchor
style={[styles.extOuter, pal.view, pal.border, style]}
href={link.uri}>
<ExternalLinkEmbed link={link} imageChild={imageChild} />
</Link>
)
}
const styles = StyleSheet.create({
extOuter: {
borderWidth: 1,
borderRadius: 8,
},
playButton: {
position: 'absolute',
alignSelf: 'center',
alignItems: 'center',
top: '44%',
justifyContent: 'center',
backgroundColor: 'black',
padding: 10,
borderRadius: 50,
opacity: 0.8,
},
webView: {
alignItems: 'center',
alignContent: 'center',
justifyContent: 'center',
},
})
+5 -13
View File
@@ -23,9 +23,7 @@ import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
import {useLightboxControls, ImagesLightbox} from '#/state/lightbox' import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {YoutubeEmbed} from './YoutubeEmbed'
import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {ExternalLinkEmbed} from './ExternalLinkEmbed'
import {getYoutubeVideoId} from 'lib/strings/url-helpers'
import {MaybeQuoteEmbed} from './QuoteEmbed' import {MaybeQuoteEmbed} from './QuoteEmbed'
import {AutoSizedImage} from '../images/AutoSizedImage' import {AutoSizedImage} from '../images/AutoSizedImage'
import {ListEmbed} from './ListEmbed' import {ListEmbed} from './ListEmbed'
@@ -168,19 +166,13 @@ export function PostEmbeds({
// = // =
if (AppBskyEmbedExternal.isView(embed)) { if (AppBskyEmbedExternal.isView(embed)) {
const link = embed.external const link = embed.external
const youtubeVideoId = getYoutubeVideoId(link.uri)
if (youtubeVideoId) {
return <YoutubeEmbed link={link} style={style} />
}
return ( return (
<Link <View style={[styles.extOuter, pal.view, pal.border, style]}>
asAnchor <Link asAnchor href={link.uri}>
style={[styles.extOuter, pal.view, pal.border, style]} <ExternalLinkEmbed link={link} />
href={link.uri}> </Link>
<ExternalLinkEmbed link={link} /> </View>
</Link>
) )
} }
+3 -9
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {ActivityIndicator, StyleSheet, View, RefreshControl} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {ViewHeader} from 'view/com/util/ViewHeader' import {ViewHeader} from 'view/com/util/ViewHeader'
@@ -487,14 +487,8 @@ export function FeedsScreen(_props: Props) {
keyExtractor={item => item.key} keyExtractor={item => item.key}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
renderItem={renderItem} renderItem={renderItem}
refreshControl={ refreshing={isPTR}
<RefreshControl onRefresh={isUserSearching ? undefined : onPullToRefresh}
refreshing={isPTR}
onRefresh={isUserSearching ? undefined : onPullToRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
initialNumToRender={10} initialNumToRender={10}
onEndReached={onEndReached} onEndReached={onEndReached}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
+2 -10
View File
@@ -3,7 +3,6 @@ import {
View, View,
StyleSheet, StyleSheet,
ActivityIndicator, ActivityIndicator,
RefreshControl,
TextInput, TextInput,
Pressable, Pressable,
Platform, Platform,
@@ -185,7 +184,6 @@ type SearchResultSlice =
function SearchScreenPostResults({query}: {query: string}) { function SearchScreenPostResults({query}: {query: string}) {
const {_} = useLingui() const {_} = useLingui()
const pal = usePalette('default')
const [isPTR, setIsPTR] = React.useState(false) const [isPTR, setIsPTR] = React.useState(false)
const { const {
isFetched, isFetched,
@@ -254,14 +252,8 @@ function SearchScreenPostResults({query}: {query: string}) {
} }
}} }}
keyExtractor={item => item.key} keyExtractor={item => item.key}
refreshControl={ refreshing={isPTR}
<RefreshControl onRefresh={onPullToRefresh}
refreshing={isPTR}
onRefresh={onPullToRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
// @ts-ignore web only -prf // @ts-ignore web only -prf
desktopFixedHeight desktopFixedHeight
+45 -11
View File
@@ -10433,16 +10433,16 @@ escape-html@~1.0.3:
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
escape-string-regexp@^1.0.5: escape-string-regexp@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
escape-string-regexp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
escape-string-regexp@^4.0.0: escape-string-regexp@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@@ -12501,7 +12501,7 @@ interpret@^3.1.1:
resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
invariant@*, invariant@^2.2.4: invariant@*, invariant@2.2.4, invariant@^2.2.4:
version "2.2.4" version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -17330,6 +17330,13 @@ qs@6.11.0:
dependencies: dependencies:
side-channel "^1.0.4" side-channel "^1.0.4"
qs@^6.5.1:
version "6.11.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
dependencies:
side-channel "^1.0.4"
query-string@^7.1.3: query-string@^7.1.3:
version "7.1.3" version "7.1.3"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328"
@@ -17535,12 +17542,12 @@ react-native-dotenv@^3.3.1:
dependencies: dependencies:
dotenv "^16.3.1" dotenv "^16.3.1"
react-native-drawer-layout@^3.2.0: react-native-drawer-layout@^4.0.0-alpha.3:
version "3.2.1" version "4.0.0-alpha.3"
resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-3.2.1.tgz#eb626216181965e72de6d9377ca619fab40226f2" resolved "https://registry.yarnpkg.com/react-native-drawer-layout/-/react-native-drawer-layout-4.0.0-alpha.3.tgz#0adf51e816f2ce094d415fe33d9f43c1e6df6ae2"
integrity sha512-9GDzSLBblxox1zEACcbdXHMU8m22gCTdpT9g1G3Za4Bu766IDxVnIbgRGz6Z2dL99EMtz5E2JoWx257tdHkbpw== integrity sha512-BRlX8l2gDD41fs8RfaemjDBDV0PPspDMvej/3b9QWfp+JQ/H8tkSdWBtUBbSBzYdqyqy2bHV4epOCMb4t+0B0g==
dependencies: dependencies:
use-latest-callback "^0.1.5" use-latest-callback "^0.1.9"
react-native-fs@^2.20.0: react-native-fs@^2.20.0:
version "2.20.0" version "2.20.0"
@@ -17680,6 +17687,13 @@ react-native-web-linear-gradient@^1.1.2:
resolved "https://registry.yarnpkg.com/react-native-web-linear-gradient/-/react-native-web-linear-gradient-1.1.2.tgz#33f85f7085a0bb5ffa5106faf02ed105b92a9ed7" resolved "https://registry.yarnpkg.com/react-native-web-linear-gradient/-/react-native-web-linear-gradient-1.1.2.tgz#33f85f7085a0bb5ffa5106faf02ed105b92a9ed7"
integrity sha512-SmUnpwT49CEe78pXvIvYf72Es8Pv+ZYKCnEOgb2zAKpEUDMo0+xElfRJhwt5nfI8krJ5WbFPKnoDgD0uUjAN1A== integrity sha512-SmUnpwT49CEe78pXvIvYf72Es8Pv+ZYKCnEOgb2zAKpEUDMo0+xElfRJhwt5nfI8krJ5WbFPKnoDgD0uUjAN1A==
react-native-web-webview@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/react-native-web-webview/-/react-native-web-webview-1.0.2.tgz#c215efa70c17589f2c8d640b1f1dc669b18c6e02"
integrity sha512-oNAYNuqUqeqTuAAdIejzDqvUtYA+k5lrvhUYmASdUznZNmyIaoQFA6OKoA4K9F3wdMvark42vUXkUWIp875ewg==
dependencies:
qs "^6.5.1"
react-native-web@~0.19.6: react-native-web@~0.19.6:
version "0.19.8" version "0.19.8"
resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.19.8.tgz#46127f8b310148fde11e4fef67fe625603599d47" resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.19.8.tgz#46127f8b310148fde11e4fef67fe625603599d47"
@@ -17694,6 +17708,21 @@ react-native-web@~0.19.6:
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
styleq "^0.1.3" styleq "^0.1.3"
react-native-webview@^13.6.2:
version "13.6.2"
resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.6.2.tgz#0a9b18793e915add5b5dbdbf32509d7751b49167"
integrity sha512-QzhQ5JCU+Nf2W285DtvCZOVQy/MkJXMwNDYPZvOWQbAOgxJMSSO+BtqXTMA1UPugDsko6PxJ0TxSlUwIwJijDg==
dependencies:
escape-string-regexp "2.0.0"
invariant "2.2.4"
react-native-youtube-iframe@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-native-youtube-iframe/-/react-native-youtube-iframe-2.3.0.tgz#40ca8e55db929b91bfa8e8d30e411658cbc304c5"
integrity sha512-M+z63xwXVtS4dX3k8PbtHUUcWN+gRZt6J1EtPE7Y60BMOB979KjpkdrHqeR96or9pNR2W8K5tQhIkMXW2jwo7Q==
dependencies:
events "^3.2.0"
react-native@0.72.5: react-native@0.72.5:
version "0.72.5" version "0.72.5"
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.72.5.tgz#2c343fa6f3ead362cf07376634a33a4078864357" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.72.5.tgz#2c343fa6f3ead362cf07376634a33a4078864357"
@@ -20142,6 +20171,11 @@ use-latest-callback@^0.1.5:
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.6.tgz#3fa6e7babbb5f9bfa24b5094b22939e1e92ebcf6" resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.6.tgz#3fa6e7babbb5f9bfa24b5094b22939e1e92ebcf6"
integrity sha512-VO/P91A/PmKH9bcN9a7O3duSuxe6M14ZoYXgA6a8dab8doWNdhiIHzEkX/jFeTTRBsX0Ubk6nG4q2NIjNsj+bg== integrity sha512-VO/P91A/PmKH9bcN9a7O3duSuxe6M14ZoYXgA6a8dab8doWNdhiIHzEkX/jFeTTRBsX0Ubk6nG4q2NIjNsj+bg==
use-latest-callback@^0.1.9:
version "0.1.9"
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.1.9.tgz#10191dc54257e65a8e52322127643a8940271e2a"
integrity sha512-CL/29uS74AwreI/f2oz2hLTW7ZqVeV5+gxFeGudzQrgkCytrHw33G4KbnQOrRlAEzzAFXi7dDLMC9zhWcVpzmw==
use-sidecar@^1.1.2: use-sidecar@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"