Merge branch 'main' into patch-3

zio/stable
Minseo Lee 2024-02-29 09:51:56 +09:00 committed by GitHub
commit a1127bfcfc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 450 additions and 364 deletions

View File

@ -202,6 +202,7 @@ func serve(cctx *cli.Context) error {
e.GET("/support/tos", server.WebGeneric)
e.GET("/support/community-guidelines", server.WebGeneric)
e.GET("/support/copyright", server.WebGeneric)
e.GET("/intent/compose", server.WebGeneric)
// profile endpoints; only first populates info
e.GET("/profile/:handleOrDID", server.WebProfile)

View File

@ -119,7 +119,7 @@ class ShareViewController: UIViewController {
// extension does.
if let dir = FileManager()
.containerURL(
forSecurityApplicationGroupIdentifier: "group.\(Bundle.main.bundleIdentifier?.replacingOccurrences(of: ".Share-with-Bluesky", with: "") ?? "")")
forSecurityApplicationGroupIdentifier: "group.app.bsky")
{
let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"

View File

@ -108,14 +108,26 @@ class RNUITextView: UIView {
fractionOfDistanceBetweenInsertionPoints: nil
)
var lastUpperBound: String.Index? = nil
for child in self.reactSubviews() {
if let child = child as? RNUITextViewChild, let childText = child.text {
let fullText = self.textView.attributedText.string
let range = fullText.range(of: childText)
// We want to skip over the children we have already checked, otherwise we could run into
// collisions of similar strings (i.e. links that get shortened to the same hostname but
// different paths)
let range = fullText.range(of: childText, options: [], range: (lastUpperBound ?? String.Index(utf16Offset: 0, in: fullText) )..<fullText.endIndex)
if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
if charIndex >= lowerBound.utf16Offset(in: fullText) && charIndex <= upperBound.utf16Offset(in: fullText) {
let lowerOffset = lowerBound.utf16Offset(in: fullText)
let upperOffset = upperBound.utf16Offset(in: fullText)
if charIndex >= lowerOffset,
charIndex <= upperOffset
{
return child
} else {
lastUpperBound = upperBound
}
}
}

View File

@ -1,25 +1,25 @@
import {Platform} from 'react-native'
import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
export function web(value: any) {
return Platform.select({
web: value,
})
if (isWeb) {
return value
}
}
export function ios(value: any) {
return Platform.select({
ios: value,
})
if (isIOS) {
return value
}
}
export function android(value: any) {
return Platform.select({
android: value,
})
if (isAndroid) {
return value
}
}
export function native(value: any) {
return Platform.select({
native: value,
})
if (isNative) {
return value
}
}

View File

@ -11,6 +11,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useTheme, atoms as a, flatten} from '#/alf'
import {Portal} from '#/components/Portal'
import {createInput} from '#/components/forms/TextField'
import {logger} from '#/logger'
import {
DialogOuterProps,
@ -56,7 +57,7 @@ export function Outer({
)
const close = React.useCallback<DialogControlProps['close']>(cb => {
if (cb) {
if (cb && typeof cb === 'function') {
closeCallback.current = cb
}
sheet.current?.close()
@ -74,8 +75,16 @@ export function Outer({
const onChange = React.useCallback(
(index: number) => {
if (index === -1) {
closeCallback.current?.()
closeCallback.current = undefined
try {
closeCallback.current?.()
} catch (e: any) {
logger.error(`Dialog closeCallback failed`, {
message: e.message,
})
} finally {
closeCallback.current = undefined
}
onClose?.()
setOpenIndex(-1)
}

View File

@ -190,7 +190,7 @@ export function Close() {
variant="ghost"
color="secondary"
shape="round"
onPress={close}
onPress={() => close()}
label={_(msg`Close active dialog`)}>
<ButtonIcon icon={X} size="md" />
</Button>

View File

@ -6,8 +6,13 @@ import {ViewStyleProp} from '#/alf'
type A11yProps = Required<AccessibilityProps>
export type DialogControlProps = {
open: (options?: DialogControlOpenOptions) => void
close: (callback?: () => void) => void
}
export type DialogContextProps = {
close: () => void
close: DialogControlProps['close']
}
export type DialogControlOpenOptions = {
@ -20,11 +25,6 @@ export type DialogControlOpenOptions = {
index?: number
}
export type DialogControlProps = {
open: (options?: DialogControlOpenOptions) => void
close: (callback?: () => void) => void
}
export type DialogOuterProps = {
control: {
ref: React.RefObject<DialogControlProps>

View File

@ -89,7 +89,7 @@ export function Cancel({
color="secondary"
size="small"
label={_(msg`Cancel`)}
onPress={close}>
onPress={() => close()}>
{children}
</Button>
)

View File

@ -11,12 +11,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: 'outlineTag', targets: ['tag']}],
rt.text,
rt.facets,
['outlineTag'],
)
const match = hasMutedWord({
mutedWords: [{value: 'outlineTag', targets: ['tag']}],
text: rt.text,
facets: rt.facets,
outlineTags: ['outlineTag'],
})
expect(match).toBe(true)
})
@ -27,12 +27,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: 'inlineTag', targets: ['tag']}],
rt.text,
rt.facets,
['outlineTag'],
)
const match = hasMutedWord({
mutedWords: [{value: 'inlineTag', targets: ['tag']}],
text: rt.text,
facets: rt.facets,
outlineTags: ['outlineTag'],
})
expect(match).toBe(true)
})
@ -43,12 +43,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: 'inlineTag', targets: ['content']}],
rt.text,
rt.facets,
['outlineTag'],
)
const match = hasMutedWord({
mutedWords: [{value: 'inlineTag', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: ['outlineTag'],
})
expect(match).toBe(true)
})
@ -59,12 +59,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: 'inlineTag', targets: ['tag']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'inlineTag', targets: ['tag']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(false)
})
@ -80,12 +80,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: '希', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: '希', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -96,12 +96,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: 'politics', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'politics', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(false)
})
@ -112,12 +112,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: 'javascript', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'javascript', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -130,12 +130,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: 'javascript', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'javascript', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -146,12 +146,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: 'ai', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'ai', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(false)
})
@ -162,12 +162,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: 'brain', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'brain', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -178,12 +178,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: `:)`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `:)`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -197,23 +197,23 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution()
it(`match: yay!`, () => {
const match = hasMutedWord(
[{value: 'yay!', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'yay!', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: yay`, () => {
const match = hasMutedWord(
[{value: 'yay', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'yay', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -226,24 +226,24 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution()
it(`match: y!ppee`, () => {
const match = hasMutedWord(
[{value: 'y!ppee', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'y!ppee', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
// single exclamation point, source has double
it(`no match: y!ppee!`, () => {
const match = hasMutedWord(
[{value: 'y!ppee!', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'y!ppee!', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -256,23 +256,23 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution()
it(`match: S@assy`, () => {
const match = hasMutedWord(
[{value: 'S@assy', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'S@assy', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: s@assy`, () => {
const match = hasMutedWord(
[{value: 's@assy', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 's@assy', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -286,12 +286,12 @@ describe(`hasMutedWord`, () => {
// case insensitive
it(`match: new york times`, () => {
const match = hasMutedWord(
[{value: 'new york times', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'new york times', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -304,23 +304,23 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution()
it(`match: !command`, () => {
const match = hasMutedWord(
[{value: `!command`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `!command`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: command`, () => {
const match = hasMutedWord(
[{value: `command`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `command`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -331,12 +331,12 @@ describe(`hasMutedWord`, () => {
})
rt.detectFacetsWithoutResolution()
const match = hasMutedWord(
[{value: `!command`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `!command`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(false)
})
@ -349,23 +349,23 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution()
it(`match: e/acc`, () => {
const match = hasMutedWord(
[{value: `e/acc`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `e/acc`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: acc`, () => {
const match = hasMutedWord(
[{value: `acc`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `acc`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -378,45 +378,45 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution()
it(`match: super-bad`, () => {
const match = hasMutedWord(
[{value: `super-bad`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `super-bad`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: super`, () => {
const match = hasMutedWord(
[{value: `super`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `super`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: super bad`, () => {
const match = hasMutedWord(
[{value: `super bad`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `super bad`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: superbad`, () => {
const match = hasMutedWord(
[{value: `superbad`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `superbad`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(false)
})
@ -429,47 +429,49 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution()
it(`match: idk what this would be`, () => {
const match = hasMutedWord(
[{value: `idk what this would be`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `idk what this would be`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`no match: idk what this would be for`, () => {
// extra word
const match = hasMutedWord(
[{value: `idk what this would be for`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [
{value: `idk what this would be for`, targets: ['content']},
],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(false)
})
it(`match: idk`, () => {
// extra word
const match = hasMutedWord(
[{value: `idk`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `idk`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: idkwhatthiswouldbe`, () => {
const match = hasMutedWord(
[{value: `idkwhatthiswouldbe`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `idkwhatthiswouldbe`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(false)
})
@ -482,45 +484,45 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution()
it(`match: context(iykyk)`, () => {
const match = hasMutedWord(
[{value: `context(iykyk)`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `context(iykyk)`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: context`, () => {
const match = hasMutedWord(
[{value: `context`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `context`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: iykyk`, () => {
const match = hasMutedWord(
[{value: `iykyk`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `iykyk`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: (iykyk)`, () => {
const match = hasMutedWord(
[{value: `(iykyk)`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `(iykyk)`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -533,12 +535,12 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution()
it(`match: 🦋`, () => {
const match = hasMutedWord(
[{value: `🦋`, targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: `🦋`, targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
@ -553,23 +555,46 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution()
it(`match: stop worrying`, () => {
const match = hasMutedWord(
[{value: 'stop worrying', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'stop worrying', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
it(`match: turtles, or how`, () => {
const match = hasMutedWord(
[{value: 'turtles, or how', targets: ['content']}],
rt.text,
rt.facets,
[],
)
const match = hasMutedWord({
mutedWords: [{value: 'turtles, or how', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(true)
})
})
})
describe(`languages without spaces`, () => {
// I love turtles, or how I learned to stop worrying and love the internet
describe(`私はカメが好きです、またはどのようにして心配するのをやめてインターネットを愛するようになったのか`, () => {
const rt = new RichText({
text: `私はカメが好きです、またはどのようにして心配するのをやめてインターネットを愛するようになったのか`,
})
rt.detectFacetsWithoutResolution()
// internet
it(`match: インターネット`, () => {
const match = hasMutedWord({
mutedWords: [{value: 'インターネット', targets: ['content']}],
text: rt.text,
facets: rt.facets,
outlineTags: [],
languages: ['ja'],
})
expect(match).toBe(true)
})

View File

@ -3,6 +3,7 @@ import * as Linking from 'expo-linking'
import {isNative} from 'platform/detection'
import {useComposerControls} from 'state/shell'
import {useSession} from 'state/session'
import {useCloseAllActiveElements} from 'state/util'
type IntentType = 'compose'
@ -42,6 +43,7 @@ export function useIntentHandler() {
}
function useComposeIntent() {
const closeAllActiveElements = useCloseAllActiveElements()
const {openComposer} = useComposerControls()
const {hasSession} = useSession()
@ -55,6 +57,8 @@ function useComposeIntent() {
}) => {
if (!hasSession) return
closeAllActiveElements()
const imageUris = imageUrisStr
?.split(',')
.filter(part => {
@ -82,6 +86,6 @@ function useComposeIntent() {
})
}, 500)
},
[openComposer, hasSession],
[hasSession, closeAllActiveElements, openComposer],
)
}

View File

@ -21,12 +21,34 @@ const REGEX = {
WORD_BOUNDARY: /[\s\n\t\r\f\v]+?/g,
}
export function hasMutedWord(
mutedWords: AppBskyActorDefs.MutedWord[],
text: string,
facets?: AppBskyRichtextFacet.Main[],
outlineTags?: string[],
) {
/**
* List of 2-letter lang codes for languages that either don't use spaces, or
* don't use spaces in a way conducive to word-based filtering.
*
* For these, we use a simple `String.includes` to check for a match.
*/
const LANGUAGE_EXCEPTIONS = [
'ja', // Japanese
'zh', // Chinese
'ko', // Korean
'th', // Thai
'vi', // Vietnamese
]
export function hasMutedWord({
mutedWords,
text,
facets,
outlineTags,
languages,
}: {
mutedWords: AppBskyActorDefs.MutedWord[]
text: string
facets?: AppBskyRichtextFacet.Main[]
outlineTags?: string[]
languages?: string[]
}) {
const exception = LANGUAGE_EXCEPTIONS.includes(languages?.[0] || '')
const tags = ([] as string[])
.concat(outlineTags || [])
.concat(
@ -48,8 +70,9 @@ export function hasMutedWord(
if (tags.includes(mutedWord)) return true
// rest of the checks are for `content` only
if (!mute.targets.includes('content')) continue
// single character, has to use includes
if (mutedWord.length === 1 && postText.includes(mutedWord)) return true
// single character or other exception, has to use includes
if ((mutedWord.length === 1 || exception) && postText.includes(mutedWord))
return true
// too long
if (mutedWord.length > postText.length) continue
// exact match
@ -134,19 +157,28 @@ export function moderatePost_wrapped(
}
if (AppBskyFeedPost.isRecord(subject.record)) {
let muted = hasMutedWord(
let muted = hasMutedWord({
mutedWords,
subject.record.text,
subject.record.facets || [],
subject.record.tags || [],
)
text: subject.record.text,
facets: subject.record.facets || [],
outlineTags: subject.record.tags || [],
languages: subject.record.langs,
})
if (
subject.record.embed &&
AppBskyEmbedImages.isMain(subject.record.embed)
) {
for (const image of subject.record.embed.images) {
muted = muted || hasMutedWord(mutedWords, image.alt, [], [])
muted =
muted ||
hasMutedWord({
mutedWords,
text: image.alt,
facets: [],
outlineTags: [],
languages: subject.record.langs,
})
}
}
@ -172,17 +204,25 @@ export function moderatePost_wrapped(
if (AppBskyFeedPost.isRecord(subject.embed.record.value)) {
embedHidden =
embedHidden ||
hasMutedWord(
hasMutedWord({
mutedWords,
subject.embed.record.value.text,
subject.embed.record.value.facets,
subject.embed.record.value.tags,
)
text: subject.embed.record.value.text,
facets: subject.embed.record.value.facets,
outlineTags: subject.embed.record.value.tags,
languages: subject.embed.record.value.langs,
})
if (AppBskyEmbedImages.isMain(subject.embed.record.value.embed)) {
for (const image of subject.embed.record.value.embed.images) {
embedHidden =
embedHidden || hasMutedWord(mutedWords, image.alt, [], [])
embedHidden ||
hasMutedWord({
mutedWords,
text: image.alt,
facets: [],
outlineTags: [],
languages: subject.embed.record.value.langs,
})
}
}
}

View File

@ -118,7 +118,7 @@ export function AdultContentEnabledPref({
</Trans>
</Prompt.Description>
<Prompt.Actions>
<Prompt.Action onPress={prompt.close}>
<Prompt.Action onPress={() => prompt.close()}>
<Trans>OK</Trans>
</Prompt.Action>
</Prompt.Actions>

View File

@ -1,11 +1,9 @@
import React from 'react'
import {
useQuery,
useInfiniteQuery,
InfiniteData,
QueryKey,
useMutation,
useQueryClient,
} from '@tanstack/react-query'
import {
AtUri,
@ -15,7 +13,6 @@ import {
AppBskyUnspeccedGetPopularFeedGenerators,
} from '@atproto/api'
import {logger} from '#/logger'
import {router} from '#/routes'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
@ -219,83 +216,59 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
likeUri: '',
}
export function usePinnedFeedsInfos(): {
feeds: FeedSourceInfo[]
hasPinnedCustom: boolean
isLoading: boolean
} {
const queryClient = useQueryClient()
const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([
FOLLOWING_FEED_STUB,
])
const [isLoading, setLoading] = React.useState(true)
const {data: preferences} = usePreferencesQuery()
export function usePinnedFeedsInfos() {
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const pinnedUris = preferences?.feeds?.pinned ?? []
const hasPinnedCustom = React.useMemo<boolean>(() => {
return tabs.some(tab => tab !== FOLLOWING_FEED_STUB)
}, [tabs])
return useQuery({
staleTime: STALE.INFINITY,
enabled: !isLoadingPrefs,
queryKey: ['pinnedFeedsInfos', pinnedUris.join(',')],
queryFn: async () => {
let resolved = new Map()
React.useEffect(() => {
if (!preferences?.feeds?.pinned) return
const uris = preferences.feeds.pinned
async function fetchFeedInfo() {
const reqs = []
for (const uri of uris) {
const cached = queryClient.getQueryData<FeedSourceInfo>(
feedSourceInfoQueryKey({uri}),
)
if (cached) {
reqs.push(cached)
} else {
reqs.push(
(async () => {
// these requests can fail, need to filter those out
try {
return await queryClient.fetchQuery({
staleTime: STALE.SECONDS.FIFTEEN,
queryKey: feedSourceInfoQueryKey({uri}),
queryFn: async () => {
const type = getFeedTypeFromUri(uri)
if (type === 'feed') {
const res =
await getAgent().app.bsky.feed.getFeedGenerator({
feed: uri,
})
return hydrateFeedGenerator(res.data.view)
} else {
const res = await getAgent().app.bsky.graph.getList({
list: uri,
limit: 1,
})
return hydrateList(res.data.list)
}
},
})
} catch (e) {
// expected failure
logger.info(`usePinnedFeedsInfos: failed to fetch ${uri}`, {
error: e,
})
}
})(),
)
}
// Get all feeds. We can do this in a batch.
const feedUris = pinnedUris.filter(
uri => getFeedTypeFromUri(uri) === 'feed',
)
let feedsPromise = Promise.resolve()
if (feedUris.length > 0) {
feedsPromise = getAgent()
.app.bsky.feed.getFeedGenerators({
feeds: feedUris,
})
.then(res => {
for (let feedView of res.data.feeds) {
resolved.set(feedView.uri, hydrateFeedGenerator(feedView))
}
})
}
const views = (await Promise.all(reqs)).filter(
Boolean,
) as FeedSourceInfo[]
// Get all lists. This currently has to be done individually.
const listUris = pinnedUris.filter(
uri => getFeedTypeFromUri(uri) === 'list',
)
const listsPromises = listUris.map(listUri =>
getAgent()
.app.bsky.graph.getList({
list: listUri,
limit: 1,
})
.then(res => {
const listView = res.data.list
resolved.set(listView.uri, hydrateList(listView))
}),
)
setTabs([FOLLOWING_FEED_STUB].concat(views))
setLoading(false)
}
fetchFeedInfo()
}, [queryClient, setTabs, preferences?.feeds?.pinned])
return {feeds: tabs, hasPinnedCustom, isLoading}
// The returned result will have the original order.
const result = [FOLLOWING_FEED_STUB]
await Promise.allSettled([feedsPromise, ...listsPromises])
for (let pinnedUri of pinnedUris) {
if (resolved.has(pinnedUri)) {
result.push(resolved.get(pinnedUri))
}
}
return result
},
})
}

View File

@ -3,6 +3,7 @@ import {useLightboxControls} from './lightbox'
import {useModalControls} from './modals'
import {useComposerControls} from './shell/composer'
import {useSetDrawerOpen} from './shell/drawer-open'
import {useDialogStateControlContext} from 'state/dialogs'
/**
* returns true if something was closed
@ -35,11 +36,19 @@ export function useCloseAllActiveElements() {
const {closeLightbox} = useLightboxControls()
const {closeAllModals} = useModalControls()
const {closeComposer} = useComposerControls()
const {closeAllDialogs: closeAlfDialogs} = useDialogStateControlContext()
const setDrawerOpen = useSetDrawerOpen()
return useCallback(() => {
closeLightbox()
closeAllModals()
closeComposer()
closeAlfDialogs()
setDrawerOpen(false)
}, [closeLightbox, closeAllModals, closeComposer, setDrawerOpen])
}, [
closeLightbox,
closeAllModals,
closeComposer,
closeAlfDialogs,
setDrawerOpen,
])
}

View File

@ -1,7 +1,7 @@
import React from 'react'
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
import {HomeHeaderLayout} from './HomeHeaderLayout'
import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {FeedSourceInfo} from '#/state/queries/feed'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {isWeb} from 'platform/detection'
@ -9,15 +9,22 @@ import {TabBar} from '../pager/TabBar'
import {usePalette} from '#/lib/hooks/usePalette'
export function HomeHeader(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
props: RenderTabBarFnProps & {
testID?: string
onPressSelected: () => void
feeds: FeedSourceInfo[]
},
) {
const {feeds} = props
const navigation = useNavigation<NavigationProp>()
const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
const pal = usePalette('default')
const hasPinnedCustom = React.useMemo<boolean>(() => {
return feeds.some(tab => tab.uri !== '')
}, [feeds])
const items = React.useMemo(() => {
const pinnedNames = feeds.map(f => f.displayName)
if (!hasPinnedCustom) {
return pinnedNames.concat('Feeds ✨')
}

View File

@ -17,11 +17,12 @@ import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {emitSoftReset} from '#/state/events'
import {useSession} from '#/state/session'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export function HomeScreen(props: Props) {
const {data: preferences} = usePreferencesQuery()
const {feeds: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
usePinnedFeedsInfos()
if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
return (
@ -66,6 +67,8 @@ function HomeScreenReady({
const selectedIndex = Math.max(0, maybeFoundIndex)
const selectedFeed = allFeeds[selectedIndex]
useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName)
const pagerRef = React.useRef<PagerRef>(null)
const lastPagerReportedIndexRef = React.useRef(selectedIndex)
React.useLayoutEffect(() => {
@ -124,10 +127,11 @@ function HomeScreenReady({
onSelect={props.onSelect}
testID="homeScreenFeedTabs"
onPressSelected={onPressSelected}
feeds={pinnedFeedInfos}
/>
)
},
[onPressSelected],
[onPressSelected, pinnedFeedInfos],
)
const renderFollowingEmptyState = React.useCallback(() => {

View File

@ -15,7 +15,7 @@ import {emitSoftReset} from '#/state/events'
export function DesktopFeeds() {
const pal = usePalette('default')
const {_} = useLingui()
const {feeds: pinnedFeedInfos} = usePinnedFeedsInfos()
const {data: pinnedFeedInfos} = usePinnedFeedsInfos()
const selectedFeed = useSelectedFeed()
const setSelectedFeed = useSetSelectedFeed()
const navigation = useNavigation<NavigationProp>()
@ -25,7 +25,9 @@ export function DesktopFeeds() {
}
return getCurrentRoute(state)
})
if (!pinnedFeedInfos) {
return null
}
return (
<View style={[styles.container, pal.view]}>
{pinnedFeedInfos.map(feedInfo => {