Compare commits

..

1 Commits

Author SHA1 Message Date
Eric Bailey 90dc3edc5b Use Inter variable font 2024-09-30 12:40:16 -05:00
24 changed files with 518 additions and 650 deletions
+35
View File
@@ -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. -->
-67
View File
@@ -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
+24
View File
@@ -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
-3
View File
@@ -15,6 +15,3 @@ android
ios
src/locale/locales
lib/react-compiler-runtime
bskyweb/static
coverage
web-build
-1
View File
@@ -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
-14
View File
@@ -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'),
})
}
-75
View File
@@ -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>
)
},
)
+1 -1
View File
@@ -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
View File
@@ -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
}
+10 -8
View File
@@ -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
View File
@@ -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;
+6 -23
View File
@@ -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
+10 -6
View File
@@ -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)
}}
/>
)
-131
View File
@@ -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 -2
View File
@@ -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()
+43 -31
View File
@@ -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>
)
}
+124
View File
@@ -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
View File
@@ -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>
</>
+89 -16
View File
@@ -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,
+5 -5
View File
@@ -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]}>