Merge branch 'main' into patch-3

This commit is contained in:
Minseo Lee 2024-02-27 14:39:41 +09:00 committed by GitHub
commit 8d394a3541
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 2060 additions and 125 deletions

View file

@ -133,8 +133,8 @@ function IsValidIcon({valid}: {valid: boolean}) {
const t = useTheme()
if (!valid) {
return <Check size="md" style={{color: t.palette.negative_500}} />
return <Times size="md" style={{color: t.palette.negative_500}} />
}
return <Times size="md" style={{color: t.palette.positive_700}} />
return <Check size="md" style={{color: t.palette.positive_700}} />
}

View file

@ -107,7 +107,7 @@ export const LoginForm = ({
const errMsg = e.toString()
setIsProcessing(false)
if (errMsg.includes('Authentication Required')) {
logger.info('Failed to login due to invalid credentials', {
logger.debug('Failed to login due to invalid credentials', {
error: errMsg,
})
setError(_(msg`Invalid username or password`))

View file

@ -190,12 +190,11 @@ export const TextInput = forwardRef(function TextInputImpl(
let i = 0
return Array.from(richtext.segments()).map(segment => {
const isTag = AppBskyRichtextFacet.isTag(segment.facet?.features?.[0])
return (
<Text
key={i++}
style={[
segment.facet && !isTag ? pal.link : pal.text,
segment.facet ? pal.link : pal.text,
styles.textInputFormatting,
]}>
{segment.text}

View file

@ -23,6 +23,7 @@ import {Portal} from '#/components/Portal'
import {Text} from '../../util/text/Text'
import {Trans} from '@lingui/macro'
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
import {TagDecorator} from './web/TagDecorator'
export interface TextInputRef {
focus: () => void
@ -67,6 +68,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
() => [
Document,
LinkDecorator,
TagDecorator,
Mention.configure({
HTMLAttributes: {
class: 'mention',

View file

@ -0,0 +1,83 @@
/**
* TipTap is a stateful rich-text editor, which is extremely useful
* when you _want_ it to be stateful formatting such as bold and italics.
*
* However we also use "stateless" behaviors, specifically for URLs
* where the text itself drives the formatting.
*
* This plugin uses a regex to detect URIs and then applies
* link decorations (a <span> with the "autolink") class. That avoids
* adding any stateful formatting to TipTap's document model.
*
* We then run the URI detection again when constructing the
* RichText object from TipTap's output and merge their features into
* the facet-set.
*/
import {Mark} from '@tiptap/core'
import {Plugin, PluginKey} from '@tiptap/pm/state'
import {Node as ProsemirrorNode} from '@tiptap/pm/model'
import {Decoration, DecorationSet} from '@tiptap/pm/view'
function getDecorations(doc: ProsemirrorNode) {
const decorations: Decoration[] = []
doc.descendants((node, pos) => {
if (node.isText && node.text) {
const regex = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g
const textContent = node.textContent
let match
while ((match = regex.exec(textContent))) {
const [matchedString, tag] = match
if (tag.length > 66) continue
const [trailingPunc = ''] = tag.match(/\p{P}+$/u) || []
const from = match.index + matchedString.indexOf(tag)
const to = from + (tag.length - trailingPunc.length)
decorations.push(
Decoration.inline(pos + from, pos + to, {
class: 'autolink',
}),
)
}
}
})
return DecorationSet.create(doc, decorations)
}
const tagDecoratorPlugin: Plugin = new Plugin({
key: new PluginKey('link-decorator'),
state: {
init: (_, {doc}) => getDecorations(doc),
apply: (transaction, decorationSet) => {
if (transaction.docChanged) {
return getDecorations(transaction.doc)
}
return decorationSet.map(transaction.mapping, transaction.doc)
},
},
props: {
decorations(state) {
return tagDecoratorPlugin.getState(state)
},
},
})
export const TagDecorator = Mark.create({
name: 'tag-decorator',
priority: 1000,
keepOnSplit: false,
inclusive() {
return true
},
addProseMirrorPlugins() {
return [tagDecoratorPlugin]
},
})

View file

@ -200,21 +200,12 @@ export function FeedPage({
function useHeaderOffset() {
const {isDesktop, isTablet} = useWebMediaQueries()
const {fontScale} = useWindowDimensions()
const {hasSession} = useSession()
if (isDesktop || isTablet) {
return 0
}
if (hasSession) {
const navBarPad = 16
const navBarText = 21 * fontScale
const tabBarPad = 20 + 3 // nav bar padding + border
const tabBarText = 16 * fontScale
const magic = 7 * fontScale
return navBarPad + navBarText + tabBarPad + tabBarText + magic
} else {
const navBarPad = 16
const navBarText = 21 * fontScale
const magic = 4 * fontScale
return navBarPad + navBarText + magic
}
const navBarHeight = 42
const tabBarPad = 10 + 10 + 3 // padding + border
const normalLineHeight = 1.2
const tabBarText = 16 * normalLineHeight * fontScale
return navBarHeight + tabBarPad + tabBarText
}

View file

@ -4,7 +4,6 @@ import {Text} from '../util/text/Text'
import {PressableWithHover} from '../util/PressableWithHover'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {isWeb} from 'platform/detection'
import {DraggableScrollView} from './DraggableScrollView'
export interface TabBarProps {
@ -32,13 +31,15 @@ export function TabBar({
[indicatorColor, pal],
)
const {isDesktop, isTablet} = useWebMediaQueries()
const styles = isDesktop || isTablet ? desktopStyles : mobileStyles
// scrolls to the selected item when the page changes
useEffect(() => {
scrollElRef.current?.scrollTo({
x: itemXs[selectedPage] || 0,
x:
(itemXs[selectedPage] || 0) - styles.contentContainer.paddingHorizontal,
})
}, [scrollElRef, itemXs, selectedPage])
}, [scrollElRef, itemXs, selectedPage, styles])
const onPressItem = useCallback(
(index: number) => {
@ -63,8 +64,6 @@ export function TabBar({
[],
)
const styles = isDesktop || isTablet ? desktopStyles : mobileStyles
return (
<View testID={testID} style={[pal.view, styles.outer]}>
<DraggableScrollView
@ -80,15 +79,17 @@ export function TabBar({
testID={`${testID}-selector-${i}`}
key={`${item}-${i}`}
onLayout={e => onItemLayout(e, i)}
style={[styles.item, selected && indicatorStyle]}
style={styles.item}
hoverStyle={pal.viewLight}
onPress={() => onPressItem(i)}>
<Text
type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'}
testID={testID ? `${testID}-${item}` : undefined}
style={selected ? pal.text : pal.textLight}>
{item}
</Text>
<View style={[styles.itemInner, selected && indicatorStyle]}>
<Text
type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'}
testID={testID ? `${testID}-${item}` : undefined}
style={selected ? pal.text : pal.textLight}>
{item}
</Text>
</View>
</PressableWithHover>
)
})}
@ -103,18 +104,18 @@ const desktopStyles = StyleSheet.create({
width: 598,
},
contentContainer: {
columnGap: 8,
marginLeft: 14,
paddingRight: 14,
paddingHorizontal: 0,
backgroundColor: 'transparent',
},
item: {
paddingTop: 14,
paddingHorizontal: 14,
justifyContent: 'center',
},
itemInner: {
paddingBottom: 12,
paddingHorizontal: 10,
borderBottomWidth: 3,
borderBottomColor: 'transparent',
justifyContent: 'center',
},
})
@ -123,17 +124,17 @@ const mobileStyles = StyleSheet.create({
flexDirection: 'row',
},
contentContainer: {
columnGap: isWeb ? 0 : 20,
marginLeft: isWeb ? 0 : 18,
paddingRight: isWeb ? 0 : 36,
backgroundColor: 'transparent',
paddingHorizontal: 8,
},
item: {
paddingTop: 10,
paddingBottom: 10,
paddingHorizontal: isWeb ? 8 : 0,
borderBottomWidth: 3,
borderBottomColor: 'transparent',
paddingHorizontal: 10,
justifyContent: 'center',
},
itemInner: {
paddingBottom: 10,
borderBottomWidth: 3,
borderBottomColor: 'transparent',
},
})

View file

@ -437,6 +437,7 @@ function PostThreadLoaded({
// @ts-ignore our .web version only -prf
desktopFixedHeight
removeClippedSubviews={isAndroid ? false : undefined}
windowSize={11}
/>
)
}

View file

@ -327,9 +327,11 @@ let PostThreadItemLoaded = ({
styles.postTextLargeContainer,
]}>
<RichText
enableTags
selectable
value={richText}
style={[a.flex_1, a.text_xl]}
selectable
authorHandle={post.author.handle}
/>
</View>
) : undefined}
@ -521,9 +523,11 @@ let PostThreadItemLoaded = ({
{richText?.text ? (
<View style={styles.postTextContainer}>
<RichText
enableTags
value={richText}
style={[a.flex_1, a.text_md]}
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
authorHandle={post.author.handle}
/>
</View>
) : undefined}

View file

@ -184,10 +184,12 @@ function PostInner({
{richText.text ? (
<View style={styles.postTextContainer}>
<RichText
enableTags
testID="postText"
value={richText}
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
style={[a.flex_1, a.text_md]}
authorHandle={post.author.handle}
/>
</View>
) : undefined}

View file

@ -347,10 +347,12 @@ let PostContent = ({
{richText.text ? (
<View style={styles.postTextContainer}>
<RichText
enableTags
testID="postText"
value={richText}
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
style={[a.flex_1, a.text_md]}
authorHandle={postAuthor.handle}
/>
</View>
) : undefined}

View file

@ -172,7 +172,7 @@ function ListImpl<ItemT>(
<View
ref={containerRef}
style={[
styles.contentContainer,
!isMobile && styles.sideBorders,
contentContainerStyle,
desktopFixedHeight ? styles.minHeightViewport : null,
pal.border,
@ -304,7 +304,7 @@ export const List = memo(React.forwardRef(ListImpl)) as <ItemT>(
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
const styles = StyleSheet.create({
contentContainer: {
sideBorders: {
borderLeftWidth: 1,
borderRightWidth: 1,
},

View file

@ -21,6 +21,7 @@ export const DropdownMenuItem = (props: ItemProps & {testID?: string}) => {
return (
<DropdownMenu.Item
className="nativeDropdown-item"
{...props}
style={StyleSheet.flatten([
styles.item,
@ -232,6 +233,10 @@ const styles = StyleSheet.create({
paddingLeft: 12,
paddingRight: 12,
borderRadius: 8,
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
outline: 0,
border: 0,
},
itemTitle: {
fontSize: 16,

View file

@ -34,6 +34,7 @@ import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session'
import {isWeb} from '#/platform/detection'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
let PostDropdownBtn = ({
testID,
@ -67,6 +68,7 @@ let PostDropdownBtn = ({
const {hidePost} = useHiddenPostsApi()
const openLink = useOpenLink()
const navigation = useNavigation()
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
const rootUri = record.reply?.root?.uri || postUri
const isThreadMuted = mutedThreads.includes(rootUri)
@ -210,6 +212,20 @@ let PostDropdownBtn = ({
web: 'comment-slash',
},
},
hasSession && {
label: _(msg`Mute words & tags`),
onPress() {
mutedWordsDialogControl.open()
},
testID: 'postDropdownMuteWordsBtn',
icon: {
ios: {
name: 'speaker.slash',
},
android: 'ic_lock_silent_mode',
web: 'filter',
},
},
hasSession &&
!isAuthor &&
!isPostHidden && {

View file

@ -128,10 +128,12 @@ export function QuoteEmbed({
) : null}
{richText ? (
<RichText
enableTags
value={richText}
style={[a.text_md]}
numberOfLines={20}
disableLinks
authorHandle={quote.author.handle}
/>
) : null}
{embed && <PostEmbeds embed={embed} moderation={{}} />}

View file

@ -7,6 +7,9 @@ import {lh} from 'lib/styles'
import {toShortUrl} from 'lib/strings/url-helpers'
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {makeTagLink} from 'lib/routes/links'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {isNative} from '#/platform/detection'
const WORD_WRAP = {wordWrap: 1}
@ -82,6 +85,7 @@ export function RichText({
for (const segment of richText.segments()) {
const link = segment.link
const mention = segment.mention
const tag = segment.tag
if (
!noLinks &&
mention &&
@ -115,6 +119,21 @@ export function RichText({
/>,
)
}
} else if (
!noLinks &&
tag &&
AppBskyRichtextFacet.validateTag(tag).success
) {
els.push(
<RichTextTag
key={key}
text={segment.text}
type={type}
style={style}
lineHeightStyle={lineHeightStyle}
selectable={selectable}
/>,
)
} else {
els.push(segment.text)
}
@ -133,3 +152,50 @@ export function RichText({
</Text>
)
}
function RichTextTag({
text: tag,
type,
style,
lineHeightStyle,
selectable,
}: {
text: string
type?: TypographyVariant
style?: StyleProp<TextStyle>
lineHeightStyle?: TextStyle
selectable?: boolean
}) {
const pal = usePalette('default')
const control = useTagMenuControl()
const open = React.useCallback(() => {
control.open()
}, [control])
return (
<React.Fragment>
<TagMenu control={control} tag={tag}>
{isNative ? (
<TextLink
type={type}
text={tag}
// segment.text has the leading "#" while tag.tag does not
href={makeTagLink(tag)}
style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
dataSet={WORD_WRAP}
selectable={selectable}
onPress={open}
/>
) : (
<Text
selectable={selectable}
type={type}
style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}>
{tag}
</Text>
)}
</TagMenu>
</React.Fragment>
)
}

View file

@ -103,6 +103,7 @@ import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash'
import {faX} from '@fortawesome/free-solid-svg-icons/faX'
import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'
import {faFilter} from '@fortawesome/free-solid-svg-icons/faFilter'
library.add(
faAddressCard,
@ -208,4 +209,5 @@ library.add(
faX,
faXmark,
faChevronDown,
faFilter,
)

View file

@ -31,6 +31,7 @@ import {
useProfileUpdateMutation,
} from '#/state/queries/profile'
import {ScrollView} from '../com/util/Views'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>
export function ModerationScreen({}: Props) {
@ -40,6 +41,7 @@ export function ModerationScreen({}: Props) {
const {screen, track} = useAnalytics()
const {isTabletOrDesktop} = useWebMediaQueries()
const {openModal} = useModalControls()
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
useFocusEffect(
React.useCallback(() => {
@ -71,7 +73,7 @@ export function ModerationScreen({}: Props) {
accessibilityRole="tab"
accessibilityLabel={_(msg`Content filtering`)}
accessibilityHint={_(
msg`Opens modal for content filtering preferences`,
msg`Opens modal for content filtering settings`,
)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
@ -83,6 +85,23 @@ export function ModerationScreen({}: Props) {
<Trans>Content filtering</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="mutedWordsBtn"
style={[styles.linkCard, pal.view]}
onPress={() => mutedWordsDialogControl.open()}
accessibilityRole="tab"
accessibilityLabel={_(msg`Muted words & tags`)}
accessibilityHint={_(msg`Open modal for muted words settings`)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="filter"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
<Text type="lg" style={pal.text}>
<Trans>Muted words & tags</Trans>
</Text>
</TouchableOpacity>
<Link
testID="moderationlistsBtn"
style={[styles.linkCard, pal.view]}

View file

@ -16,7 +16,7 @@ import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useFocusEffect} from '@react-navigation/native'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {logger} from '#/logger'
import {
@ -53,6 +53,7 @@ import {listenSoftReset} from '#/state/events'
import {s} from '#/lib/styles'
import AsyncStorage from '@react-native-async-storage/async-storage'
import {augmentSearchQuery} from '#/lib/strings/helpers'
import {NavigationProp} from '#/lib/routes/types'
function Loader() {
const pal = usePalette('default')
@ -448,6 +449,7 @@ export function SearchScreenInner({
export function SearchScreen(
props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
) {
const navigation = useNavigation<NavigationProp>()
const theme = useTheme()
const textInput = React.useRef<TextInput>(null)
const {_} = useLingui()
@ -472,6 +474,27 @@ export function SearchScreen(
React.useState(false)
const [searchHistory, setSearchHistory] = React.useState<string[]>([])
/**
* The Search screen's `q` param
*/
const queryParam = props.route?.params?.q
/**
* If `true`, this means we received new instructions from the router. This
* is handled in a effect, and used to update the value of `query` locally
* within this screen.
*/
const routeParamsMismatch = queryParam && queryParam !== query
React.useEffect(() => {
if (queryParam && routeParamsMismatch) {
// reset immediately and let local state take over
navigation.setParams({q: ''})
// update query for next search
setQuery(queryParam)
}
}, [queryParam, routeParamsMismatch, navigation])
React.useEffect(() => {
const loadSearchHistory = async () => {
try {
@ -774,6 +797,8 @@ export function SearchScreen(
)}
</View>
</CenteredView>
) : routeParamsMismatch ? (
<ActivityIndicator />
) : (
<SearchScreenInner query={query} />
)}

View file

@ -29,6 +29,7 @@ import {useSession} from '#/state/session'
import {useCloseAnyActiveElement} from '#/state/util'
import * as notifications from 'lib/notifications/notifications'
import {Outlet as PortalOutlet} from '#/components/Portal'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
function ShellInner() {
const isDrawerOpen = useIsDrawerOpen()
@ -94,6 +95,7 @@ function ShellInner() {
</View>
<Composer winHeight={winDim.height} />
<ModalsContainer />
<MutedWordsDialog />
<PortalOutlet />
<Lightbox />
</>

View file

@ -16,6 +16,7 @@ import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
import {useCloseAllActiveElements} from '#/state/util'
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import {Outlet as PortalOutlet} from '#/components/Portal'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
function ShellInner() {
const isDrawerOpen = useIsDrawerOpen()
@ -40,6 +41,7 @@ function ShellInner() {
</ErrorBoundary>
<Composer winHeight={0} />
<ModalsContainer />
<MutedWordsDialog />
<PortalOutlet />
<Lightbox />
{!isDesktop && isDrawerOpen && (