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/tos", server.WebGeneric)
e.GET("/support/community-guidelines", server.WebGeneric) e.GET("/support/community-guidelines", server.WebGeneric)
e.GET("/support/copyright", server.WebGeneric) e.GET("/support/copyright", server.WebGeneric)
e.GET("/intent/compose", server.WebGeneric)
// profile endpoints; only first populates info // profile endpoints; only first populates info
e.GET("/profile/:handleOrDID", server.WebProfile) e.GET("/profile/:handleOrDID", server.WebProfile)

View File

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

View File

@ -108,14 +108,26 @@ class RNUITextView: UIView {
fractionOfDistanceBetweenInsertionPoints: nil fractionOfDistanceBetweenInsertionPoints: nil
) )
var lastUpperBound: String.Index? = nil
for child in self.reactSubviews() { for child in self.reactSubviews() {
if let child = child as? RNUITextViewChild, let childText = child.text { if let child = child as? RNUITextViewChild, let childText = child.text {
let fullText = self.textView.attributedText.string 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 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 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) { export function web(value: any) {
return Platform.select({ if (isWeb) {
web: value, return value
}) }
} }
export function ios(value: any) { export function ios(value: any) {
return Platform.select({ if (isIOS) {
ios: value, return value
}) }
} }
export function android(value: any) { export function android(value: any) {
return Platform.select({ if (isAndroid) {
android: value, return value
}) }
} }
export function native(value: any) { export function native(value: any) {
return Platform.select({ if (isNative) {
native: value, 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 {useTheme, atoms as a, flatten} from '#/alf'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {createInput} from '#/components/forms/TextField' import {createInput} from '#/components/forms/TextField'
import {logger} from '#/logger'
import { import {
DialogOuterProps, DialogOuterProps,
@ -56,7 +57,7 @@ export function Outer({
) )
const close = React.useCallback<DialogControlProps['close']>(cb => { const close = React.useCallback<DialogControlProps['close']>(cb => {
if (cb) { if (cb && typeof cb === 'function') {
closeCallback.current = cb closeCallback.current = cb
} }
sheet.current?.close() sheet.current?.close()
@ -74,8 +75,16 @@ export function Outer({
const onChange = React.useCallback( const onChange = React.useCallback(
(index: number) => { (index: number) => {
if (index === -1) { if (index === -1) {
closeCallback.current?.() try {
closeCallback.current = undefined closeCallback.current?.()
} catch (e: any) {
logger.error(`Dialog closeCallback failed`, {
message: e.message,
})
} finally {
closeCallback.current = undefined
}
onClose?.() onClose?.()
setOpenIndex(-1) setOpenIndex(-1)
} }

View File

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

View File

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

View File

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

View File

@ -11,12 +11,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'outlineTag', targets: ['tag']}], mutedWords: [{value: 'outlineTag', targets: ['tag']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
['outlineTag'], outlineTags: ['outlineTag'],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -27,12 +27,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'inlineTag', targets: ['tag']}], mutedWords: [{value: 'inlineTag', targets: ['tag']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
['outlineTag'], outlineTags: ['outlineTag'],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -43,12 +43,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'inlineTag', targets: ['content']}], mutedWords: [{value: 'inlineTag', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
['outlineTag'], outlineTags: ['outlineTag'],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -59,12 +59,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'inlineTag', targets: ['tag']}], mutedWords: [{value: 'inlineTag', targets: ['tag']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(false) expect(match).toBe(false)
}) })
@ -80,12 +80,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: '希', targets: ['content']}], mutedWords: [{value: '希', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -96,12 +96,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'politics', targets: ['content']}], mutedWords: [{value: 'politics', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(false) expect(match).toBe(false)
}) })
@ -112,12 +112,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'javascript', targets: ['content']}], mutedWords: [{value: 'javascript', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -130,12 +130,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'javascript', targets: ['content']}], mutedWords: [{value: 'javascript', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -146,12 +146,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'ai', targets: ['content']}], mutedWords: [{value: 'ai', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(false) expect(match).toBe(false)
}) })
@ -162,12 +162,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'brain', targets: ['content']}], mutedWords: [{value: 'brain', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -178,12 +178,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: `:)`, targets: ['content']}], mutedWords: [{value: `:)`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -197,23 +197,23 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
it(`match: yay!`, () => { it(`match: yay!`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'yay!', targets: ['content']}], mutedWords: [{value: 'yay!', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: yay`, () => { it(`match: yay`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'yay', targets: ['content']}], mutedWords: [{value: 'yay', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -226,24 +226,24 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
it(`match: y!ppee`, () => { it(`match: y!ppee`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'y!ppee', targets: ['content']}], mutedWords: [{value: 'y!ppee', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
// single exclamation point, source has double // single exclamation point, source has double
it(`no match: y!ppee!`, () => { it(`no match: y!ppee!`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'y!ppee!', targets: ['content']}], mutedWords: [{value: 'y!ppee!', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -256,23 +256,23 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
it(`match: S@assy`, () => { it(`match: S@assy`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'S@assy', targets: ['content']}], mutedWords: [{value: 'S@assy', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: s@assy`, () => { it(`match: s@assy`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: 's@assy', targets: ['content']}], mutedWords: [{value: 's@assy', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -286,12 +286,12 @@ describe(`hasMutedWord`, () => {
// case insensitive // case insensitive
it(`match: new york times`, () => { it(`match: new york times`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'new york times', targets: ['content']}], mutedWords: [{value: 'new york times', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -304,23 +304,23 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
it(`match: !command`, () => { it(`match: !command`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `!command`, targets: ['content']}], mutedWords: [{value: `!command`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: command`, () => { it(`match: command`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `command`, targets: ['content']}], mutedWords: [{value: `command`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -331,12 +331,12 @@ describe(`hasMutedWord`, () => {
}) })
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
const match = hasMutedWord( const match = hasMutedWord({
[{value: `!command`, targets: ['content']}], mutedWords: [{value: `!command`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(false) expect(match).toBe(false)
}) })
@ -349,23 +349,23 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
it(`match: e/acc`, () => { it(`match: e/acc`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `e/acc`, targets: ['content']}], mutedWords: [{value: `e/acc`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: acc`, () => { it(`match: acc`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `acc`, targets: ['content']}], mutedWords: [{value: `acc`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -378,45 +378,45 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
it(`match: super-bad`, () => { it(`match: super-bad`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `super-bad`, targets: ['content']}], mutedWords: [{value: `super-bad`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: super`, () => { it(`match: super`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `super`, targets: ['content']}], mutedWords: [{value: `super`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: super bad`, () => { it(`match: super bad`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `super bad`, targets: ['content']}], mutedWords: [{value: `super bad`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: superbad`, () => { it(`match: superbad`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `superbad`, targets: ['content']}], mutedWords: [{value: `superbad`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(false) expect(match).toBe(false)
}) })
@ -429,47 +429,49 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
it(`match: idk what this would be`, () => { it(`match: idk what this would be`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `idk what this would be`, targets: ['content']}], mutedWords: [{value: `idk what this would be`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`no match: idk what this would be for`, () => { it(`no match: idk what this would be for`, () => {
// extra word // extra word
const match = hasMutedWord( const match = hasMutedWord({
[{value: `idk what this would be for`, targets: ['content']}], mutedWords: [
rt.text, {value: `idk what this would be for`, targets: ['content']},
rt.facets, ],
[], text: rt.text,
) facets: rt.facets,
outlineTags: [],
})
expect(match).toBe(false) expect(match).toBe(false)
}) })
it(`match: idk`, () => { it(`match: idk`, () => {
// extra word // extra word
const match = hasMutedWord( const match = hasMutedWord({
[{value: `idk`, targets: ['content']}], mutedWords: [{value: `idk`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: idkwhatthiswouldbe`, () => { it(`match: idkwhatthiswouldbe`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `idkwhatthiswouldbe`, targets: ['content']}], mutedWords: [{value: `idkwhatthiswouldbe`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(false) expect(match).toBe(false)
}) })
@ -482,45 +484,45 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
it(`match: context(iykyk)`, () => { it(`match: context(iykyk)`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `context(iykyk)`, targets: ['content']}], mutedWords: [{value: `context(iykyk)`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: context`, () => { it(`match: context`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `context`, targets: ['content']}], mutedWords: [{value: `context`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: iykyk`, () => { it(`match: iykyk`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `iykyk`, targets: ['content']}], mutedWords: [{value: `iykyk`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: (iykyk)`, () => { it(`match: (iykyk)`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `(iykyk)`, targets: ['content']}], mutedWords: [{value: `(iykyk)`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -533,12 +535,12 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
it(`match: 🦋`, () => { it(`match: 🦋`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: `🦋`, targets: ['content']}], mutedWords: [{value: `🦋`, targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
@ -553,23 +555,46 @@ describe(`hasMutedWord`, () => {
rt.detectFacetsWithoutResolution() rt.detectFacetsWithoutResolution()
it(`match: stop worrying`, () => { it(`match: stop worrying`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'stop worrying', targets: ['content']}], mutedWords: [{value: 'stop worrying', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, facets: rt.facets,
[], outlineTags: [],
) })
expect(match).toBe(true) expect(match).toBe(true)
}) })
it(`match: turtles, or how`, () => { it(`match: turtles, or how`, () => {
const match = hasMutedWord( const match = hasMutedWord({
[{value: 'turtles, or how', targets: ['content']}], mutedWords: [{value: 'turtles, or how', targets: ['content']}],
rt.text, text: rt.text,
rt.facets, 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) expect(match).toBe(true)
}) })

View File

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

View File

@ -21,12 +21,34 @@ const REGEX = {
WORD_BOUNDARY: /[\s\n\t\r\f\v]+?/g, WORD_BOUNDARY: /[\s\n\t\r\f\v]+?/g,
} }
export function hasMutedWord( /**
mutedWords: AppBskyActorDefs.MutedWord[], * List of 2-letter lang codes for languages that either don't use spaces, or
text: string, * don't use spaces in a way conducive to word-based filtering.
facets?: AppBskyRichtextFacet.Main[], *
outlineTags?: string[], * 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[]) const tags = ([] as string[])
.concat(outlineTags || []) .concat(outlineTags || [])
.concat( .concat(
@ -48,8 +70,9 @@ export function hasMutedWord(
if (tags.includes(mutedWord)) return true if (tags.includes(mutedWord)) return true
// rest of the checks are for `content` only // rest of the checks are for `content` only
if (!mute.targets.includes('content')) continue if (!mute.targets.includes('content')) continue
// single character, has to use includes // single character or other exception, has to use includes
if (mutedWord.length === 1 && postText.includes(mutedWord)) return true if ((mutedWord.length === 1 || exception) && postText.includes(mutedWord))
return true
// too long // too long
if (mutedWord.length > postText.length) continue if (mutedWord.length > postText.length) continue
// exact match // exact match
@ -134,19 +157,28 @@ export function moderatePost_wrapped(
} }
if (AppBskyFeedPost.isRecord(subject.record)) { if (AppBskyFeedPost.isRecord(subject.record)) {
let muted = hasMutedWord( let muted = hasMutedWord({
mutedWords, mutedWords,
subject.record.text, text: subject.record.text,
subject.record.facets || [], facets: subject.record.facets || [],
subject.record.tags || [], outlineTags: subject.record.tags || [],
) languages: subject.record.langs,
})
if ( if (
subject.record.embed && subject.record.embed &&
AppBskyEmbedImages.isMain(subject.record.embed) AppBskyEmbedImages.isMain(subject.record.embed)
) { ) {
for (const image of subject.record.embed.images) { 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)) { if (AppBskyFeedPost.isRecord(subject.embed.record.value)) {
embedHidden = embedHidden =
embedHidden || embedHidden ||
hasMutedWord( hasMutedWord({
mutedWords, mutedWords,
subject.embed.record.value.text, text: subject.embed.record.value.text,
subject.embed.record.value.facets, facets: subject.embed.record.value.facets,
subject.embed.record.value.tags, outlineTags: subject.embed.record.value.tags,
) languages: subject.embed.record.value.langs,
})
if (AppBskyEmbedImages.isMain(subject.embed.record.value.embed)) { if (AppBskyEmbedImages.isMain(subject.embed.record.value.embed)) {
for (const image of subject.embed.record.value.embed.images) { for (const image of subject.embed.record.value.embed.images) {
embedHidden = 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> </Trans>
</Prompt.Description> </Prompt.Description>
<Prompt.Actions> <Prompt.Actions>
<Prompt.Action onPress={prompt.close}> <Prompt.Action onPress={() => prompt.close()}>
<Trans>OK</Trans> <Trans>OK</Trans>
</Prompt.Action> </Prompt.Action>
</Prompt.Actions> </Prompt.Actions>

View File

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

View File

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

View File

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

View File

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

View File

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