Merge branch 'bluesky-social:main' into zh

zio/stable
Kuwa Lee 2024-06-19 02:47:38 +08:00 committed by GitHub
commit a6d49062e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 374 additions and 159 deletions

View File

@ -31,6 +31,7 @@ module.exports = {
}, },
}, },
], ],
'bsky-internal/use-exact-imports': 'error',
'bsky-internal/use-typed-gates': 'error', 'bsky-internal/use-typed-gates': 'error',
'simple-import-sort/imports': [ 'simple-import-sort/imports': [
'warn', 'warn',

View File

@ -6,7 +6,6 @@ import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
import {enforceLen} from '../../src/lib/strings/helpers' import {enforceLen} from '../../src/lib/strings/helpers'
import {detectLinkables} from '../../src/lib/strings/rich-text-detection' import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
import {shortenLinks} from '../../src/lib/strings/rich-text-manip' import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
import {ago} from '../../src/lib/strings/time'
import { import {
makeRecordUri, makeRecordUri,
toNiceDomain, toNiceDomain,
@ -142,79 +141,6 @@ describe('makeRecordUri', () => {
}) })
}) })
// FIXME: Reenable after fixing non-deterministic test.
describe.skip('ago', () => {
const oneYearDate = new Date(
new Date().setMonth(new Date().getMonth() - 11),
).setDate(new Date().getDate() - 28)
const inputs = [
1671461038,
'04 Dec 1995 00:12:00 GMT',
new Date(),
new Date().setSeconds(new Date().getSeconds() - 10),
new Date().setMinutes(new Date().getMinutes() - 10),
new Date().setHours(new Date().getHours() - 1),
new Date().setDate(new Date().getDate() - 1),
new Date().setDate(new Date().getDate() - 20),
new Date().setDate(new Date().getDate() - 25),
new Date().setDate(new Date().getDate() - 28),
new Date().setDate(new Date().getDate() - 29),
new Date().setDate(new Date().getDate() - 30),
new Date().setMonth(new Date().getMonth() - 1),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 20,
),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 25,
),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 28,
),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 29,
),
new Date().setMonth(new Date().getMonth() - 11),
new Date(new Date().setMonth(new Date().getMonth() - 11)).setDate(
new Date().getDate() - 20,
),
new Date(new Date().setMonth(new Date().getMonth() - 11)).setDate(
new Date().getDate() - 25,
),
oneYearDate,
]
const outputs = [
new Date(1671461038).toLocaleDateString(),
new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString(),
'now',
'10s',
'10m',
'1h',
'1d',
'20d',
'25d',
'28d',
'29d',
'1mo',
'1mo',
'1mo',
'1mo',
'2mo',
'2mo',
'11mo',
'11mo',
'11mo',
new Date(oneYearDate).toLocaleDateString(),
]
it('correctly calculates how much time passed, in a string', () => {
for (let i = 0; i < inputs.length; i++) {
const result = ago(inputs[i])
expect(result).toEqual(outputs[i])
}
})
})
describe('makeValidHandle', () => { describe('makeValidHandle', () => {
const inputs = [ const inputs = [
'test-handle-123', 'test-handle-123',

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#FFC404" fill-rule="evenodd" d="M11.183 8.561c0 .544.348.984.892.984.545 0 .893-.44.893-.985V6.985c0-.544-.348-.985-.893-.985-.543 0-.892.44-.892.985v1.576Zm5.94 7.481c0 .539-.438.942-.976.942H8.004c-.538 0-.975-.411-.975-.95 0-2.782 2.264-5.021 5.046-5.021 2.783 0 5.047 2.247 5.047 5.03Zm-.43-4.584a.983.983 0 0 1 0-1.393l1.114-1.114a.985.985 0 0 1 1.393 1.393l-1.114 1.114a.985.985 0 0 1-1.393 0Zm2.897 3.741h1.575c.544 0 .985.349.985.892 0 .544-.44.892-.985.892h-1.67a.872.872 0 0 1-.89-.887c0-.543.44-.897.985-.897Zm-14.045.893c0-.544-.44-.892-.985-.892H2.985c-.544 0-.985.349-.985.892 0 .544.44.892.985.892H4.56c.545 0 .985-.349.985-.892Zm1.913-6.027a.985.985 0 0 1-1.393 1.393L4.95 10.344A.985.985 0 0 1 6.344 8.95l1.114 1.114Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 848 B

View File

@ -3,6 +3,7 @@
module.exports = { module.exports = {
rules: { rules: {
'avoid-unwrapped-text': require('./avoid-unwrapped-text'), 'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
'use-exact-imports': require('./use-exact-imports'),
'use-typed-gates': require('./use-typed-gates'), 'use-typed-gates': require('./use-typed-gates'),
}, },
} }

View File

@ -0,0 +1,22 @@
/* eslint-disable bsky-internal/use-exact-imports */
const BANNED_IMPORTS = [
'@fortawesome/free-regular-svg-icons',
'@fortawesome/free-solid-svg-icons',
]
exports.create = function create(context) {
return {
Literal(node) {
if (typeof node.value !== 'string') {
return
}
if (BANNED_IMPORTS.includes(node.value)) {
context.report({
node,
message:
'Import the specific thing you want instead of the entire package',
})
}
},
}
}

View File

@ -271,6 +271,7 @@
"resolutions": { "resolutions": {
"@types/react": "^18", "@types/react": "^18",
"**/zeed-dom": "0.10.9", "**/zeed-dom": "0.10.9",
"**/zod": "3.23.8",
"**/expo-constants": "16.0.1", "**/expo-constants": "16.0.1",
"**/expo-device": "6.0.2", "**/expo-device": "6.0.2",
"@react-native/babel-preset": "0.74.1" "@react-native/babel-preset": "0.74.1"

View File

@ -267,6 +267,9 @@ export const atoms = {
font_bold: { font_bold: {
fontWeight: tokens.fontWeight.bold, fontWeight: tokens.fontWeight.bold,
}, },
font_heavy: {
fontWeight: tokens.fontWeight.heavy,
},
italic: { italic: {
fontStyle: 'italic', fontStyle: 'italic',
}, },

View File

@ -118,6 +118,7 @@ export const fontWeight = {
normal: '400', normal: '400',
semibold: '500', semibold: '500',
bold: '600', bold: '600',
heavy: '700',
} as const } as const
export const gradients = { export const gradients = {

View File

@ -0,0 +1,81 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {differenceInSeconds} from 'date-fns'
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {HITSLOP_10} from 'lib/constants'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {atoms as a} from '#/alf'
import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useDialogControl} from '#/components/Dialog'
import {Newskie} from '#/components/icons/Newskie'
import {Text} from '#/components/Typography'
export function NewskieDialog({
profile,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const control = useDialogControl()
const profileName = React.useMemo(() => {
const name = profile.displayName || profile.handle
if (!moderationOpts) return name
const moderation = moderateProfile(profile, moderationOpts)
return sanitizeDisplayName(name, moderation.ui('displayName'))
}, [moderationOpts, profile])
const timeAgo = useGetTimeAgo()
const createdAt = profile.createdAt as string | undefined
const daysOld = React.useMemo(() => {
if (!createdAt) return Infinity
return differenceInSeconds(new Date(), new Date(createdAt)) / 86400
}, [createdAt])
if (!createdAt || daysOld > 7) return null
return (
<View style={[a.pr_2xs]}>
<Button
label={_(
msg`This user is new here. Press for more info about when they joined.`,
)}
hitSlop={HITSLOP_10}
onPress={control.open}>
{({hovered, pressed}) => (
<Newskie
size="lg"
fill="#FFC404"
style={{
opacity: hovered || pressed ? 0.5 : 1,
}}
/>
)}
</Button>
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`New user info dialog`)}
style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}>
<View style={[a.gap_sm]}>
<Text style={[a.font_bold, a.text_xl]}>
<Trans>Say hello!</Trans>
</Text>
<Text style={[a.text_md]}>
<Trans>
{profileName} joined Bluesky{' '}
{timeAgo(createdAt, {format: 'long'})} ago
</Trans>
</Text>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
</View>
)
}

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {Keyboard, View} from 'react-native'
import DatePicker from 'react-native-date-picker' import DatePicker from 'react-native-date-picker'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@ -49,7 +49,10 @@ export function DateField({
<DateFieldButton <DateFieldButton
label={label} label={label}
value={value} value={value}
onPress={control.open} onPress={() => {
Keyboard.dismiss()
control.open()
}}
isInvalid={isInvalid} isInvalid={isInvalid}
accessibilityHint={accessibilityHint} accessibilityHint={accessibilityHint}
/> />

View File

@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Newskie = createSinglePathSVG({
path: 'M11.183 8.561c0 .544.348.984.892.984.545 0 .893-.44.893-.985V6.985c0-.544-.348-.985-.893-.985-.543 0-.892.44-.892.985v1.576Zm5.94 7.481c0 .539-.438.942-.976.942H8.004c-.538 0-.975-.411-.975-.95 0-2.782 2.264-5.021 5.046-5.021 2.783 0 5.047 2.247 5.047 5.03Zm-.43-4.584a.983.983 0 0 1 0-1.393l1.114-1.114a.985.985 0 0 1 1.393 1.393l-1.114 1.114a.985.985 0 0 1-1.393 0Zm2.897 3.741h1.575c.544 0 .985.349.985.892 0 .544-.44.892-.985.892h-1.67a.872.872 0 0 1-.89-.887c0-.543.44-.897.985-.897Zm-14.045.893c0-.544-.44-.892-.985-.892H2.985c-.544 0-.985.349-.985.892 0 .544.44.892.985.892H4.56c.545 0 .985-.349.985-.892Zm1.913-6.027a.985.985 0 0 1-1.393 1.393L4.95 10.344A.985.985 0 0 1 6.344 8.95l1.114 1.114Z',
})

View File

@ -0,0 +1,102 @@
import {describe, expect, it} from '@jest/globals'
import {MessageDescriptor} from '@lingui/core'
import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns'
import {dateDiff} from '../useTimeAgo'
const lingui: any = (obj: MessageDescriptor) => obj.message
const base = new Date('2024-06-17T00:00:00Z')
describe('dateDiff', () => {
it(`works with numbers`, () => {
expect(dateDiff(subDays(base, 3), Number(base), {lingui})).toEqual('3d')
})
it(`works with strings`, () => {
expect(dateDiff(subDays(base, 3), base.toString(), {lingui})).toEqual('3d')
})
it(`works with dates`, () => {
expect(dateDiff(subDays(base, 3), base, {lingui})).toEqual('3d')
})
it(`equal values return now`, () => {
expect(dateDiff(base, base, {lingui})).toEqual('now')
})
it(`future dates return now`, () => {
expect(dateDiff(addDays(base, 3), base, {lingui})).toEqual('now')
})
it(`values < 5 seconds ago return now`, () => {
const then = subSeconds(base, 4)
expect(dateDiff(then, base, {lingui})).toEqual('now')
})
it(`values >= 5 seconds ago return seconds`, () => {
const then = subSeconds(base, 5)
expect(dateDiff(then, base, {lingui})).toEqual('5s')
})
it(`values < 1 min return seconds`, () => {
const then = subSeconds(base, 59)
expect(dateDiff(then, base, {lingui})).toEqual('59s')
})
it(`values >= 1 min return minutes`, () => {
const then = subSeconds(base, 60)
expect(dateDiff(then, base, {lingui})).toEqual('1m')
})
it(`minutes round down`, () => {
const then = subSeconds(base, 119)
expect(dateDiff(then, base, {lingui})).toEqual('1m')
})
it(`values < 1 hour return minutes`, () => {
const then = subMinutes(base, 59)
expect(dateDiff(then, base, {lingui})).toEqual('59m')
})
it(`values >= 1 hour return hours`, () => {
const then = subMinutes(base, 60)
expect(dateDiff(then, base, {lingui})).toEqual('1h')
})
it(`hours round down`, () => {
const then = subMinutes(base, 119)
expect(dateDiff(then, base, {lingui})).toEqual('1h')
})
it(`values < 1 day return hours`, () => {
const then = subHours(base, 23)
expect(dateDiff(then, base, {lingui})).toEqual('23h')
})
it(`values >= 1 day return days`, () => {
const then = subHours(base, 24)
expect(dateDiff(then, base, {lingui})).toEqual('1d')
})
it(`days round down`, () => {
const then = subHours(base, 47)
expect(dateDiff(then, base, {lingui})).toEqual('1d')
})
it(`values < 30 days return days`, () => {
const then = subDays(base, 29)
expect(dateDiff(then, base, {lingui})).toEqual('29d')
})
it(`values >= 30 days return months`, () => {
const then = subDays(base, 30)
expect(dateDiff(then, base, {lingui})).toEqual('1mo')
})
it(`months round down`, () => {
const then = subDays(base, 59)
expect(dateDiff(then, base, {lingui})).toEqual('1mo')
})
it(`values are rounded by increments of 30`, () => {
const then = subDays(base, 61)
expect(dateDiff(then, base, {lingui})).toEqual('2mo')
})
it(`values < 360 days return months`, () => {
const then = subDays(base, 359)
expect(dateDiff(then, base, {lingui})).toEqual('11mo')
})
it(`values >= 360 days return the earlier value`, () => {
const then = subDays(base, 360)
expect(dateDiff(then, base, {lingui})).toEqual(then.toLocaleDateString())
})
})

View File

@ -0,0 +1,95 @@
import {useCallback, useMemo} from 'react'
import {msg, plural} from '@lingui/macro'
import {I18nContext, useLingui} from '@lingui/react'
import {differenceInSeconds} from 'date-fns'
export type TimeAgoOptions = {
lingui: I18nContext['_']
format?: 'long' | 'short'
}
export function useGetTimeAgo() {
const {_} = useLingui()
return useCallback(
(
date: number | string | Date,
options?: Omit<TimeAgoOptions, 'lingui'>,
) => {
return dateDiff(date, Date.now(), {lingui: _, format: options?.format})
},
[_],
)
}
export function useTimeAgo(
date: number | string | Date,
options?: Omit<TimeAgoOptions, 'lingui'>,
): string {
const timeAgo = useGetTimeAgo()
return useMemo(() => {
return timeAgo(date, {...options})
}, [date, options, timeAgo])
}
const NOW = 5
const MINUTE = 60
const HOUR = MINUTE * 60
const DAY = HOUR * 24
const MONTH_30 = DAY * 30
/**
* Returns the difference between `earlier` and `later` dates, formatted as a
* natural language string.
*
* - All month are considered exactly 30 days.
* - Dates assume `earlier` <= `later`, and will otherwise return 'now'.
* - Differences >= 360 days are returned as the "M/D/YYYY" string
* - All values round down
*/
export function dateDiff(
earlier: number | string | Date,
later: number | string | Date,
options: TimeAgoOptions,
): string {
const _ = options.lingui
const format = options?.format || 'short'
const long = format === 'long'
const diffSeconds = differenceInSeconds(new Date(later), new Date(earlier))
if (diffSeconds < NOW) {
return _(msg`now`)
} else if (diffSeconds < MINUTE) {
return `${diffSeconds}${
long ? ` ${plural(diffSeconds, {one: 'second', other: 'seconds'})}` : 's'
}`
} else if (diffSeconds < HOUR) {
const diff = Math.floor(diffSeconds / MINUTE)
return `${diff}${
long ? ` ${plural(diff, {one: 'minute', other: 'minutes'})}` : 'm'
}`
} else if (diffSeconds < DAY) {
const diff = Math.floor(diffSeconds / HOUR)
return `${diff}${
long ? ` ${plural(diff, {one: 'hour', other: 'hours'})}` : 'h'
}`
} else if (diffSeconds < MONTH_30) {
const diff = Math.floor(diffSeconds / DAY)
return `${diff}${
long ? ` ${plural(diff, {one: 'day', other: 'days'})}` : 'd'
}`
} else {
const diff = Math.floor(diffSeconds / MONTH_30)
if (diff < 12) {
return `${diff}${
long ? ` ${plural(diff, {one: 'month', other: 'months'})}` : 'mo'
}`
} else {
const str = new Date(earlier).toLocaleDateString()
if (long) {
return _(msg`on ${str}`)
}
return str
}
}
}

View File

@ -1,45 +1,3 @@
const NOW = 5
const MINUTE = 60
const HOUR = MINUTE * 60
const DAY = HOUR * 24
const MONTH_30 = DAY * 30
const MONTH = DAY * 30.41675 // This results in 365.001 days in a year, which is close enough for nearly all cases
export function ago(date: number | string | Date): string {
let ts: number
if (typeof date === 'string') {
ts = Number(new Date(date))
} else if (date instanceof Date) {
ts = Number(date)
} else {
ts = date
}
const diffSeconds = Math.floor((Date.now() - ts) / 1e3)
if (diffSeconds < NOW) {
return `now`
} else if (diffSeconds < MINUTE) {
return `${diffSeconds}s`
} else if (diffSeconds < HOUR) {
return `${Math.floor(diffSeconds / MINUTE)}m`
} else if (diffSeconds < DAY) {
return `${Math.floor(diffSeconds / HOUR)}h`
} else if (diffSeconds < MONTH_30) {
return `${Math.round(diffSeconds / DAY)}d`
} else {
let months = diffSeconds / MONTH
if (months % 1 >= 0.9) {
months = Math.ceil(months)
} else {
months = Math.floor(months)
}
if (months < 12) {
return `${months}mo`
} else {
return new Date(ts).toLocaleDateString()
}
}
}
export function niceDate(date: number | string | Date) { export function niceDate(date: number | string | Date) {
const d = new Date(date) const d = new Date(date)
return `${d.toLocaleDateString('en-us', { return `${d.toLocaleDateString('en-us', {

View File

@ -1,5 +1,5 @@
import '@formatjs/intl-locale/polyfill' import '@formatjs/intl-locale/polyfill'
import '@formatjs/intl-pluralrules/polyfill' import '@formatjs/intl-pluralrules/polyfill-force' // Don't remove -force because detection is very slow
import '@formatjs/intl-pluralrules/locale-data/en' import '@formatjs/intl-pluralrules/locale-data/en'
import {useEffect} from 'react' import {useEffect} from 'react'

View File

@ -5,7 +5,9 @@ import {Trans} from '@lingui/macro'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
import {isInvalidHandle} from 'lib/strings/handles' import {isInvalidHandle} from 'lib/strings/handles'
import {isAndroid} from 'platform/detection'
import {atoms as a, useTheme, web} from '#/alf' import {atoms as a, useTheme, web} from '#/alf'
import {NewskieDialog} from '#/components/NewskieDialog'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export function ProfileHeaderHandle({ export function ProfileHeaderHandle({
@ -17,7 +19,10 @@ export function ProfileHeaderHandle({
const invalidHandle = isInvalidHandle(profile.handle) const invalidHandle = isInvalidHandle(profile.handle)
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
return ( return (
<View style={[a.flex_row, a.gap_xs, a.align_center]} pointerEvents="none"> <View
style={[a.flex_row, a.gap_xs, a.align_center]}
pointerEvents={isAndroid ? 'box-only' : 'auto'}>
<NewskieDialog profile={profile} />
{profile.viewer?.followedBy && !blockHide ? ( {profile.viewer?.followedBy && !blockHide ? (
<View style={[t.atoms.bg_contrast_25, a.rounded_xs, a.px_sm, a.py_xs]}> <View style={[t.atoms.bg_contrast_25, a.rounded_xs, a.px_sm, a.py_xs]}>
<Text style={[t.atoms.text, a.text_sm]}> <Text style={[t.atoms.text, a.text_sm]}>

View File

@ -1,26 +1,26 @@
import React from 'react' import React from 'react'
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {useTickEveryMinute} from '#/state/shell' import {useTickEveryMinute} from '#/state/shell'
import {ago} from 'lib/strings/time'
export function TimeElapsed({ export function TimeElapsed({
timestamp, timestamp,
children, children,
timeToString = ago, timeToString,
}: { }: {
timestamp: string timestamp: string
children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
timeToString?: (timeElapsed: string) => string timeToString?: (timeElapsed: string) => string
}) { }) {
const ago = useGetTimeAgo()
const format = timeToString ?? ago
const tick = useTickEveryMinute() const tick = useTickEveryMinute()
const [timeElapsed, setTimeAgo] = React.useState(() => const [timeElapsed, setTimeAgo] = React.useState(() => format(timestamp))
timeToString(timestamp),
)
const [prevTick, setPrevTick] = React.useState(tick) const [prevTick, setPrevTick] = React.useState(tick)
if (prevTick !== tick) { if (prevTick !== tick) {
setPrevTick(tick) setPrevTick(tick)
setTimeAgo(timeToString(timestamp)) setTimeAgo(format(timestamp))
} }
return children({timeElapsed}) return children({timeElapsed})

View File

@ -3,6 +3,7 @@ import {ScrollView, StyleSheet, View} from 'react-native'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
@ -29,13 +30,18 @@ export const LoggedOutLayout = ({
borderLeftWidth: 1, borderLeftWidth: 1,
}) })
const [isKeyboardVisible] = useIsKeyboardVisible()
if (isMobile) { if (isMobile) {
if (scrollable) { if (scrollable) {
return ( return (
<ScrollView <ScrollView
style={styles.scrollview} style={styles.scrollview}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"> keyboardDismissMode="none"
contentContainerStyle={[
{paddingBottom: isKeyboardVisible ? 300 : 0},
]}>
<View style={a.pt_md}>{children}</View> <View style={a.pt_md}>{children}</View>
</ScrollView> </ScrollView>
) )

View File

@ -1,5 +1,5 @@
import {library} from '@fortawesome/fontawesome-svg-core' import {library} from '@fortawesome/fontawesome-svg-core'
import {faAddressCard} from '@fortawesome/free-regular-svg-icons' import {faAddressCard} from '@fortawesome/free-regular-svg-icons/faAddressCard'
import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell' import {faBell as farBell} from '@fortawesome/free-regular-svg-icons/faBell'
import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark' import {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark'
import {faCalendar as farCalendar} from '@fortawesome/free-regular-svg-icons/faCalendar' import {faCalendar as farCalendar} from '@fortawesome/free-regular-svg-icons/faCalendar'
@ -25,8 +25,6 @@ import {faSquareCheck} from '@fortawesome/free-regular-svg-icons/faSquareCheck'
import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus' import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus'
import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan' import {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
import {faUser} from '@fortawesome/free-regular-svg-icons/faUser' import {faUser} from '@fortawesome/free-regular-svg-icons/faUser'
import {faFlask} from '@fortawesome/free-solid-svg-icons'
import {faUniversalAccess} from '@fortawesome/free-solid-svg-icons'
import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown' import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown'
import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft' import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight' import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
@ -62,6 +60,7 @@ import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation'
import {faEye} from '@fortawesome/free-solid-svg-icons/faEye' import {faEye} from '@fortawesome/free-solid-svg-icons/faEye'
import {faFilter} from '@fortawesome/free-solid-svg-icons/faFilter' import {faFilter} from '@fortawesome/free-solid-svg-icons/faFilter'
import {faFire} from '@fortawesome/free-solid-svg-icons/faFire' import {faFire} from '@fortawesome/free-solid-svg-icons/faFire'
import {faFlask} from '@fortawesome/free-solid-svg-icons/faFlask'
import {faGear} from '@fortawesome/free-solid-svg-icons/faGear' import {faGear} from '@fortawesome/free-solid-svg-icons/faGear'
import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe' import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe'
import {faHand} from '@fortawesome/free-solid-svg-icons/faHand' import {faHand} from '@fortawesome/free-solid-svg-icons/faHand'
@ -97,6 +96,7 @@ import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal'
import {faSliders} from '@fortawesome/free-solid-svg-icons/faSliders' import {faSliders} from '@fortawesome/free-solid-svg-icons/faSliders'
import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack' import {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack'
import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
import {faUniversalAccess} from '@fortawesome/free-solid-svg-icons/faUniversalAccess'
import {faUserCheck} from '@fortawesome/free-solid-svg-icons/faUserCheck' import {faUserCheck} from '@fortawesome/free-solid-svg-icons/faUserCheck'
import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus' import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus'
import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers' import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers'

View File

@ -1,18 +1,19 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {ScrollView} from '../com/util/Views'
import {s} from 'lib/styles'
import {ViewHeader} from '../com/util/ViewHeader'
import {Text} from '../com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {getEntries} from '#/logger/logDump'
import {ago} from 'lib/strings/time'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {getEntries} from '#/logger/logDump'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {usePalette} from 'lib/hooks/usePalette'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {s} from 'lib/styles'
import {Text} from '../com/util/text/Text'
import {ViewHeader} from '../com/util/ViewHeader'
import {ScrollView} from '../com/util/Views'
export function LogScreen({}: NativeStackScreenProps< export function LogScreen({}: NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
@ -22,6 +23,7 @@ export function LogScreen({}: NativeStackScreenProps<
const {_} = useLingui() const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const [expanded, setExpanded] = React.useState<string[]>([]) const [expanded, setExpanded] = React.useState<string[]>([])
const timeAgo = useGetTimeAgo()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@ -70,7 +72,7 @@ export function LogScreen({}: NativeStackScreenProps<
/> />
) : undefined} ) : undefined}
<Text type="sm" style={[styles.ts, pal.textLight]}> <Text type="sm" style={[styles.ts, pal.textLight]}>
{ago(entry.timestamp)} {timeAgo(entry.timestamp)}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
{expanded.includes(entry.id) ? ( {expanded.includes(entry.id) ? (

View File

@ -64,7 +64,7 @@ function SuggestedItemsHeader({
fill={t.palette.primary_500} fill={t.palette.primary_500}
style={{marginLeft: -2}} style={{marginLeft: -2}}
/> />
<Text style={[a.text_2xl, a.font_bold, t.atoms.text]}>{title}</Text> <Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}>{title}</Text>
</View> </View>
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}> <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
{description} {description}
@ -119,6 +119,9 @@ function LoadMore({
}) })
.filter(Boolean) as LoadMoreItems[] .filter(Boolean) as LoadMoreItems[]
}, [item.items, moderationOpts]) }, [item.items, moderationOpts])
if (items.length === 0) return null
const type = items[0].type const type = items[0].type
return ( return (
@ -142,20 +145,20 @@ function LoadMore({
a.relative, a.relative,
{ {
height: 32, height: 32,
width: 32 + 15 * 3, width: 32 + 15 * items.length,
}, },
]}> ]}>
<View <View
style={[ style={[
a.align_center, a.align_center,
a.justify_center, a.justify_center,
a.border,
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
a.absolute, a.absolute,
{ {
width: 30, width: 30,
height: 30, height: 30,
left: 0, left: 0,
borderWidth: 1,
backgroundColor: t.palette.primary_500, backgroundColor: t.palette.primary_500,
borderColor: t.atoms.bg.backgroundColor, borderColor: t.atoms.bg.backgroundColor,
borderRadius: type === 'profile' ? 999 : 4, borderRadius: type === 'profile' ? 999 : 4,
@ -169,13 +172,13 @@ function LoadMore({
<View <View
key={_item.key} key={_item.key}
style={[ style={[
a.border,
t.atoms.bg_contrast_25, t.atoms.bg_contrast_25,
a.absolute, a.absolute,
{ {
width: 30, width: 30,
height: 30, height: 30,
left: (i + 1) * 15, left: (i + 1) * 15,
borderWidth: 1,
borderColor: t.atoms.bg.backgroundColor, borderColor: t.atoms.bg.backgroundColor,
borderRadius: _item.type === 'profile' ? 999 : 4, borderRadius: _item.type === 'profile' ? 999 : 4,
zIndex: 3 - i, zIndex: 3 - i,
@ -350,13 +353,15 @@ export function Explore() {
} }
} }
i.push({ if (hasNextProfilesPage) {
type: 'loadMore', i.push({
key: 'loadMoreProfiles', type: 'loadMore',
isLoadingMore: isLoadingMoreProfiles, key: 'loadMoreProfiles',
onLoadMore: onLoadMoreProfiles, isLoadingMore: isLoadingMoreProfiles,
items: i.filter(item => item.type === 'profile').slice(-3), onLoadMore: onLoadMoreProfiles,
}) items: i.filter(item => item.type === 'profile').slice(-3),
})
}
} else { } else {
if (profilesError) { if (profilesError) {
i.push({ i.push({
@ -412,7 +417,7 @@ export function Explore() {
message: _(msg`Failed to load feeds preferences`), message: _(msg`Failed to load feeds preferences`),
error: cleanError(preferencesError), error: cleanError(preferencesError),
}) })
} else { } else if (hasNextFeedsPage) {
i.push({ i.push({
type: 'loadMore', type: 'loadMore',
key: 'loadMoreFeeds', key: 'loadMoreFeeds',
@ -454,6 +459,8 @@ export function Explore() {
profilesError, profilesError,
feedsError, feedsError,
preferencesError, preferencesError,
hasNextProfilesPage,
hasNextFeedsPage,
]) ])
const renderItem = React.useCallback( const renderItem = React.useCallback(

View File

@ -22470,12 +22470,7 @@ zod-validation-error@^3.0.3:
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.3.0.tgz#2cfe81b62d044e0453d1aa3ae7c32a2f36dde9af" resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.3.0.tgz#2cfe81b62d044e0453d1aa3ae7c32a2f36dde9af"
integrity sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw== integrity sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw==
zod@^3.14.2, zod@^3.20.2: zod@3.23.8, zod@^3.14.2, zod@^3.20.2, zod@^3.21.4, zod@^3.22.4:
version "3.22.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b"
integrity sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==
zod@^3.21.4, zod@^3.22.4:
version "3.23.8" version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==