Merge branch 'main' into patch-3
commit
a1127bfcfc
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
try {
|
||||||
closeCallback.current?.()
|
closeCallback.current?.()
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error(`Dialog closeCallback failed`, {
|
||||||
|
message: e.message,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
closeCallback.current = undefined
|
closeCallback.current = undefined
|
||||||
|
}
|
||||||
|
|
||||||
onClose?.()
|
onClose?.()
|
||||||
setOpenIndex(-1)
|
setOpenIndex(-1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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(',')],
|
||||||
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 () => {
|
queryFn: async () => {
|
||||||
const type = getFeedTypeFromUri(uri)
|
let resolved = new Map()
|
||||||
|
|
||||||
if (type === 'feed') {
|
// Get all feeds. We can do this in a batch.
|
||||||
const res =
|
const feedUris = pinnedUris.filter(
|
||||||
await getAgent().app.bsky.feed.getFeedGenerator({
|
uri => getFeedTypeFromUri(uri) === 'feed',
|
||||||
feed: uri,
|
)
|
||||||
|
let feedsPromise = Promise.resolve()
|
||||||
|
if (feedUris.length > 0) {
|
||||||
|
feedsPromise = getAgent()
|
||||||
|
.app.bsky.feed.getFeedGenerators({
|
||||||
|
feeds: feedUris,
|
||||||
})
|
})
|
||||||
return hydrateFeedGenerator(res.data.view)
|
.then(res => {
|
||||||
} else {
|
for (let feedView of res.data.feeds) {
|
||||||
const res = await getAgent().app.bsky.graph.getList({
|
resolved.set(feedView.uri, hydrateFeedGenerator(feedView))
|
||||||
list: uri,
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
limit: 1,
|
||||||
})
|
})
|
||||||
return hydrateList(res.data.list)
|
.then(res => {
|
||||||
|
const listView = res.data.list
|
||||||
|
resolved.set(listView.uri, hydrateList(listView))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (e) {
|
|
||||||
// expected failure
|
|
||||||
logger.info(`usePinnedFeedsInfos: failed to fetch ${uri}`, {
|
|
||||||
error: e,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const views = (await Promise.all(reqs)).filter(
|
|
||||||
Boolean,
|
|
||||||
) as FeedSourceInfo[]
|
|
||||||
|
|
||||||
setTabs([FOLLOWING_FEED_STUB].concat(views))
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchFeedInfo()
|
|
||||||
}, [queryClient, setTabs, preferences?.feeds?.pinned])
|
|
||||||
|
|
||||||
return {feeds: tabs, hasPinnedCustom, isLoading}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ✨')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
Loading…
Reference in New Issue