Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90dc3edc5b |
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1.
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Details**
|
||||
|
||||
- Platform: <!-- desktop chrome windows, mobile safari, iOS, Android -->
|
||||
- Platform version:
|
||||
- App version:
|
||||
|
||||
**Additional context**
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
@@ -1,67 +0,0 @@
|
||||
name: "Bug Report"
|
||||
description: "Create a report for an issue you have experience in the app."
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report an issue you've found in the app! Before you submit this issue, please make
|
||||
sure you've searched for similar issues that are already open and being tracked. If you find an open issue that
|
||||
seems relevant to yours, it is best to leave a response there with your information instead of opening a new
|
||||
issue, since it helps to consolidate the info in one place.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
Please provide the steps that may reproduce the issue that you are experiencing. The more concise of a
|
||||
reproduction that you provide, the easier it will be for us to identify the issue and fix it in a timely manner.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: |
|
||||
If possible, please provide any images or videos that may help us understand the issue you are experiencing.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What platform(s) does this occur on?
|
||||
multiple: true
|
||||
options:
|
||||
- iOS
|
||||
- Android
|
||||
- Web (Desktop)
|
||||
- Web (Mobile)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Device Info
|
||||
description: |
|
||||
Please provide the device information that you are using when experiencing this issue. This can include the
|
||||
device model, operating system version, and any other relevant information.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: What version of the app are you using?
|
||||
description: |
|
||||
You can easily find this information by visting the Settings screen in the app, and tapping on the version info
|
||||
at the bottom of the screen. All the relevant information will be copied to your clipboard, and you will be
|
||||
able to share it here.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: |
|
||||
Please provide any additional information that you think may be helpful in identifying the issue you are
|
||||
experiencing. This can include any error messages, logs, or other information that you think may be relevant.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,72 +0,0 @@
|
||||
name: "Bug Report - Fabric/New Architecture"
|
||||
description: "Create a bug report for builds using the New Architecture. If you are not sure what that means, don't use this template, and instead use the default bug report template."
|
||||
labels: ["bug", "fabric"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: You're running a build that uses the React Native New Architecture, right?
|
||||
description: This template is specifically for issues related to builds that use the React Native New Architecture. If you are not sure what that means, this template is probably not relevant to you, and you should use the default Bug Report template.
|
||||
options:
|
||||
- label: I am running a build that uses the New Architecture
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report an issue you've found in the app! Before you submit this issue, please make
|
||||
sure you've searched for similar issues that are already open and being tracked. If you find an open issue that
|
||||
seems relevant to yours, it is best to leave a response there with your information instead of opening a new
|
||||
issue, since it helps to consolidate the info in one place.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
Please provide the steps that may reproduce the issue that you are experiencing. The more concise of a
|
||||
reproduction that you provide, the easier it will be for us to identify the issue and fix it in a timely manner.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: |
|
||||
If possible, please provide any images or videos that may help us understand the issue you are experiencing.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What platform(s) does this occur on?
|
||||
multiple: true
|
||||
options:
|
||||
- iOS
|
||||
- Android
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Device Info
|
||||
description: |
|
||||
Please provide the device information that you are using when experiencing this issue. This can include the
|
||||
device model, operating system version, and any other relevant information.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: What version of the app are you using?
|
||||
description: |
|
||||
You can easily find this information by visting the Settings screen in the app, and tapping on the version info
|
||||
at the bottom of the screen. All the relevant information will be copied to your clipboard, and you will be
|
||||
able to share it here.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: |
|
||||
Please provide any additional information that you think may be helpful in identifying the issue you are
|
||||
experiencing. This can include any error messages, logs, or other information that you think may be relevant.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature-request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
@@ -1,45 +0,0 @@
|
||||
name: "Feature Request"
|
||||
description: "Suggest an idea for the Bluesky app."
|
||||
labels: ["feature-request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for submitting a feature request! Before you submit this request, please make sure you've searched for
|
||||
similar requests that are already open and being tracked. If you find an open request that seems relevant to
|
||||
yours, it is best to leave a response there with your information instead of opening a new request, since it
|
||||
helps to consolidate the info in one place.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Feature
|
||||
description: |
|
||||
Please provide a detailed description of the feature you would like to see implemented in the app. The more
|
||||
information you provide, the easier it will be for us to understand the feature you are requesting and to
|
||||
implement it in a timely manner.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: |
|
||||
If helpful, provide some images or videos of similar features in other apps that you think would be helpful
|
||||
for us to understand the feature you are requesting, or places within the app that you think the feature belongs
|
||||
in or is missing from.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe Alternatives
|
||||
description: |
|
||||
If there are any alternative solutions or features that you think would be helpful to implement in place of the
|
||||
feature you are requesting, please provide a description of those as well.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: |
|
||||
If there is any additional context or information that you think would be helpful for us to know about the
|
||||
feature you are requesting, please provide that information here.
|
||||
validations:
|
||||
required: false
|
||||
@@ -15,6 +15,3 @@ android
|
||||
ios
|
||||
src/locale/locales
|
||||
lib/react-compiler-runtime
|
||||
bskyweb/static
|
||||
coverage
|
||||
web-build
|
||||
|
||||
@@ -279,7 +279,6 @@ func serve(cctx *cli.Context) error {
|
||||
|
||||
// starter packs
|
||||
e.GET("/starter-pack/:handleOrDID/:rkey", server.WebStarterPack)
|
||||
e.GET("/starter-pack-short/:code", server.WebGeneric)
|
||||
e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack)
|
||||
|
||||
// ipcc
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import {useFonts} from 'expo-font'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {Device, device} from '#/storage'
|
||||
|
||||
@@ -65,15 +63,3 @@ export function applyFonts(
|
||||
*/
|
||||
style.fontVariant = ['no-contextual']
|
||||
}
|
||||
|
||||
/*
|
||||
* IMPORTANT: This is unused. Expo statically extracts these fonts.
|
||||
*
|
||||
* All used fonts MUST be configured here. Unused fonts can be commented out.
|
||||
*/
|
||||
export function DO_NOT_USE() {
|
||||
return useFonts({
|
||||
InterVariable: require('../../assets/fonts/inter/InterVariable.ttf'),
|
||||
'InterVariable-Italic': require('../../assets/fonts/inter/InterVariable-Italic.ttf'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import React from 'react'
|
||||
import {TextInput, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass2'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
|
||||
type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
|
||||
label?: TextField.InputProps['label']
|
||||
/**
|
||||
* Called when the user presses the (X) button
|
||||
*/
|
||||
onClearText?: () => void
|
||||
}
|
||||
|
||||
export const SearchInput = React.forwardRef<TextInput, SearchInputProps>(
|
||||
function SearchInput({value, label, onClearText, ...rest}, ref) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<View style={[a.w_full, a.relative]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={MagnifyingGlassIcon} />
|
||||
<TextField.Input
|
||||
inputRef={ref}
|
||||
label={label || _(msg`Search`)}
|
||||
value={value}
|
||||
placeholder={_(msg`Search`)}
|
||||
returnKeyType="search"
|
||||
keyboardAppearance={t.scheme}
|
||||
selectTextOnFocus={isNative}
|
||||
autoFocus={false}
|
||||
accessibilityRole="search"
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
{...rest}
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
{value && value.length > 0 && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
a.my_auto,
|
||||
a.inset_0,
|
||||
a.justify_center,
|
||||
a.pr_sm,
|
||||
{left: 'auto'},
|
||||
]}>
|
||||
<Button
|
||||
testID="searchTextInputClearBtn"
|
||||
onPress={onClearText}
|
||||
label={_(msg`Clear search query`)}
|
||||
hitSlop={HITSLOP_10}
|
||||
size="tiny"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary">
|
||||
<ButtonIcon icon={X} size="xs" />
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -126,7 +126,7 @@ export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {
|
||||
value?: string
|
||||
onChangeText?: (value: string) => void
|
||||
isInvalid?: boolean
|
||||
inputRef?: React.RefObject<TextInput> | React.ForwardedRef<TextInput>
|
||||
inputRef?: React.RefObject<TextInput>
|
||||
}
|
||||
|
||||
export function createInput(Component: typeof TextInput) {
|
||||
|
||||
+137
-119
@@ -13,8 +13,6 @@ import {
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
|
||||
import {logger} from '#/logger'
|
||||
import {ComposerImage, compressImage} from '#/state/gallery'
|
||||
import {writePostgateRecord} from '#/state/queries/postgate'
|
||||
@@ -24,7 +22,8 @@ import {
|
||||
threadgateAllowUISettingToAllowRecordValue,
|
||||
writeThreadgateRecord,
|
||||
} from '#/state/queries/threadgate'
|
||||
import {ComposerState} from '#/view/com/composer/state'
|
||||
import {isNetworkError} from 'lib/strings/errors'
|
||||
import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip'
|
||||
import {LinkMeta} from '../link-meta/link-meta'
|
||||
import {uploadBlob} from './upload-blob'
|
||||
|
||||
@@ -39,7 +38,6 @@ export interface ExternalEmbedDraft {
|
||||
}
|
||||
|
||||
interface PostOpts {
|
||||
composerState: ComposerState // TODO: Not used yet.
|
||||
rawText: string
|
||||
replyTo?: string
|
||||
quote?: {
|
||||
@@ -62,6 +60,13 @@ interface PostOpts {
|
||||
}
|
||||
|
||||
export async function post(agent: BskyAgent, opts: PostOpts) {
|
||||
let embed:
|
||||
| AppBskyEmbedImages.Main
|
||||
| AppBskyEmbedExternal.Main
|
||||
| AppBskyEmbedRecord.Main
|
||||
| AppBskyEmbedVideo.Main
|
||||
| AppBskyEmbedRecordWithMedia.Main
|
||||
| undefined
|
||||
let reply
|
||||
let rt = new RichText({text: opts.rawText.trimEnd()}, {cleanNewlines: true})
|
||||
|
||||
@@ -72,7 +77,134 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
|
||||
rt = shortenLinks(rt)
|
||||
rt = stripInvalidMentions(rt)
|
||||
|
||||
const embed = await resolveEmbed(agent, opts)
|
||||
// add quote embed if present
|
||||
if (opts.quote) {
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.record',
|
||||
record: {
|
||||
uri: opts.quote.uri,
|
||||
cid: opts.quote.cid,
|
||||
},
|
||||
} as AppBskyEmbedRecord.Main
|
||||
}
|
||||
|
||||
// add image embed if present
|
||||
if (opts.images?.length) {
|
||||
logger.debug(`Uploading images`, {
|
||||
count: opts.images.length,
|
||||
})
|
||||
|
||||
const images: AppBskyEmbedImages.Image[] = []
|
||||
for (const image of opts.images) {
|
||||
opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
|
||||
|
||||
logger.debug(`Compressing image`)
|
||||
const {path, width, height, mime} = await compressImage(image)
|
||||
|
||||
logger.debug(`Uploading image`)
|
||||
const res = await uploadBlob(agent, path, mime)
|
||||
|
||||
images.push({
|
||||
image: res.data.blob,
|
||||
alt: image.alt,
|
||||
aspectRatio: {width, height},
|
||||
})
|
||||
}
|
||||
|
||||
if (opts.quote) {
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.recordWithMedia',
|
||||
record: embed,
|
||||
media: {
|
||||
$type: 'app.bsky.embed.images',
|
||||
images,
|
||||
},
|
||||
} as AppBskyEmbedRecordWithMedia.Main
|
||||
} else {
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.images',
|
||||
images,
|
||||
} as AppBskyEmbedImages.Main
|
||||
}
|
||||
}
|
||||
|
||||
// add video embed if present
|
||||
if (opts.video) {
|
||||
const captions = await Promise.all(
|
||||
opts.video.captions
|
||||
.filter(caption => caption.lang !== '')
|
||||
.map(async caption => {
|
||||
const {data} = await agent.uploadBlob(caption.file, {
|
||||
encoding: 'text/vtt',
|
||||
})
|
||||
return {lang: caption.lang, file: data.blob}
|
||||
}),
|
||||
)
|
||||
if (opts.quote) {
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.recordWithMedia',
|
||||
record: embed,
|
||||
media: {
|
||||
$type: 'app.bsky.embed.video',
|
||||
video: opts.video.blobRef,
|
||||
alt: opts.video.altText || undefined,
|
||||
captions: captions.length === 0 ? undefined : captions,
|
||||
aspectRatio: opts.video.aspectRatio,
|
||||
} as AppBskyEmbedVideo.Main,
|
||||
} as AppBskyEmbedRecordWithMedia.Main
|
||||
} else {
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.video',
|
||||
video: opts.video.blobRef,
|
||||
alt: opts.video.altText || undefined,
|
||||
captions: captions.length === 0 ? undefined : captions,
|
||||
aspectRatio: opts.video.aspectRatio,
|
||||
} as AppBskyEmbedVideo.Main
|
||||
}
|
||||
}
|
||||
|
||||
// add external embed if present
|
||||
if (opts.extLink && !opts.images?.length) {
|
||||
if (opts.extLink.embed) {
|
||||
embed = opts.extLink.embed
|
||||
} else {
|
||||
let thumb
|
||||
if (opts.extLink.localThumb) {
|
||||
opts.onStateChange?.('Uploading link thumbnail...')
|
||||
|
||||
const {path, mime} = opts.extLink.localThumb.source
|
||||
const res = await uploadBlob(agent, path, mime)
|
||||
|
||||
thumb = res.data.blob
|
||||
}
|
||||
|
||||
if (opts.quote) {
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.recordWithMedia',
|
||||
record: embed,
|
||||
media: {
|
||||
$type: 'app.bsky.embed.external',
|
||||
external: {
|
||||
uri: opts.extLink.uri,
|
||||
title: opts.extLink.meta?.title || '',
|
||||
description: opts.extLink.meta?.description || '',
|
||||
thumb,
|
||||
},
|
||||
} as AppBskyEmbedExternal.Main,
|
||||
} as AppBskyEmbedRecordWithMedia.Main
|
||||
} else {
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.external',
|
||||
external: {
|
||||
uri: opts.extLink.uri,
|
||||
title: opts.extLink.meta?.title || '',
|
||||
description: opts.extLink.meta?.description || '',
|
||||
thumb,
|
||||
},
|
||||
} as AppBskyEmbedExternal.Main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add replyTo if post is a reply to another post
|
||||
if (opts.replyTo) {
|
||||
@@ -181,117 +313,3 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function resolveEmbed(
|
||||
agent: BskyAgent,
|
||||
opts: PostOpts,
|
||||
): Promise<
|
||||
| AppBskyEmbedImages.Main
|
||||
| AppBskyEmbedVideo.Main
|
||||
| AppBskyEmbedExternal.Main
|
||||
| AppBskyEmbedRecord.Main
|
||||
| AppBskyEmbedRecordWithMedia.Main
|
||||
| undefined
|
||||
> {
|
||||
const media = await resolveMedia(agent, opts)
|
||||
if (opts.quote) {
|
||||
const quoteRecord = {
|
||||
$type: 'app.bsky.embed.record',
|
||||
record: {
|
||||
uri: opts.quote.uri,
|
||||
cid: opts.quote.cid,
|
||||
},
|
||||
}
|
||||
if (media) {
|
||||
return {
|
||||
$type: 'app.bsky.embed.recordWithMedia',
|
||||
record: quoteRecord,
|
||||
media,
|
||||
}
|
||||
} else {
|
||||
return quoteRecord
|
||||
}
|
||||
}
|
||||
if (media) {
|
||||
return media
|
||||
}
|
||||
if (opts.extLink?.embed) {
|
||||
return opts.extLink.embed
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function resolveMedia(
|
||||
agent: BskyAgent,
|
||||
opts: PostOpts,
|
||||
): Promise<
|
||||
| AppBskyEmbedExternal.Main
|
||||
| AppBskyEmbedImages.Main
|
||||
| AppBskyEmbedVideo.Main
|
||||
| undefined
|
||||
> {
|
||||
if (opts.images?.length) {
|
||||
logger.debug(`Uploading images`, {
|
||||
count: opts.images.length,
|
||||
})
|
||||
opts.onStateChange?.(`Uploading images...`)
|
||||
const images: AppBskyEmbedImages.Image[] = await Promise.all(
|
||||
opts.images.map(async (image, i) => {
|
||||
logger.debug(`Compressing image #${i}`)
|
||||
const {path, width, height, mime} = await compressImage(image)
|
||||
logger.debug(`Uploading image #${i}`)
|
||||
const res = await uploadBlob(agent, path, mime)
|
||||
return {
|
||||
image: res.data.blob,
|
||||
alt: image.alt,
|
||||
aspectRatio: {width, height},
|
||||
}
|
||||
}),
|
||||
)
|
||||
return {
|
||||
$type: 'app.bsky.embed.images',
|
||||
images,
|
||||
}
|
||||
}
|
||||
if (opts.video) {
|
||||
const captions = await Promise.all(
|
||||
opts.video.captions
|
||||
.filter(caption => caption.lang !== '')
|
||||
.map(async caption => {
|
||||
const {data} = await agent.uploadBlob(caption.file, {
|
||||
encoding: 'text/vtt',
|
||||
})
|
||||
return {lang: caption.lang, file: data.blob}
|
||||
}),
|
||||
)
|
||||
return {
|
||||
$type: 'app.bsky.embed.video',
|
||||
video: opts.video.blobRef,
|
||||
alt: opts.video.altText || undefined,
|
||||
captions: captions.length === 0 ? undefined : captions,
|
||||
aspectRatio: opts.video.aspectRatio,
|
||||
}
|
||||
}
|
||||
if (opts.extLink) {
|
||||
if (opts.extLink.embed) {
|
||||
return undefined
|
||||
}
|
||||
let thumb
|
||||
if (opts.extLink.localThumb) {
|
||||
opts.onStateChange?.('Uploading link thumbnail...')
|
||||
const {path, mime} = opts.extLink.localThumb.source
|
||||
const res = await uploadBlob(agent, path, mime)
|
||||
thumb = res.data.blob
|
||||
}
|
||||
return {
|
||||
$type: 'app.bsky.embed.external',
|
||||
external: {
|
||||
uri: opts.extLink.uri,
|
||||
title: opts.extLink.meta?.title || '',
|
||||
description: opts.extLink.meta?.description || '',
|
||||
thumb,
|
||||
},
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -4,17 +4,17 @@ import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
|
||||
import {AppBskyFeedDefs, ModerationOpts} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {DISCOVER_FEED_URI} from '#/lib/constants'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {DISCOVER_FEED_URI} from 'lib/constants'
|
||||
import {
|
||||
useGetPopularFeedsQuery,
|
||||
usePopularFeedsSearch,
|
||||
useSavedFeeds,
|
||||
} from '#/state/queries/feed'
|
||||
import {List} from '#/view/com/util/List'
|
||||
} from 'state/queries/feed'
|
||||
import {SearchInput} from 'view/com/util/forms/SearchInput'
|
||||
import {List} from 'view/com/util/List'
|
||||
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {SearchInput} from '#/components/forms/SearchInput'
|
||||
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
|
||||
@@ -81,11 +81,12 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
|
||||
return (
|
||||
<ScreenTransition style={[a.flex_1]} direction={state.transitionDirection}>
|
||||
<View style={[a.border_b, t.atoms.border_contrast_medium]}>
|
||||
<View style={[a.py_sm, a.px_md, {height: 60}]}>
|
||||
<View style={[a.my_sm, a.px_md, {height: 40}]}>
|
||||
<SearchInput
|
||||
value={query}
|
||||
onChangeText={t => setQuery(t)}
|
||||
onClearText={() => setQuery('')}
|
||||
query={query}
|
||||
onChangeQuery={t => setQuery(t)}
|
||||
onPressCancelSearch={() => setQuery('')}
|
||||
onSubmitQuery={() => {}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -93,6 +94,7 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
|
||||
data={query ? searchedFeeds : suggestedFeeds}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
contentContainerStyle={{paddingTop: 6}}
|
||||
onEndReached={
|
||||
!query && !screenReaderEnabled ? () => fetchNextPage() : undefined
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
|
||||
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useA11y} from '#/state/a11y'
|
||||
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||
import {useActorSearchPaginated} from '#/state/queries/actor-search'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
|
||||
import {useActorSearchPaginated} from 'state/queries/actor-search'
|
||||
import {SearchInput} from 'view/com/util/forms/SearchInput'
|
||||
import {List} from 'view/com/util/List'
|
||||
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {SearchInput} from '#/components/forms/SearchInput'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
|
||||
import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard'
|
||||
@@ -65,11 +65,12 @@ export function StepProfiles({
|
||||
return (
|
||||
<ScreenTransition style={[a.flex_1]} direction={state.transitionDirection}>
|
||||
<View style={[a.border_b, t.atoms.border_contrast_medium]}>
|
||||
<View style={[a.py_sm, a.px_md, {height: 60}]}>
|
||||
<View style={[a.my_sm, a.px_md, {height: 40}]}>
|
||||
<SearchInput
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
onClearText={() => setQuery('')}
|
||||
query={query}
|
||||
onChangeQuery={setQuery}
|
||||
onPressCancelSearch={() => setQuery('')}
|
||||
onSubmitQuery={() => {}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
+4
-2
@@ -8,14 +8,16 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'InterVariable';
|
||||
src: url(/assets/fonts/inter/InterVariable.ttf) format('truetype');
|
||||
src: local('InterVariable'),
|
||||
url(/assets/fonts/inter/InterVariable.ttf) format('truetype');
|
||||
font-weight: 300 1000;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterVariableItalic';
|
||||
src: url(/assets/fonts/inter/InterVariable-Italic.ttf) format('truetype');
|
||||
src: local('InterVariableItalic'),
|
||||
url(/assets/fonts/inter/InterVariable-Italic.ttf) format('truetype');
|
||||
font-weight: 300 1000;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
|
||||
@@ -3,7 +3,6 @@ import React, {
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
@@ -67,7 +66,7 @@ import {logger} from '#/logger'
|
||||
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {emitPostCreated} from '#/state/events'
|
||||
import {ComposerImage, pasteImage} from '#/state/gallery'
|
||||
import {ComposerImage, createInitialImages, pasteImage} from '#/state/gallery'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useModals} from '#/state/modals'
|
||||
import {useRequireAltTextEnabled} from '#/state/preferences'
|
||||
@@ -120,7 +119,6 @@ import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {Text as NewText} from '#/components/Typography'
|
||||
import {composerReducer, createComposerState} from './state'
|
||||
|
||||
const MAX_IMAGES = 4
|
||||
|
||||
@@ -128,8 +126,6 @@ type CancelRef = {
|
||||
onPressCancel: () => void
|
||||
}
|
||||
|
||||
const NO_IMAGES: ComposerImage[] = []
|
||||
|
||||
type Props = ComposerOpts
|
||||
export const ComposePost = ({
|
||||
replyTo,
|
||||
@@ -217,17 +213,9 @@ export const ComposePost = ({
|
||||
)
|
||||
const [postgate, setPostgate] = useState(createPostgateRecord({post: ''}))
|
||||
|
||||
// TODO: Move more state here.
|
||||
const [composerState, dispatch] = useReducer(
|
||||
composerReducer,
|
||||
{initImageUris},
|
||||
createComposerState,
|
||||
const [images, setImages] = useState<ComposerImage[]>(() =>
|
||||
createInitialImages(initImageUris),
|
||||
)
|
||||
let images = NO_IMAGES
|
||||
if (composerState.embed.media?.type === 'images') {
|
||||
images = composerState.embed.media.images
|
||||
}
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
closeComposer()
|
||||
}, [closeComposer])
|
||||
@@ -313,12 +301,9 @@ export const ComposePost = ({
|
||||
|
||||
const onImageAdd = useCallback(
|
||||
(next: ComposerImage[]) => {
|
||||
dispatch({
|
||||
type: 'embed_add_images',
|
||||
images: next,
|
||||
})
|
||||
setImages(prev => prev.concat(next.slice(0, MAX_IMAGES - prev.length)))
|
||||
},
|
||||
[dispatch],
|
||||
[setImages],
|
||||
)
|
||||
|
||||
const onPhotoPasted = useCallback(
|
||||
@@ -389,7 +374,6 @@ export const ComposePost = ({
|
||||
try {
|
||||
postUri = (
|
||||
await apilib.post(agent, {
|
||||
composerState, // TODO: not used yet.
|
||||
rawText: richtext.text,
|
||||
replyTo: replyTo?.uri,
|
||||
images,
|
||||
@@ -491,7 +475,6 @@ export const ComposePost = ({
|
||||
_,
|
||||
agent,
|
||||
captions,
|
||||
composerState,
|
||||
extLink,
|
||||
images,
|
||||
graphemeLength,
|
||||
@@ -734,7 +717,7 @@ export const ComposePost = ({
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Gallery images={images} dispatch={dispatch} />
|
||||
<Gallery images={images} onChange={setImages} />
|
||||
{images.length === 0 && extLink && (
|
||||
<View style={a.relative}>
|
||||
<ExternalEmbed
|
||||
|
||||
@@ -21,7 +21,6 @@ import {ComposerImage, cropImage} from '#/state/gallery'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {useTheme} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {ComposerAction} from '../state'
|
||||
import {EditImageDialog} from './EditImageDialog'
|
||||
import {ImageAltTextDialog} from './ImageAltTextDialog'
|
||||
|
||||
@@ -29,7 +28,7 @@ const IMAGE_GAP = 8
|
||||
|
||||
interface GalleryProps {
|
||||
images: ComposerImage[]
|
||||
dispatch: (action: ComposerAction) => void
|
||||
onChange: (next: ComposerImage[]) => void
|
||||
}
|
||||
|
||||
export let Gallery = (props: GalleryProps): React.ReactNode => {
|
||||
@@ -57,7 +56,7 @@ interface GalleryInnerProps extends GalleryProps {
|
||||
containerInfo: Dimensions
|
||||
}
|
||||
|
||||
const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
||||
const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
||||
const {altTextControlStyle, imageControlsStyle, imageStyle} =
|
||||
@@ -97,7 +96,7 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
||||
return images.length !== 0 ? (
|
||||
<>
|
||||
<View testID="selectedPhotosView" style={styles.gallery}>
|
||||
{images.map(image => {
|
||||
{images.map((image, index) => {
|
||||
return (
|
||||
<GalleryItem
|
||||
key={image.source.id}
|
||||
@@ -106,10 +105,15 @@ const GalleryInner = ({images, containerInfo, dispatch}: GalleryInnerProps) => {
|
||||
imageControlsStyle={imageControlsStyle}
|
||||
imageStyle={imageStyle}
|
||||
onChange={next => {
|
||||
dispatch({type: 'embed_update_image', image: next})
|
||||
onChange(
|
||||
images.map(i => (i.source === image.source ? next : i)),
|
||||
)
|
||||
}}
|
||||
onRemove={() => {
|
||||
dispatch({type: 'embed_remove_image', image})
|
||||
const next = images.slice()
|
||||
next.splice(index, 1)
|
||||
|
||||
onChange(next)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import {ComposerImage, createInitialImages} from '#/state/gallery'
|
||||
import {ComposerOpts} from '#/state/shell/composer'
|
||||
|
||||
type PostRecord = {
|
||||
uri: string
|
||||
}
|
||||
|
||||
type ImagesMedia = {
|
||||
type: 'images'
|
||||
images: ComposerImage[]
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
type ComposerEmbed = {
|
||||
// TODO: Other record types.
|
||||
record: PostRecord | undefined
|
||||
// TODO: Other media types.
|
||||
media: ImagesMedia | undefined
|
||||
}
|
||||
|
||||
export type ComposerState = {
|
||||
// TODO: Other draft data.
|
||||
embed: ComposerEmbed
|
||||
}
|
||||
|
||||
export type ComposerAction =
|
||||
| {type: 'embed_add_images'; images: ComposerImage[]}
|
||||
| {type: 'embed_update_image'; image: ComposerImage}
|
||||
| {type: 'embed_remove_image'; image: ComposerImage}
|
||||
|
||||
const MAX_IMAGES = 4
|
||||
|
||||
export function composerReducer(
|
||||
state: ComposerState,
|
||||
action: ComposerAction,
|
||||
): ComposerState {
|
||||
switch (action.type) {
|
||||
case 'embed_add_images': {
|
||||
const prevMedia = state.embed.media
|
||||
let nextMedia = prevMedia
|
||||
if (!prevMedia) {
|
||||
nextMedia = {
|
||||
type: 'images',
|
||||
images: action.images.slice(0, MAX_IMAGES),
|
||||
labels: [],
|
||||
}
|
||||
} else if (prevMedia.type === 'images') {
|
||||
nextMedia = {
|
||||
...prevMedia,
|
||||
images: [...prevMedia.images, ...action.images].slice(0, MAX_IMAGES),
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
embed: {
|
||||
...state.embed,
|
||||
media: nextMedia,
|
||||
},
|
||||
}
|
||||
}
|
||||
case 'embed_update_image': {
|
||||
const prevMedia = state.embed.media
|
||||
if (prevMedia?.type === 'images') {
|
||||
const updatedImage = action.image
|
||||
const nextMedia = {
|
||||
...prevMedia,
|
||||
images: prevMedia.images.map(img => {
|
||||
if (img.source.id === updatedImage.source.id) {
|
||||
return updatedImage
|
||||
}
|
||||
return img
|
||||
}),
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
embed: {
|
||||
...state.embed,
|
||||
media: nextMedia,
|
||||
},
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
case 'embed_remove_image': {
|
||||
const prevMedia = state.embed.media
|
||||
if (prevMedia?.type === 'images') {
|
||||
const removedImage = action.image
|
||||
let nextMedia: ImagesMedia | undefined = {
|
||||
...prevMedia,
|
||||
images: prevMedia.images.filter(img => {
|
||||
return img.source.id !== removedImage.source.id
|
||||
}),
|
||||
}
|
||||
if (nextMedia.images.length === 0) {
|
||||
nextMedia = undefined
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
embed: {
|
||||
...state.embed,
|
||||
media: nextMedia,
|
||||
},
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export function createComposerState({
|
||||
initImageUris,
|
||||
}: {
|
||||
initImageUris: ComposerOpts['imageUris']
|
||||
}): ComposerState {
|
||||
let media: ImagesMedia | undefined
|
||||
if (initImageUris?.length) {
|
||||
media = {
|
||||
type: 'images',
|
||||
images: createInitialImages(initImageUris),
|
||||
labels: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
embed: {
|
||||
record: undefined,
|
||||
media,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {FABInner, FABProps} from './FABInner'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
|
||||
export const FAB = (_opts: FABProps) => {
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, {ComponentProps} from 'react'
|
||||
import {StyleSheet, TouchableWithoutFeedback} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {PressableScale} from '#/lib/custom-animations/PressableScale'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
@@ -12,6 +11,7 @@ import {clamp} from '#/lib/numbers'
|
||||
import {gradients} from '#/lib/styles'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useHapticsDisabled} from '#/state/preferences'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
|
||||
export interface FABProps
|
||||
extends ComponentProps<typeof TouchableWithoutFeedback> {
|
||||
@@ -25,6 +25,11 @@ export function FABInner({testID, icon, onPress, ...props}: FABProps) {
|
||||
const playHaptic = useHaptics()
|
||||
const isHapticsDisabled = useHapticsDisabled()
|
||||
const fabMinimalShellTransform = useMinimalShellFabTransform()
|
||||
const {
|
||||
state: pressed,
|
||||
onIn: onPressIn,
|
||||
onOut: onPressOut,
|
||||
} = useInteractionState()
|
||||
|
||||
const size = isTablet ? styles.sizeLarge : styles.sizeRegular
|
||||
|
||||
@@ -32,36 +37,43 @@ export function FABInner({testID, icon, onPress, ...props}: FABProps) {
|
||||
? {right: 50, bottom: 50}
|
||||
: {right: 24, bottom: clamp(insets.bottom, 15, 60) + 15}
|
||||
|
||||
const scale = useAnimatedStyle(() => ({
|
||||
transform: [{scale: withTiming(pressed ? 0.95 : 1)}],
|
||||
}))
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.outer,
|
||||
size,
|
||||
tabletSpacing,
|
||||
isMobile && fabMinimalShellTransform,
|
||||
]}>
|
||||
<PressableScale
|
||||
testID={testID}
|
||||
onPress={e => {
|
||||
playHaptic('Light')
|
||||
setTimeout(
|
||||
() => {
|
||||
onPress?.(e)
|
||||
},
|
||||
isHapticsDisabled ? 0 : 75,
|
||||
)
|
||||
}}
|
||||
targetScale={0.9}
|
||||
{...props}>
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.inner, size]}>
|
||||
{icon}
|
||||
</LinearGradient>
|
||||
</PressableScale>
|
||||
</Animated.View>
|
||||
<TouchableWithoutFeedback
|
||||
testID={testID}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
onPress={e => {
|
||||
playHaptic('Light')
|
||||
setTimeout(
|
||||
() => {
|
||||
onPress?.(e)
|
||||
},
|
||||
isHapticsDisabled ? 0 : 75,
|
||||
)
|
||||
}}
|
||||
{...props}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.outer,
|
||||
size,
|
||||
tabletSpacing,
|
||||
isMobile && fabMinimalShellTransform,
|
||||
]}>
|
||||
<Animated.View style={scale}>
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.inner, size]}>
|
||||
{icon}
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</TouchableWithoutFeedback>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {MagnifyingGlassIcon} from 'lib/icons'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
interface Props {
|
||||
query: string
|
||||
setIsInputFocused?: (v: boolean) => void
|
||||
onChangeQuery: (v: string) => void
|
||||
onPressCancelSearch: () => void
|
||||
onSubmitQuery: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export interface SearchInputRef {
|
||||
focus?: () => void
|
||||
}
|
||||
|
||||
export const SearchInput = React.forwardRef<SearchInputRef, Props>(
|
||||
function SearchInput(
|
||||
{
|
||||
query,
|
||||
setIsInputFocused,
|
||||
onChangeQuery,
|
||||
onPressCancelSearch,
|
||||
onSubmitQuery,
|
||||
style,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const textInput = React.useRef<TextInput>(null)
|
||||
|
||||
const onPressCancelSearchInner = React.useCallback(() => {
|
||||
onPressCancelSearch()
|
||||
textInput.current?.blur()
|
||||
}, [onPressCancelSearch, textInput])
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: () => textInput.current?.focus(),
|
||||
blur: () => textInput.current?.blur(),
|
||||
}))
|
||||
|
||||
return (
|
||||
<View style={[pal.viewLight, styles.container, style]}>
|
||||
<MagnifyingGlassIcon style={[pal.icon, styles.icon]} size={21} />
|
||||
<TextInput
|
||||
testID="searchTextInput"
|
||||
ref={textInput}
|
||||
placeholder={_(msg`Search`)}
|
||||
placeholderTextColor={pal.colors.textLight}
|
||||
selectTextOnFocus
|
||||
returnKeyType="search"
|
||||
value={query}
|
||||
style={[pal.text, styles.input]}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
onFocus={() => setIsInputFocused?.(true)}
|
||||
onBlur={() => setIsInputFocused?.(false)}
|
||||
onChangeText={onChangeQuery}
|
||||
onSubmitEditing={onSubmitQuery}
|
||||
accessibilityRole="search"
|
||||
accessibilityLabel={_(msg`Search`)}
|
||||
accessibilityHint=""
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
{query ? (
|
||||
<TouchableOpacity
|
||||
onPress={onPressCancelSearchInner}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Clear search query`)}
|
||||
accessibilityHint=""
|
||||
hitSlop={HITSLOP_10}>
|
||||
<FontAwesomeIcon
|
||||
icon="xmark"
|
||||
size={16}
|
||||
style={pal.textLight as FontAwesomeIconStyle}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : undefined}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 30,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
icon: {
|
||||
marginRight: 6,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 17,
|
||||
minWidth: 0, // overflow mitigation for firefox
|
||||
},
|
||||
cancelBtn: {
|
||||
paddingLeft: 10,
|
||||
},
|
||||
})
|
||||
+19
-20
@@ -6,12 +6,6 @@ import {useLingui} from '@lingui/react'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {ComposeIcon2} from '#/lib/icons'
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {s} from '#/lib/styles'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {
|
||||
SavedFeedItem,
|
||||
@@ -22,19 +16,25 @@ import {
|
||||
import {useSession} from '#/state/session'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import {TextLink} from '#/view/com/util/Link'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {FeedFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {ComposeIcon2} from 'lib/icons'
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {s} from 'lib/styles'
|
||||
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||
import {FAB} from 'view/com/util/fab/FAB'
|
||||
import {SearchInput} from 'view/com/util/forms/SearchInput'
|
||||
import {TextLink} from 'view/com/util/Link'
|
||||
import {List} from 'view/com/util/List'
|
||||
import {FeedFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
|
||||
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {SearchInput} from '#/components/forms/SearchInput'
|
||||
import {IconCircle} from '#/components/IconCircle'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||
import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline'
|
||||
@@ -481,12 +481,11 @@ export function FeedsScreen(_props: Props) {
|
||||
<FeedsAboutHeader />
|
||||
<View style={{paddingHorizontal: 12, paddingBottom: 4}}>
|
||||
<SearchInput
|
||||
value={query}
|
||||
onChangeText={onChangeQuery}
|
||||
onClearText={onPressCancelSearch}
|
||||
onSubmitEditing={onSubmitQuery}
|
||||
onFocus={() => onChangeSearchFocus(true)}
|
||||
onBlur={() => onChangeSearchFocus(false)}
|
||||
query={query}
|
||||
onChangeQuery={onChangeQuery}
|
||||
onPressCancelSearch={onPressCancelSearch}
|
||||
onSubmitQuery={onSubmitQuery}
|
||||
setIsInputFocused={onChangeSearchFocus}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -62,10 +62,12 @@ import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils'
|
||||
import {atoms as a, useBreakpoints, useTheme as useThemeNew, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {SearchInput} from '#/components/forms/SearchInput'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
|
||||
function Loader() {
|
||||
const pal = usePalette('default')
|
||||
@@ -622,7 +624,7 @@ export function SearchScreen(
|
||||
* Arbitrary sizing, so guess and check, used for sticky header alignment and
|
||||
* sizing.
|
||||
*/
|
||||
const headerHeight = 64 + (showFilters ? 40 : 0)
|
||||
const headerHeight = 56 + (showFilters ? 40 : 0)
|
||||
|
||||
useFocusEffect(
|
||||
useNonReactiveCallback(() => {
|
||||
@@ -836,18 +838,18 @@ export function SearchScreen(
|
||||
<CenteredView
|
||||
style={[
|
||||
a.p_md,
|
||||
a.pb_sm,
|
||||
a.pb_0,
|
||||
a.gap_sm,
|
||||
t.atoms.bg,
|
||||
web({
|
||||
height: headerHeight,
|
||||
height: headerHeight + a.mb_sm.marginBottom,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
}),
|
||||
]}
|
||||
sideBorders={gtMobile}>
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
<View style={[a.flex_row, a.gap_sm, a.mb_sm]}>
|
||||
{!gtMobile && (
|
||||
<Button
|
||||
testID="viewHeaderBackOrMenuBtn"
|
||||
@@ -862,16 +864,15 @@ export function SearchScreen(
|
||||
<ButtonIcon icon={Menu} size="lg" />
|
||||
</Button>
|
||||
)}
|
||||
<View style={[a.flex_1]}>
|
||||
<SearchInput
|
||||
ref={textInput}
|
||||
value={searchText}
|
||||
onFocus={onSearchInputFocus}
|
||||
onChangeText={onChangeText}
|
||||
onClearText={onPressClearQuery}
|
||||
onSubmitEditing={onSubmit}
|
||||
/>
|
||||
</View>
|
||||
<SearchInputBox
|
||||
textInput={textInput}
|
||||
searchText={searchText}
|
||||
showAutocomplete={showAutocomplete}
|
||||
onFocus={onSearchInputFocus}
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={onSubmit}
|
||||
onPressClearQuery={onPressClearQuery}
|
||||
/>
|
||||
{showFiltersButton && (
|
||||
<Button
|
||||
onPress={() => setShowFilters(!showFilters)}
|
||||
@@ -886,7 +887,7 @@ export function SearchScreen(
|
||||
fill={
|
||||
showFilters
|
||||
? t.palette.primary_500
|
||||
: t.atoms.text_contrast_medium.color
|
||||
: t.atoms.text_contrast_low.color
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
@@ -960,6 +961,78 @@ export function SearchScreen(
|
||||
)
|
||||
}
|
||||
|
||||
let SearchInputBox = ({
|
||||
textInput,
|
||||
searchText,
|
||||
showAutocomplete,
|
||||
onFocus,
|
||||
onChangeText,
|
||||
onSubmit,
|
||||
onPressClearQuery,
|
||||
}: {
|
||||
textInput: React.RefObject<TextInput>
|
||||
searchText: string
|
||||
showAutocomplete: boolean
|
||||
onFocus: () => void
|
||||
onChangeText: (text: string) => void
|
||||
onSubmit: () => void
|
||||
onPressClearQuery: () => void
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const t = useThemeNew()
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={MagnifyingGlass} />
|
||||
<TextField.Input
|
||||
inputRef={textInput}
|
||||
label={_(msg`Search`)}
|
||||
value={searchText}
|
||||
placeholder={_(msg`Search`)}
|
||||
returnKeyType="search"
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmit}
|
||||
onFocus={onFocus}
|
||||
keyboardAppearance={t.scheme}
|
||||
selectTextOnFocus={isNative}
|
||||
autoFocus={false}
|
||||
accessibilityRole="search"
|
||||
autoCorrect={false}
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
{showAutocomplete && searchText.length > 0 && (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.z_10,
|
||||
a.my_auto,
|
||||
a.inset_0,
|
||||
a.justify_center,
|
||||
a.pr_sm,
|
||||
{left: 'auto'},
|
||||
]}>
|
||||
<Button
|
||||
testID="searchTextInputClearBtn"
|
||||
onPress={onPressClearQuery}
|
||||
label={_(msg`Clear search query`)}
|
||||
hitSlop={HITSLOP_10}
|
||||
size="tiny"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary">
|
||||
<ButtonIcon icon={X} size="sm" />
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
SearchInputBox = React.memo(SearchInputBox)
|
||||
|
||||
let AutocompleteResults = ({
|
||||
isAutocompleteFetching,
|
||||
autocompleteData,
|
||||
|
||||
@@ -25,11 +25,11 @@ import {s} from '#/lib/styles'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||
import {precacheProfile} from '#/state/queries/profile'
|
||||
import {SearchInput} from '#/view/com/util/forms/SearchInput'
|
||||
import {Link} from '#/view/com/util/Link'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {SearchInput} from '#/components/forms/SearchInput'
|
||||
|
||||
let SearchLinkCard = ({
|
||||
label,
|
||||
@@ -184,10 +184,10 @@ export function DesktopSearch() {
|
||||
return (
|
||||
<View style={[styles.container, pal.view]}>
|
||||
<SearchInput
|
||||
value={query}
|
||||
onChangeText={onChangeText}
|
||||
onClearText={onPressCancelSearch}
|
||||
onSubmitEditing={onSubmit}
|
||||
query={query}
|
||||
onChangeQuery={onChangeText}
|
||||
onPressCancelSearch={onPressCancelSearch}
|
||||
onSubmitQuery={onSubmit}
|
||||
/>
|
||||
{query !== '' && isActive && moderationOpts && (
|
||||
<View style={[pal.view, pal.borderDark, styles.resultsContainer]}>
|
||||
|
||||
Reference in New Issue
Block a user