Merge branch 'bluesky-social:main' into zh
This commit is contained in:
		
						commit
						a6d49062e6
					
				
					 22 changed files with 374 additions and 159 deletions
				
			
		| 
						 | 
				
			
			@ -31,6 +31,7 @@ module.exports = {
 | 
			
		|||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    'bsky-internal/use-exact-imports': 'error',
 | 
			
		||||
    'bsky-internal/use-typed-gates': 'error',
 | 
			
		||||
    'simple-import-sort/imports': [
 | 
			
		||||
      'warn',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
 | 
			
		|||
import {enforceLen} from '../../src/lib/strings/helpers'
 | 
			
		||||
import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
 | 
			
		||||
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
 | 
			
		||||
import {ago} from '../../src/lib/strings/time'
 | 
			
		||||
import {
 | 
			
		||||
  makeRecordUri,
 | 
			
		||||
  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', () => {
 | 
			
		||||
  const inputs = [
 | 
			
		||||
    'test-handle-123',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								assets/icons/newskie.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/icons/newskie.svg
									
										
									
									
									
										Normal 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  | 
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
  rules: {
 | 
			
		||||
    'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
 | 
			
		||||
    'use-exact-imports': require('./use-exact-imports'),
 | 
			
		||||
    'use-typed-gates': require('./use-typed-gates'),
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										22
									
								
								eslint/use-exact-imports.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								eslint/use-exact-imports.js
									
										
									
									
									
										Normal 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',
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -271,6 +271,7 @@
 | 
			
		|||
  "resolutions": {
 | 
			
		||||
    "@types/react": "^18",
 | 
			
		||||
    "**/zeed-dom": "0.10.9",
 | 
			
		||||
    "**/zod": "3.23.8",
 | 
			
		||||
    "**/expo-constants": "16.0.1",
 | 
			
		||||
    "**/expo-device": "6.0.2",
 | 
			
		||||
    "@react-native/babel-preset": "0.74.1"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -267,6 +267,9 @@ export const atoms = {
 | 
			
		|||
  font_bold: {
 | 
			
		||||
    fontWeight: tokens.fontWeight.bold,
 | 
			
		||||
  },
 | 
			
		||||
  font_heavy: {
 | 
			
		||||
    fontWeight: tokens.fontWeight.heavy,
 | 
			
		||||
  },
 | 
			
		||||
  italic: {
 | 
			
		||||
    fontStyle: 'italic',
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -118,6 +118,7 @@ export const fontWeight = {
 | 
			
		|||
  normal: '400',
 | 
			
		||||
  semibold: '500',
 | 
			
		||||
  bold: '600',
 | 
			
		||||
  heavy: '700',
 | 
			
		||||
} as const
 | 
			
		||||
 | 
			
		||||
export const gradients = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										81
									
								
								src/components/NewskieDialog.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/components/NewskieDialog.tsx
									
										
									
									
									
										Normal 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>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import React from 'react'
 | 
			
		||||
import {View} from 'react-native'
 | 
			
		||||
import {Keyboard, View} from 'react-native'
 | 
			
		||||
import DatePicker from 'react-native-date-picker'
 | 
			
		||||
import {msg, Trans} from '@lingui/macro'
 | 
			
		||||
import {useLingui} from '@lingui/react'
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +49,10 @@ export function DateField({
 | 
			
		|||
      <DateFieldButton
 | 
			
		||||
        label={label}
 | 
			
		||||
        value={value}
 | 
			
		||||
        onPress={control.open}
 | 
			
		||||
        onPress={() => {
 | 
			
		||||
          Keyboard.dismiss()
 | 
			
		||||
          control.open()
 | 
			
		||||
        }}
 | 
			
		||||
        isInvalid={isInvalid}
 | 
			
		||||
        accessibilityHint={accessibilityHint}
 | 
			
		||||
      />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										5
									
								
								src/components/icons/Newskie.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/components/icons/Newskie.tsx
									
										
									
									
									
										Normal 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',
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										102
									
								
								src/lib/hooks/__tests__/useTimeAgo.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/lib/hooks/__tests__/useTimeAgo.test.ts
									
										
									
									
									
										Normal 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())
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										95
									
								
								src/lib/hooks/useTimeAgo.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/lib/hooks/useTimeAgo.ts
									
										
									
									
									
										Normal 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
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
  const d = new Date(date)
 | 
			
		||||
  return `${d.toLocaleDateString('en-us', {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
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 {useEffect} from 'react'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,9 @@ import {Trans} from '@lingui/macro'
 | 
			
		|||
 | 
			
		||||
import {Shadow} from '#/state/cache/types'
 | 
			
		||||
import {isInvalidHandle} from 'lib/strings/handles'
 | 
			
		||||
import {isAndroid} from 'platform/detection'
 | 
			
		||||
import {atoms as a, useTheme, web} from '#/alf'
 | 
			
		||||
import {NewskieDialog} from '#/components/NewskieDialog'
 | 
			
		||||
import {Text} from '#/components/Typography'
 | 
			
		||||
 | 
			
		||||
export function ProfileHeaderHandle({
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +19,10 @@ export function ProfileHeaderHandle({
 | 
			
		|||
  const invalidHandle = isInvalidHandle(profile.handle)
 | 
			
		||||
  const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
 | 
			
		||||
  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 ? (
 | 
			
		||||
        <View style={[t.atoms.bg_contrast_25, a.rounded_xs, a.px_sm, a.py_xs]}>
 | 
			
		||||
          <Text style={[t.atoms.text, a.text_sm]}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,26 +1,26 @@
 | 
			
		|||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
 | 
			
		||||
import {useTickEveryMinute} from '#/state/shell'
 | 
			
		||||
import {ago} from 'lib/strings/time'
 | 
			
		||||
 | 
			
		||||
export function TimeElapsed({
 | 
			
		||||
  timestamp,
 | 
			
		||||
  children,
 | 
			
		||||
  timeToString = ago,
 | 
			
		||||
  timeToString,
 | 
			
		||||
}: {
 | 
			
		||||
  timestamp: string
 | 
			
		||||
  children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
 | 
			
		||||
  timeToString?: (timeElapsed: string) => string
 | 
			
		||||
}) {
 | 
			
		||||
  const ago = useGetTimeAgo()
 | 
			
		||||
  const format = timeToString ?? ago
 | 
			
		||||
  const tick = useTickEveryMinute()
 | 
			
		||||
  const [timeElapsed, setTimeAgo] = React.useState(() =>
 | 
			
		||||
    timeToString(timestamp),
 | 
			
		||||
  )
 | 
			
		||||
  const [timeElapsed, setTimeAgo] = React.useState(() => format(timestamp))
 | 
			
		||||
 | 
			
		||||
  const [prevTick, setPrevTick] = React.useState(tick)
 | 
			
		||||
  if (prevTick !== tick) {
 | 
			
		||||
    setPrevTick(tick)
 | 
			
		||||
    setTimeAgo(timeToString(timestamp))
 | 
			
		||||
    setTimeAgo(format(timestamp))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return children({timeElapsed})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ import {ScrollView, StyleSheet, View} from 'react-native'
 | 
			
		|||
 | 
			
		||||
import {isWeb} from '#/platform/detection'
 | 
			
		||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
 | 
			
		||||
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
 | 
			
		||||
import {usePalette} from 'lib/hooks/usePalette'
 | 
			
		||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 | 
			
		||||
import {atoms as a} from '#/alf'
 | 
			
		||||
| 
						 | 
				
			
			@ -29,13 +30,18 @@ export const LoggedOutLayout = ({
 | 
			
		|||
    borderLeftWidth: 1,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const [isKeyboardVisible] = useIsKeyboardVisible()
 | 
			
		||||
 | 
			
		||||
  if (isMobile) {
 | 
			
		||||
    if (scrollable) {
 | 
			
		||||
      return (
 | 
			
		||||
        <ScrollView
 | 
			
		||||
          style={styles.scrollview}
 | 
			
		||||
          keyboardShouldPersistTaps="handled"
 | 
			
		||||
          keyboardDismissMode="on-drag">
 | 
			
		||||
          keyboardDismissMode="none"
 | 
			
		||||
          contentContainerStyle={[
 | 
			
		||||
            {paddingBottom: isKeyboardVisible ? 300 : 0},
 | 
			
		||||
          ]}>
 | 
			
		||||
          <View style={a.pt_md}>{children}</View>
 | 
			
		||||
        </ScrollView>
 | 
			
		||||
      )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
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 {faBookmark as farBookmark} from '@fortawesome/free-regular-svg-icons/faBookmark'
 | 
			
		||||
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 {faTrashCan} from '@fortawesome/free-regular-svg-icons/faTrashCan'
 | 
			
		||||
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 {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
 | 
			
		||||
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 {faFilter} from '@fortawesome/free-solid-svg-icons/faFilter'
 | 
			
		||||
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 {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe'
 | 
			
		||||
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 {faThumbtack} from '@fortawesome/free-solid-svg-icons/faThumbtack'
 | 
			
		||||
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 {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus'
 | 
			
		||||
import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,19 @@
 | 
			
		|||
import React from 'react'
 | 
			
		||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
 | 
			
		||||
import {useFocusEffect} from '@react-navigation/native'
 | 
			
		||||
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 {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 {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<
 | 
			
		||||
  CommonNavigatorParams,
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +23,7 @@ export function LogScreen({}: NativeStackScreenProps<
 | 
			
		|||
  const {_} = useLingui()
 | 
			
		||||
  const setMinimalShellMode = useSetMinimalShellMode()
 | 
			
		||||
  const [expanded, setExpanded] = React.useState<string[]>([])
 | 
			
		||||
  const timeAgo = useGetTimeAgo()
 | 
			
		||||
 | 
			
		||||
  useFocusEffect(
 | 
			
		||||
    React.useCallback(() => {
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +72,7 @@ export function LogScreen({}: NativeStackScreenProps<
 | 
			
		|||
                    />
 | 
			
		||||
                  ) : undefined}
 | 
			
		||||
                  <Text type="sm" style={[styles.ts, pal.textLight]}>
 | 
			
		||||
                    {ago(entry.timestamp)}
 | 
			
		||||
                    {timeAgo(entry.timestamp)}
 | 
			
		||||
                  </Text>
 | 
			
		||||
                </TouchableOpacity>
 | 
			
		||||
                {expanded.includes(entry.id) ? (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -64,7 +64,7 @@ function SuggestedItemsHeader({
 | 
			
		|||
            fill={t.palette.primary_500}
 | 
			
		||||
            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>
 | 
			
		||||
        <Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
 | 
			
		||||
          {description}
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +119,9 @@ function LoadMore({
 | 
			
		|||
      })
 | 
			
		||||
      .filter(Boolean) as LoadMoreItems[]
 | 
			
		||||
  }, [item.items, moderationOpts])
 | 
			
		||||
 | 
			
		||||
  if (items.length === 0) return null
 | 
			
		||||
 | 
			
		||||
  const type = items[0].type
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
| 
						 | 
				
			
			@ -142,20 +145,20 @@ function LoadMore({
 | 
			
		|||
                a.relative,
 | 
			
		||||
                {
 | 
			
		||||
                  height: 32,
 | 
			
		||||
                  width: 32 + 15 * 3,
 | 
			
		||||
                  width: 32 + 15 * items.length,
 | 
			
		||||
                },
 | 
			
		||||
              ]}>
 | 
			
		||||
              <View
 | 
			
		||||
                style={[
 | 
			
		||||
                  a.align_center,
 | 
			
		||||
                  a.justify_center,
 | 
			
		||||
                  a.border,
 | 
			
		||||
                  t.atoms.bg_contrast_25,
 | 
			
		||||
                  a.absolute,
 | 
			
		||||
                  {
 | 
			
		||||
                    width: 30,
 | 
			
		||||
                    height: 30,
 | 
			
		||||
                    left: 0,
 | 
			
		||||
                    borderWidth: 1,
 | 
			
		||||
                    backgroundColor: t.palette.primary_500,
 | 
			
		||||
                    borderColor: t.atoms.bg.backgroundColor,
 | 
			
		||||
                    borderRadius: type === 'profile' ? 999 : 4,
 | 
			
		||||
| 
						 | 
				
			
			@ -169,13 +172,13 @@ function LoadMore({
 | 
			
		|||
                  <View
 | 
			
		||||
                    key={_item.key}
 | 
			
		||||
                    style={[
 | 
			
		||||
                      a.border,
 | 
			
		||||
                      t.atoms.bg_contrast_25,
 | 
			
		||||
                      a.absolute,
 | 
			
		||||
                      {
 | 
			
		||||
                        width: 30,
 | 
			
		||||
                        height: 30,
 | 
			
		||||
                        left: (i + 1) * 15,
 | 
			
		||||
                        borderWidth: 1,
 | 
			
		||||
                        borderColor: t.atoms.bg.backgroundColor,
 | 
			
		||||
                        borderRadius: _item.type === 'profile' ? 999 : 4,
 | 
			
		||||
                        zIndex: 3 - i,
 | 
			
		||||
| 
						 | 
				
			
			@ -350,13 +353,15 @@ export function Explore() {
 | 
			
		|||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      i.push({
 | 
			
		||||
        type: 'loadMore',
 | 
			
		||||
        key: 'loadMoreProfiles',
 | 
			
		||||
        isLoadingMore: isLoadingMoreProfiles,
 | 
			
		||||
        onLoadMore: onLoadMoreProfiles,
 | 
			
		||||
        items: i.filter(item => item.type === 'profile').slice(-3),
 | 
			
		||||
      })
 | 
			
		||||
      if (hasNextProfilesPage) {
 | 
			
		||||
        i.push({
 | 
			
		||||
          type: 'loadMore',
 | 
			
		||||
          key: 'loadMoreProfiles',
 | 
			
		||||
          isLoadingMore: isLoadingMoreProfiles,
 | 
			
		||||
          onLoadMore: onLoadMoreProfiles,
 | 
			
		||||
          items: i.filter(item => item.type === 'profile').slice(-3),
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (profilesError) {
 | 
			
		||||
        i.push({
 | 
			
		||||
| 
						 | 
				
			
			@ -412,7 +417,7 @@ export function Explore() {
 | 
			
		|||
          message: _(msg`Failed to load feeds preferences`),
 | 
			
		||||
          error: cleanError(preferencesError),
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
      } else if (hasNextFeedsPage) {
 | 
			
		||||
        i.push({
 | 
			
		||||
          type: 'loadMore',
 | 
			
		||||
          key: 'loadMoreFeeds',
 | 
			
		||||
| 
						 | 
				
			
			@ -454,6 +459,8 @@ export function Explore() {
 | 
			
		|||
    profilesError,
 | 
			
		||||
    feedsError,
 | 
			
		||||
    preferencesError,
 | 
			
		||||
    hasNextProfilesPage,
 | 
			
		||||
    hasNextFeedsPage,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  const renderItem = React.useCallback(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
  integrity sha512-Syib9oumw1NTqEv4LT0e6U83Td9aVRk9iTXPUQr1otyV1PuXQKOvOwhMNqZIq5hluzHP2pMgnOmHEo7kPdI2mw==
 | 
			
		||||
 | 
			
		||||
zod@^3.14.2, zod@^3.20.2:
 | 
			
		||||
  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:
 | 
			
		||||
zod@3.23.8, zod@^3.14.2, zod@^3.20.2, zod@^3.21.4, zod@^3.22.4:
 | 
			
		||||
  version "3.23.8"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
 | 
			
		||||
  integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue