Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6c5044c1d | |||
| c35c840c3d | |||
| 9054ba6979 | |||
| 6903b68557 |
+2
-1
@@ -95,7 +95,8 @@ web-build/
|
||||
/ios/
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Firebase (Android) Google services
|
||||
# INCLUDED: google-services.json
|
||||
# INCLUDED: google-services.json
|
||||
@@ -25,8 +25,6 @@ jobs:
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -25,8 +25,6 @@ jobs:
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -54,9 +54,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: last-successful-commit-hash.txt
|
||||
key: last-successful-deployment-commit-${{ github.ref_name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
last-successful-deployment-commit-${{ github.ref_name }}-
|
||||
key: last-successful-deployment-commit-${{ github.ref_name }}
|
||||
|
||||
- name: Add the last successful deployment commit to the output
|
||||
id: last-successful-commit
|
||||
@@ -70,7 +68,7 @@ jobs:
|
||||
- name: 🕵️ Get the base commit
|
||||
id: base-commit
|
||||
run: |
|
||||
if ${{ inputs.channel == 'production' }}; then
|
||||
if [ -z "${{ inputs.channel == 'production' }}" ]; then
|
||||
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo base-commit=${{ steps.last-successful-commit.base-commit }} >> "$GITHUB_OUTPUT"
|
||||
@@ -186,8 +184,6 @@ jobs:
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -259,8 +255,6 @@ jobs:
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -3,7 +3,7 @@ import {RichText} from '@atproto/api'
|
||||
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
|
||||
import {cleanError} from '../../src/lib/strings/errors'
|
||||
import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
|
||||
import {enforceLen} from '../../src/lib/strings/helpers'
|
||||
import {enforceLen, pluralize} 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'
|
||||
@@ -127,6 +127,35 @@ describe('detectLinkables', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('pluralize', () => {
|
||||
const inputs: [number, string, string?][] = [
|
||||
[1, 'follower'],
|
||||
[1, 'member'],
|
||||
[100, 'post'],
|
||||
[1000, 'repost'],
|
||||
[10000, 'upvote'],
|
||||
[100000, 'other'],
|
||||
[2, 'man', 'men'],
|
||||
]
|
||||
const outputs = [
|
||||
'follower',
|
||||
'member',
|
||||
'posts',
|
||||
'reposts',
|
||||
'upvotes',
|
||||
'others',
|
||||
'men',
|
||||
]
|
||||
|
||||
it('correctly pluralizes a set of words', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const output = pluralize(...input)
|
||||
expect(output).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeRecordUri', () => {
|
||||
const inputs: [string, string, string][] = [
|
||||
['alice.test', 'app.bsky.feed.post', '3jk7x4irgv52r'],
|
||||
|
||||
@@ -91,28 +91,6 @@ module.exports = function (config) {
|
||||
entitlements: {
|
||||
'com.apple.security.application-groups': 'group.app.bsky',
|
||||
},
|
||||
privacyManifests: {
|
||||
NSPrivacyAccessedAPITypes: [
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
'NSPrivacyAccessedAPICategoryFileTimestamp',
|
||||
NSPrivacyAccessedAPITypeReasons: ['C617.1', '3B52.1', '0A2A.1'],
|
||||
},
|
||||
{
|
||||
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryDiskSpace',
|
||||
NSPrivacyAccessedAPITypeReasons: ['E174.1', '85F4.1'],
|
||||
},
|
||||
{
|
||||
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryBootTime',
|
||||
NSPrivacyAccessedAPITypeReasons: ['35F9.1'],
|
||||
},
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
'NSPrivacyAccessedAPICategoryUserDefaults',
|
||||
NSPrivacyAccessedAPITypeReasons: ['CA92.1'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
androidStatusBar: {
|
||||
barStyle: 'light-content',
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M17.657 6.343A8 8 0 1 0 6.343 17.657 8 8 0 0 0 17.657 6.343ZM4.929 4.93c3.905-3.905 10.237-3.905 14.142 0 3.905 3.905 3.905 10.237 0 14.142-3.905 3.905-10.237 3.905-14.142 0-3.905-3.905-3.905-10.237 0-14.142Zm3.536 9.192a1 1 0 0 1 1.414 0 3 3 0 0 0 4.243 0 1 1 0 0 1 1.414 1.415 5 5 0 0 1-7.071 0 1 1 0 0 1 0-1.415Z M10.5 9.5c0 .828-.56 1.5-1.25 1.5S8 10.328 8 9.5 8.56 8 9.25 8s1.25.672 1.25 1.5ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 623 B |
@@ -47,14 +47,6 @@
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#preload {
|
||||
width: 100px;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||
button, input, textarea {
|
||||
font: inherit;
|
||||
@@ -270,19 +262,13 @@
|
||||
</head>
|
||||
<body>
|
||||
{%- block body_all %}
|
||||
<div id="root">
|
||||
<div id="preload">
|
||||
<!-- Bluesky SVG -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 320"><path fill="#0085ff" d="M180 142c-16.3-31.7-60.7-90.8-102-120C38.5-5.9 23.4-1 13.5 3.4 2.1 8.6 0 26.2 0 36.5c0 10.4 5.7 84.8 9.4 97.2 12.2 41 55.7 55 95.7 50.5-58.7 8.6-110.8 30-42.4 106.1 75.1 77.9 103-16.7 117.3-64.6 14.3 48 30.8 139 116 64.6 64-64.6 17.6-97.5-41.1-106.1 40 4.4 83.5-9.5 95.7-50.5 3.7-12.4 9.4-86.8 9.4-97.2 0-10.3-2-27.9-13.5-33C336.5-1 321.5-6 282 22c-41.3 29.2-85.7 88.3-102 120Z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
<h1 lang="en">JavaScript Required</h1>
|
||||
<p lang="en">This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is.
|
||||
<p lang="en">Learn more about Bluesky at <a href="https://bsky.social">bsky.social</a> and <a href="https://atproto.com">atproto.com</a>.
|
||||
{% block noscript_extra %}{% endblock %}
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<noscript>
|
||||
<h1 lang="en">JavaScript Required</h1>
|
||||
<p lang="en">This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is.
|
||||
<p lang="en">Learn more about Bluesky at <a href="https://bsky.social">bsky.social</a> and <a href="https://atproto.com">atproto.com</a>.
|
||||
{% block noscript_extra %}{% endblock %}
|
||||
</noscript>
|
||||
{% endblock -%}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {requireNativeViewManager} from 'expo-modules-core'
|
||||
import * as React from 'react'
|
||||
|
||||
import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
|
||||
|
||||
const NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =
|
||||
|
||||
+1
-5
@@ -60,8 +60,6 @@
|
||||
"@expo/webpack-config": "^19.0.0",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@floating-ui/react-dom": "^2.0.8",
|
||||
"@formatjs/intl-locale": "^3.4.3",
|
||||
"@formatjs/intl-pluralrules": "^5.2.10",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
@@ -100,7 +98,6 @@
|
||||
"@tiptap/react": "^2.0.0-beta.220",
|
||||
"@tiptap/suggestion": "^2.0.0-beta.220",
|
||||
"@types/invariant": "^2.2.37",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/node": "^18.16.2",
|
||||
"@zxing/text-encoding": "^0.9.0",
|
||||
"array.prototype.findlast": "^1.2.3",
|
||||
@@ -112,7 +109,7 @@
|
||||
"email-validator": "^2.0.4",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "^50.0.17",
|
||||
"expo": "^50.0.8",
|
||||
"expo-application": "^5.8.3",
|
||||
"expo-build-properties": "^0.11.1",
|
||||
"expo-camera": "~14.0.4",
|
||||
@@ -152,7 +149,6 @@
|
||||
"lodash.samplesize": "^4.2.0",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.shuffle": "^4.2.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mobx": "^6.6.1",
|
||||
"mobx-react-lite": "^3.4.0",
|
||||
"mobx-utils": "^6.0.6",
|
||||
|
||||
+19
-22
@@ -16,7 +16,6 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {MessagesEventBusProvider} from '#/state/messages/events'
|
||||
import {init as initPersistedState} from '#/state/persisted'
|
||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
|
||||
@@ -96,27 +95,25 @@ function InnerApp() {
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<MessagesEventBusProvider>
|
||||
<PushNotificationsListener>
|
||||
<StatsigProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</StatsigProvider>
|
||||
</PushNotificationsListener>
|
||||
</MessagesEventBusProvider>
|
||||
<PushNotificationsListener>
|
||||
<StatsigProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</StatsigProvider>
|
||||
</PushNotificationsListener>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</RootSiblingParent>
|
||||
|
||||
+16
-19
@@ -9,7 +9,6 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {MessagesEventBusProvider} from '#/state/messages/events'
|
||||
import {init as initPersistedState} from '#/state/persisted'
|
||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
|
||||
@@ -84,24 +83,22 @@ function InnerApp() {
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<MessagesEventBusProvider>
|
||||
<StatsigProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</StatsigProvider>
|
||||
</MessagesEventBusProvider>
|
||||
<StatsigProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {AppBskyLabelerDefs} from '@atproto/api'
|
||||
|
||||
@@ -13,6 +13,7 @@ import {RichText} from '#/components/RichText'
|
||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '../icons/Chevron'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
|
||||
type LabelingServiceProps = {
|
||||
labeler: AppBskyLabelerDefs.LabelerViewDetailed
|
||||
@@ -68,7 +69,9 @@ export function LikeCount({count}: {count: number}) {
|
||||
t.atoms.text_contrast_medium,
|
||||
{fontWeight: '500'},
|
||||
]}>
|
||||
<Plural value={count} one="Liked by # user" other="Liked by # users" />
|
||||
<Trans>
|
||||
Liked by {count} {pluralize(count, 'user')}
|
||||
</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||
import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom'
|
||||
import {msg, plural} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -370,14 +371,7 @@ function Inner({
|
||||
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
|
||||
const following = formatCount(profile.followsCount || 0)
|
||||
const followers = formatCount(profile.followersCount || 0)
|
||||
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
||||
one: 'follower',
|
||||
other: 'followers',
|
||||
})
|
||||
const pluralizedFollowings = plural(profile.followsCount || 0, {
|
||||
one: 'following',
|
||||
other: 'following',
|
||||
})
|
||||
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
|
||||
const profileURL = makeProfileLink({
|
||||
did: profile.did,
|
||||
handle: profile.handle,
|
||||
@@ -404,9 +398,7 @@ function Inner({
|
||||
color={profileShadow.viewer?.following ? 'secondary' : 'primary'}
|
||||
variant="solid"
|
||||
label={
|
||||
profileShadow.viewer?.following
|
||||
? _(msg`Following`)
|
||||
: _(msg`Follow`)
|
||||
profileShadow.viewer?.following ? _('Following') : _('Follow')
|
||||
}
|
||||
style={[a.rounded_full]}
|
||||
onPress={profileShadow.viewer?.following ? unfollow : follow}>
|
||||
@@ -415,9 +407,7 @@ function Inner({
|
||||
icon={profileShadow.viewer?.following ? Check : Plus}
|
||||
/>
|
||||
<ButtonText>
|
||||
{profileShadow.viewer?.following
|
||||
? _(msg`Following`)
|
||||
: _(msg`Follow`)}
|
||||
{profileShadow.viewer?.following ? _('Following') : _('Follow')}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
@@ -444,20 +434,22 @@ function Inner({
|
||||
label={`${followers} ${pluralizedFollowers}`}
|
||||
style={[t.atoms.text]}
|
||||
onPress={hide}>
|
||||
<Text style={[a.text_md, a.font_bold]}>{followers} </Text>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
{pluralizedFollowers}
|
||||
</Text>
|
||||
<Trans>
|
||||
<Text style={[a.text_md, a.font_bold]}>{followers} </Text>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
{pluralizedFollowers}
|
||||
</Text>
|
||||
</Trans>
|
||||
</InlineLinkText>
|
||||
<InlineLinkText
|
||||
to={makeProfileLink(profile, 'follows')}
|
||||
label={_(msg`${following} following`)}
|
||||
style={[t.atoms.text]}
|
||||
onPress={hide}>
|
||||
<Text style={[a.text_md, a.font_bold]}>{following} </Text>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>
|
||||
{pluralizedFollowings}
|
||||
</Text>
|
||||
<Trans>
|
||||
<Text style={[a.text_md, a.font_bold]}>{following} </Text>
|
||||
<Text style={[t.atoms.text_contrast_medium]}>following</Text>
|
||||
</Trans>
|
||||
</InlineLinkText>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -62,16 +62,12 @@ export function TitleText({children}: React.PropsWithChildren<{}>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function DescriptionText({
|
||||
children,
|
||||
selectable,
|
||||
}: React.PropsWithChildren<{selectable?: boolean}>) {
|
||||
export function DescriptionText({children}: React.PropsWithChildren<{}>) {
|
||||
const t = useTheme()
|
||||
const {descriptionId} = React.useContext(Context)
|
||||
return (
|
||||
<Text
|
||||
nativeID={descriptionId}
|
||||
selectable={selectable}
|
||||
style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high, a.pb_lg]}>
|
||||
{children}
|
||||
</Text>
|
||||
|
||||
@@ -4,10 +4,6 @@ export const EmojiSad_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M6.343 6.343a8 8 0 1 1 11.314 11.314A8 8 0 0 1 6.343 6.343ZM19.071 4.93c-3.905-3.905-10.237-3.905-14.142 0-3.905 3.905-3.905 10.237 0 14.142 3.905 3.905 10.237 3.905 14.142 0 3.905-3.905 3.905-10.237 0-14.142Zm-3.537 9.535a5 5 0 0 0-7.07 0 1 1 0 1 0 1.413 1.415 3 3 0 0 1 4.243 0 1 1 0 0 0 1.414-1.415ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5ZM9.25 11c.69 0 1.25-.672 1.25-1.5S9.94 8 9.25 8 8 8.672 8 9.5 8.56 11 9.25 11Z',
|
||||
})
|
||||
|
||||
export const EmojiSmile_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M17.657 6.343A8 8 0 1 0 6.343 17.657 8 8 0 0 0 17.657 6.343ZM4.929 4.93c3.905-3.905 10.237-3.905 14.142 0 3.905 3.905 3.905 10.237 0 14.142-3.905 3.905-10.237 3.905-14.142 0-3.905-3.905-3.905-10.237 0-14.142Zm3.536 9.192a1 1 0 0 1 1.414 0 3 3 0 0 0 4.243 0 1 1 0 0 1 1.414 1.415 5 5 0 0 1-7.071 0 1 1 0 0 1 0-1.415ZM10.5 9.5c0 .828-.56 1.5-1.25 1.5S8 10.328 8 9.5 8.56 8 9.25 8s1.25.672 1.25 1.5ZM16 9.5c0 .828-.56 1.5-1.25 1.5s-1.25-.672-1.25-1.5.56-1.5 1.25-1.5S16 8.672 16 9.5Z',
|
||||
})
|
||||
|
||||
export const EmojiArc_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm8-5a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V8a1 1 0 0 1 1-1Zm-5.894 7.803a1 1 0 0 1 1.341-.447c1.719.859 3.387.859 5.106 0a1 1 0 1 1 .894 1.788c-2.281 1.141-4.613 1.141-6.894 0a1 1 0 0 1-.447-1.341Z',
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {msg, Plural} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
@@ -39,6 +39,7 @@ export function LabelsOnMe({
|
||||
return null
|
||||
}
|
||||
|
||||
const labelTarget = isAccount ? _(msg`account`) : _(msg`content`)
|
||||
return (
|
||||
<View style={[a.flex_row, style]}>
|
||||
<LabelsOnMeDialog control={control} subject={details} labels={labels} />
|
||||
@@ -53,18 +54,11 @@ export function LabelsOnMe({
|
||||
}}>
|
||||
<ButtonIcon position="left" icon={CircleInfo} />
|
||||
<ButtonText style={[a.leading_snug]}>
|
||||
{isAccount ? (
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has been placed on this account"
|
||||
other="# labels has been placed on this account"
|
||||
/>
|
||||
{labels.length}{' '}
|
||||
{labels.length === 1 ? (
|
||||
<Trans>label has been placed on this {labelTarget}</Trans>
|
||||
) : (
|
||||
<Plural
|
||||
value={labels.length}
|
||||
one="# label has been placed on this content"
|
||||
other="# labels has been placed on this content"
|
||||
/>
|
||||
<Trans>labels have been placed on this {labelTarget}</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
|
||||
@@ -190,7 +190,7 @@ function AppealForm({
|
||||
},
|
||||
reason: details,
|
||||
})
|
||||
Toast.show(_(msg`Appeal submitted`))
|
||||
Toast.show(_(msg`Appeal submitted.`))
|
||||
} finally {
|
||||
control.close()
|
||||
}
|
||||
|
||||
@@ -121,20 +121,20 @@ function ModerationDetailsDialogInner({
|
||||
<>
|
||||
<Divider />
|
||||
<Text style={[t.atoms.text, a.text_md, a.leading_snug, a.mt_lg]}>
|
||||
{modcause.source.type === 'user' ? (
|
||||
<Trans>This label was applied by the author.</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
This label was applied by{' '}
|
||||
<Trans>
|
||||
This label was applied by{' '}
|
||||
{modcause.source.type === 'user' ? (
|
||||
<Trans>the author</Trans>
|
||||
) : (
|
||||
<InlineLinkText
|
||||
to={makeProfileLink({did: modcause.label.src, handle: ''})}
|
||||
onPress={() => control.close()}
|
||||
style={a.text_md}>
|
||||
{desc.source}
|
||||
</InlineLinkText>
|
||||
.
|
||||
</Trans>
|
||||
)}
|
||||
)}
|
||||
.
|
||||
</Trans>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
|
||||
|
||||
// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
|
||||
// along with the other version info. Useful for debugging/reporting.
|
||||
export const BUNDLE_IDENTIFIER = process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? ''
|
||||
export const BUNDLE_IDENTIFIER =
|
||||
process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? 'dev'
|
||||
|
||||
// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used
|
||||
// for Statsig reporting and shouldn't be used to identify a specific bundle.
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Kind of a hack. We needed some way to distinguish these.
|
||||
const USER_ALT_PREFIX = 'Alt: '
|
||||
const DEFAULT_ALT_PREFIX = 'ALT: '
|
||||
|
||||
export function createGIFDescription(
|
||||
tenorDescription: string,
|
||||
preferredAlt: string = '',
|
||||
) {
|
||||
preferredAlt = preferredAlt.trim()
|
||||
if (preferredAlt !== '') {
|
||||
return USER_ALT_PREFIX + preferredAlt
|
||||
} else {
|
||||
return DEFAULT_ALT_PREFIX + tenorDescription
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAltFromGIFDescription(description: string): {
|
||||
isPreferred: boolean
|
||||
alt: string
|
||||
} {
|
||||
if (description.startsWith(USER_ALT_PREFIX)) {
|
||||
return {
|
||||
isPreferred: true,
|
||||
alt: description.replace(USER_ALT_PREFIX, ''),
|
||||
}
|
||||
} else if (description.startsWith(DEFAULT_ALT_PREFIX)) {
|
||||
return {
|
||||
isPreferred: false,
|
||||
alt: description.replace(DEFAULT_ALT_PREFIX, ''),
|
||||
}
|
||||
}
|
||||
return {
|
||||
isPreferred: false,
|
||||
alt: description,
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import {
|
||||
Image as RNImage,
|
||||
openCamera as openCameraFn,
|
||||
openCropper as openCropperFn,
|
||||
Image as RNImage,
|
||||
} from 'react-native-image-crop-picker'
|
||||
|
||||
import {CameraOpts, CropperOptions} from './types'
|
||||
export {openPicker} from './picker.shared'
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/// <reference lib="dom" />
|
||||
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
|
||||
import {CameraOpts, CropperOptions} from './types'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
export {openPicker} from './picker.shared'
|
||||
import {unstable__openModal} from '#/state/modals'
|
||||
|
||||
@@ -17,10 +16,6 @@ export async function openCropper(opts: CropperOptions): Promise<RNImage> {
|
||||
unstable__openModal({
|
||||
name: 'crop-image',
|
||||
uri: opts.path,
|
||||
dimensions:
|
||||
opts.height && opts.width
|
||||
? {width: opts.width, height: opts.height}
|
||||
: undefined,
|
||||
onSelect: (img?: RNImage) => {
|
||||
if (img) {
|
||||
resolve(img)
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
export function pluralize(n: number, base: string, plural?: string): string {
|
||||
if (n === 1) {
|
||||
return base
|
||||
}
|
||||
if (plural) {
|
||||
return plural
|
||||
}
|
||||
return base + 's'
|
||||
}
|
||||
|
||||
export function enforceLen(
|
||||
str: string,
|
||||
len: number,
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import '@formatjs/intl-locale/polyfill'
|
||||
import '@formatjs/intl-pluralrules/polyfill'
|
||||
import '@formatjs/intl-pluralrules/locale-data/en'
|
||||
|
||||
import {useEffect} from 'react'
|
||||
import {i18n} from '@lingui/core'
|
||||
|
||||
@@ -33,82 +29,66 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
switch (locale) {
|
||||
case AppLanguage.ca: {
|
||||
i18n.loadAndActivate({locale, messages: messagesCa})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/ca')
|
||||
break
|
||||
}
|
||||
case AppLanguage.de: {
|
||||
i18n.loadAndActivate({locale, messages: messagesDe})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/de')
|
||||
break
|
||||
}
|
||||
case AppLanguage.es: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEs})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/es')
|
||||
break
|
||||
}
|
||||
case AppLanguage.fi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFi})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/fi')
|
||||
break
|
||||
}
|
||||
case AppLanguage.fr: {
|
||||
i18n.loadAndActivate({locale, messages: messagesFr})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/fr')
|
||||
break
|
||||
}
|
||||
case AppLanguage.ga: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGa})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/ga')
|
||||
break
|
||||
}
|
||||
case AppLanguage.hi: {
|
||||
i18n.loadAndActivate({locale, messages: messagesHi})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/hi')
|
||||
break
|
||||
}
|
||||
case AppLanguage.id: {
|
||||
i18n.loadAndActivate({locale, messages: messagesId})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/id')
|
||||
break
|
||||
}
|
||||
case AppLanguage.it: {
|
||||
i18n.loadAndActivate({locale, messages: messagesIt})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/it')
|
||||
break
|
||||
}
|
||||
case AppLanguage.ja: {
|
||||
i18n.loadAndActivate({locale, messages: messagesJa})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/ja')
|
||||
break
|
||||
}
|
||||
case AppLanguage.ko: {
|
||||
i18n.loadAndActivate({locale, messages: messagesKo})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/ko')
|
||||
break
|
||||
}
|
||||
case AppLanguage.pt_BR: {
|
||||
i18n.loadAndActivate({locale, messages: messagesPt_BR})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/pt')
|
||||
break
|
||||
}
|
||||
case AppLanguage.tr: {
|
||||
i18n.loadAndActivate({locale, messages: messagesTr})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/tr')
|
||||
break
|
||||
}
|
||||
case AppLanguage.uk: {
|
||||
i18n.loadAndActivate({locale, messages: messagesUk})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/uk')
|
||||
break
|
||||
}
|
||||
case AppLanguage.zh_CN: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_CN})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/zh')
|
||||
break
|
||||
}
|
||||
case AppLanguage.zh_TW: {
|
||||
i18n.loadAndActivate({locale, messages: messagesZh_TW})
|
||||
await import('@formatjs/intl-pluralrules/locale-data/zh')
|
||||
break
|
||||
}
|
||||
default: {
|
||||
|
||||
+508
-882
File diff suppressed because it is too large
Load Diff
+492
-870
File diff suppressed because it is too large
Load Diff
+488
-866
File diff suppressed because it is too large
Load Diff
+493
-867
File diff suppressed because it is too large
Load Diff
+577
-917
File diff suppressed because it is too large
Load Diff
+497
-875
File diff suppressed because it is too large
Load Diff
+497
-875
File diff suppressed because it is too large
Load Diff
+493
-867
File diff suppressed because it is too large
Load Diff
+1723
-1276
File diff suppressed because it is too large
Load Diff
+499
-874
File diff suppressed because it is too large
Load Diff
+506
-867
File diff suppressed because it is too large
Load Diff
+508
-873
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+494
-868
File diff suppressed because it is too large
Load Diff
+497
-875
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+994
-1280
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg, plural, Trans} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {isSessionDeactivated, useAgent, useSessionApi} from '#/state/session'
|
||||
@@ -204,16 +205,10 @@ function msToString(ms: number | undefined): string | undefined {
|
||||
return undefined
|
||||
}
|
||||
// hours
|
||||
return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, {
|
||||
one: 'hour',
|
||||
other: 'hours',
|
||||
})}`
|
||||
return `${estimatedTimeHrs} ${pluralize(estimatedTimeHrs, 'hour')}`
|
||||
}
|
||||
// minutes
|
||||
return `${estimatedTimeMins} ${plural(estimatedTimeMins, {
|
||||
one: 'minute',
|
||||
other: 'minutes',
|
||||
})}`
|
||||
return `${estimatedTimeMins} ${pluralize(estimatedTimeMins, 'minute')}`
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ export function MessageListError({
|
||||
const {_} = useLingui()
|
||||
const message = React.useMemo(() => {
|
||||
return {
|
||||
[ConvoItemError.Network]: _(
|
||||
[ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`),
|
||||
[ConvoItemError.ResumeFailed]: _(
|
||||
msg`There was an issue connecting to the chat.`,
|
||||
),
|
||||
[ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`),
|
||||
[ConvoItemError.PollFailed]: _(
|
||||
msg`This chat was disconnected due to a network error.`,
|
||||
),
|
||||
|
||||
@@ -18,7 +18,6 @@ import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||
import {ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -52,21 +51,8 @@ function Inner() {
|
||||
}
|
||||
|
||||
if (chat.status === ConvoStatus.Error) {
|
||||
// TODO
|
||||
return (
|
||||
<View>
|
||||
<CenteredView style={{flex: 1}} sideBorders>
|
||||
<Text>Something went wrong</Text>
|
||||
<Button
|
||||
label="Retry"
|
||||
onPress={() => {
|
||||
chat.error.retry()
|
||||
}}>
|
||||
<ButtonText>Retry</ButtonText>
|
||||
</Button>
|
||||
</CenteredView>
|
||||
</View>
|
||||
)
|
||||
// TODO error
|
||||
return null
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||
@@ -109,7 +109,7 @@ function PrimaryFeedCardInner({
|
||||
a.py_xs,
|
||||
ctx.selected && styles.textSelected,
|
||||
]}>
|
||||
<Trans>by @{feed.creatorHandle}</Trans>
|
||||
by @{feed.creatorHandle}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -189,9 +189,9 @@ export function StepInterests() {
|
||||
color: t.palette.negative_900,
|
||||
},
|
||||
]}>
|
||||
<Trans>Error:</Trans>{' '}
|
||||
Error:{' '}
|
||||
</Text>
|
||||
{error?.message || _(msg`an unknown error occurred`)}
|
||||
{error?.message || 'an unknown error occurred'}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg, plural} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {formatCount} from 'view/com/util/numeric/format'
|
||||
@@ -20,14 +21,7 @@ export function ProfileHeaderMetrics({
|
||||
const {_} = useLingui()
|
||||
const following = formatCount(profile.followsCount || 0)
|
||||
const followers = formatCount(profile.followersCount || 0)
|
||||
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
||||
one: 'follower',
|
||||
other: 'followers',
|
||||
})
|
||||
const pluralizedFollowings = plural(profile.followsCount || 0, {
|
||||
one: 'following',
|
||||
other: 'following',
|
||||
})
|
||||
const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -48,15 +42,17 @@ export function ProfileHeaderMetrics({
|
||||
style={[a.flex_row, t.atoms.text]}
|
||||
to={makeProfileLink(profile, 'follows')}
|
||||
label={_(msg`${following} following`)}>
|
||||
<Text style={[a.font_bold, a.text_md]}>{following} </Text>
|
||||
<Text style={[t.atoms.text_contrast_medium, a.text_md]}>
|
||||
{pluralizedFollowings}
|
||||
</Text>
|
||||
<Trans>
|
||||
<Text style={[a.font_bold, a.text_md]}>{following} </Text>
|
||||
<Text style={[t.atoms.text_contrast_medium, a.text_md]}>
|
||||
following
|
||||
</Text>
|
||||
</Trans>
|
||||
</InlineLinkText>
|
||||
<Text style={[a.font_bold, t.atoms.text, a.text_md]}>
|
||||
{formatCount(profile.postsCount || 0)}{' '}
|
||||
<Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>
|
||||
{plural(profile.postsCount || 0, {one: 'post', other: 'posts'})}
|
||||
{pluralize(profile.postsCount || 0, 'post')}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
ModerationOpts,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Plural, plural, Trans} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isAppLabeler} from '#/lib/moderation'
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
@@ -282,10 +283,12 @@ let ProfileHeaderLabeler = ({
|
||||
},
|
||||
}}
|
||||
size="tiny"
|
||||
label={plural(likeCount, {
|
||||
one: 'Liked by # user',
|
||||
other: 'Liked by # users',
|
||||
})}>
|
||||
label={_(
|
||||
msg`Liked by ${likeCount} ${pluralize(
|
||||
likeCount,
|
||||
'user',
|
||||
)}`,
|
||||
)}>
|
||||
{({hovered, focused, pressed}) => (
|
||||
<Text
|
||||
style={[
|
||||
@@ -295,11 +298,9 @@ let ProfileHeaderLabeler = ({
|
||||
(hovered || focused || pressed) &&
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Plural
|
||||
value={likeCount}
|
||||
one="Liked by # user"
|
||||
other="Liked by # users"
|
||||
/>
|
||||
<Trans>
|
||||
Liked by {likeCount} {pluralize(likeCount, 'user')}
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
@@ -95,11 +95,7 @@ function ProfileEndOfFeed() {
|
||||
const pal = usePalette('default')
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.border,
|
||||
{paddingTop: 32, paddingBottom: 32, borderTopWidth: 1},
|
||||
]}>
|
||||
<View style={[pal.border, {paddingTop: 32, borderTopWidth: 1}]}>
|
||||
<Text style={[pal.textLight, pal.border, {textAlign: 'center'}]}>
|
||||
<Trans>End of feed</Trans>
|
||||
</Text>
|
||||
|
||||
@@ -151,13 +151,11 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||
]}>
|
||||
<View style={[a.gap_sm, a.pb_3xl]}>
|
||||
<Text style={[a.font_semibold, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Step {state.activeStep + 1} of{' '}
|
||||
{state.serviceDescription &&
|
||||
!state.serviceDescription.phoneVerificationRequired
|
||||
? '2'
|
||||
: '3'}
|
||||
</Trans>
|
||||
<Trans>Step</Trans> {state.activeStep + 1} <Trans>of</Trans>{' '}
|
||||
{state.serviceDescription &&
|
||||
!state.serviceDescription.phoneVerificationRequired
|
||||
? '2'
|
||||
: '3'}
|
||||
</Text>
|
||||
<Text style={[a.text_3xl, a.font_bold]}>
|
||||
{state.activeStep === SignupStep.INFO ? (
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import React from 'react'
|
||||
import {AppState, AppStateStatus} from 'react-native'
|
||||
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {
|
||||
FeedDescriptor,
|
||||
FeedPostSliceItem,
|
||||
isFeedPostSlice,
|
||||
} from '#/state/queries/post-feed'
|
||||
import {useAgent} from './session'
|
||||
|
||||
type StateContext = {
|
||||
enabled: boolean
|
||||
onItemSeen: (item: any) => void
|
||||
sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
}
|
||||
|
||||
const stateContext = React.createContext<StateContext>({
|
||||
enabled: false,
|
||||
onItemSeen: (_item: any) => {},
|
||||
sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {},
|
||||
})
|
||||
|
||||
export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
const {getAgent} = useAgent()
|
||||
const enabled = isDiscoverFeed(feed) && hasSession
|
||||
const queue = React.useRef<Set<string>>(new Set())
|
||||
const history = React.useRef<
|
||||
// Use a WeakSet so that we don't need to clear it.
|
||||
// This assumes that referential identity of slice items maps 1:1 to feed (re)fetches.
|
||||
WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction>
|
||||
>(new WeakSet())
|
||||
|
||||
const sendToFeedNoDelay = React.useCallback(() => {
|
||||
const proxyAgent = getAgent().withProxy(
|
||||
// @ts-ignore TODO need to update withProxy() to support this key -prf
|
||||
'bsky_fg',
|
||||
// TODO when we start sending to other feeds, we need to grab their DID -prf
|
||||
'did:web:discover.bsky.app',
|
||||
) as BskyAgent
|
||||
|
||||
const interactions = Array.from(queue.current).map(toInteraction)
|
||||
queue.current.clear()
|
||||
|
||||
proxyAgent.app.bsky.feed
|
||||
.sendInteractions({interactions})
|
||||
.catch((e: any) => {
|
||||
logger.warn('Failed to send feed interactions', {error: e})
|
||||
})
|
||||
}, [getAgent])
|
||||
|
||||
const sendToFeed = React.useMemo(
|
||||
() =>
|
||||
throttle(sendToFeedNoDelay, 15e3, {
|
||||
leading: false,
|
||||
trailing: true,
|
||||
}),
|
||||
[sendToFeedNoDelay],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
const sub = AppState.addEventListener('change', (state: AppStateStatus) => {
|
||||
if (state === 'background') {
|
||||
sendToFeed.flush()
|
||||
}
|
||||
})
|
||||
return () => sub.remove()
|
||||
}, [enabled, sendToFeed])
|
||||
|
||||
const onItemSeen = React.useCallback(
|
||||
(slice: any) => {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
if (!isFeedPostSlice(slice)) {
|
||||
return
|
||||
}
|
||||
for (const postItem of slice.items) {
|
||||
if (!history.current.has(postItem)) {
|
||||
history.current.add(postItem)
|
||||
queue.current.add(
|
||||
toString({
|
||||
item: postItem.uri,
|
||||
event: 'app.bsky.feed.defs#interactionSeen',
|
||||
feedContext: postItem.feedContext,
|
||||
}),
|
||||
)
|
||||
sendToFeed()
|
||||
}
|
||||
}
|
||||
},
|
||||
[enabled, sendToFeed],
|
||||
)
|
||||
|
||||
const sendInteraction = React.useCallback(
|
||||
(interaction: AppBskyFeedDefs.Interaction) => {
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
if (!history.current.has(interaction)) {
|
||||
history.current.add(interaction)
|
||||
queue.current.add(toString(interaction))
|
||||
sendToFeed()
|
||||
}
|
||||
},
|
||||
[enabled, sendToFeed],
|
||||
)
|
||||
|
||||
return React.useMemo(() => {
|
||||
return {
|
||||
enabled,
|
||||
// pass this method to the <List> onItemSeen
|
||||
onItemSeen,
|
||||
// call on various events
|
||||
// queues the event to be sent with the throttled sendToFeed call
|
||||
sendInteraction,
|
||||
}
|
||||
}, [enabled, onItemSeen, sendInteraction])
|
||||
}
|
||||
|
||||
export const FeedFeedbackProvider = stateContext.Provider
|
||||
|
||||
export function useFeedFeedbackContext() {
|
||||
return React.useContext(stateContext)
|
||||
}
|
||||
|
||||
// TODO
|
||||
// We will introduce a permissions framework for 3p feeds to
|
||||
// take advantage of the feed feedback API. Until that's in
|
||||
// place, we're hardcoding it to the discover feed.
|
||||
// -prf
|
||||
function isDiscoverFeed(feed: FeedDescriptor) {
|
||||
return feed === `feedgen|${PROD_DEFAULT_FEED('whats-hot')}`
|
||||
}
|
||||
|
||||
function toString(interaction: AppBskyFeedDefs.Interaction): string {
|
||||
return `${interaction.item}|${interaction.event}|${
|
||||
interaction.feedContext || ''
|
||||
}`
|
||||
}
|
||||
|
||||
function toInteraction(str: string): AppBskyFeedDefs.Interaction {
|
||||
const [item, event, feedContext] = str.split('|')
|
||||
return {item, event, feedContext}
|
||||
}
|
||||
+223
-471
@@ -2,7 +2,6 @@ import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {
|
||||
BskyAgent,
|
||||
ChatBskyConvoDefs,
|
||||
ChatBskyConvoGetLog,
|
||||
ChatBskyConvoSendMessage,
|
||||
} from '@atproto-labs/api'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
@@ -19,6 +18,7 @@ export type ConvoParams = {
|
||||
export enum ConvoStatus {
|
||||
Uninitialized = 'uninitialized',
|
||||
Initializing = 'initializing',
|
||||
Resuming = 'resuming',
|
||||
Ready = 'ready',
|
||||
Error = 'error',
|
||||
Backgrounded = 'backgrounded',
|
||||
@@ -27,50 +27,14 @@ export enum ConvoStatus {
|
||||
|
||||
export enum ConvoItemError {
|
||||
HistoryFailed = 'historyFailed',
|
||||
ResumeFailed = 'resumeFailed',
|
||||
PollFailed = 'pollFailed',
|
||||
Network = 'network',
|
||||
}
|
||||
|
||||
export enum ConvoErrorCode {
|
||||
export enum ConvoError {
|
||||
InitFailed = 'initFailed',
|
||||
}
|
||||
|
||||
export type ConvoError = {
|
||||
code: ConvoErrorCode
|
||||
exception?: Error
|
||||
retry: () => void
|
||||
}
|
||||
|
||||
export enum ConvoDispatchEvent {
|
||||
Init = 'init',
|
||||
Ready = 'ready',
|
||||
Resume = 'resume',
|
||||
Background = 'background',
|
||||
Suspend = 'suspend',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export type ConvoDispatch =
|
||||
| {
|
||||
event: ConvoDispatchEvent.Init
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Ready
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Resume
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Background
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Suspend
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Error
|
||||
payload: ConvoError
|
||||
}
|
||||
|
||||
export type ConvoItem =
|
||||
| {
|
||||
type: 'message' | 'pending-message'
|
||||
@@ -169,6 +133,20 @@ export type ConvoState =
|
||||
) => Promise<void>
|
||||
fetchMessageHistory: () => Promise<void>
|
||||
}
|
||||
| {
|
||||
status: ConvoStatus.Resuming
|
||||
items: ConvoItem[]
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
error: undefined
|
||||
sender: AppBskyActorDefs.ProfileViewBasic
|
||||
recipients: AppBskyActorDefs.ProfileViewBasic[]
|
||||
isFetchingHistory: boolean
|
||||
deleteMessage: (messageId: string) => Promise<void>
|
||||
sendMessage: (
|
||||
message: ChatBskyConvoSendMessage.InputSchema['message'],
|
||||
) => Promise<void>
|
||||
fetchMessageHistory: () => Promise<void>
|
||||
}
|
||||
| {
|
||||
status: ConvoStatus.Error
|
||||
items: []
|
||||
@@ -182,12 +160,9 @@ export type ConvoState =
|
||||
fetchMessageHistory: undefined
|
||||
}
|
||||
|
||||
const ACTIVE_POLL_INTERVAL = 1e3
|
||||
const ACTIVE_POLL_INTERVAL = 2e3
|
||||
const BACKGROUND_POLL_INTERVAL = 10e3
|
||||
|
||||
// TODO temporary
|
||||
let DEBUG_ACTIVE_CHAT: string | undefined
|
||||
|
||||
export function isConvoItemMessage(
|
||||
item: ConvoItem,
|
||||
): item is ConvoItem & {type: 'message'} {
|
||||
@@ -200,16 +175,14 @@ export function isConvoItemMessage(
|
||||
}
|
||||
|
||||
export class Convo {
|
||||
private id: string
|
||||
|
||||
private agent: BskyAgent
|
||||
private __tempFromUserDid: string
|
||||
|
||||
private status: ConvoStatus = ConvoStatus.Uninitialized
|
||||
private pollInterval = ACTIVE_POLL_INTERVAL
|
||||
private status: ConvoStatus = ConvoStatus.Uninitialized
|
||||
private error:
|
||||
| {
|
||||
code: ConvoErrorCode
|
||||
code: ConvoError
|
||||
exception?: Error
|
||||
retry: () => void
|
||||
}
|
||||
@@ -217,6 +190,7 @@ export class Convo {
|
||||
private historyCursor: string | undefined | null = undefined
|
||||
private isFetchingHistory = false
|
||||
private eventsCursor: string | undefined = undefined
|
||||
private pollingFailure = false
|
||||
|
||||
private pastMessages: Map<
|
||||
string,
|
||||
@@ -234,9 +208,8 @@ export class Convo {
|
||||
private footerItems: Map<string, ConvoItem> = new Map()
|
||||
private headerItems: Map<string, ConvoItem> = new Map()
|
||||
|
||||
private pendingEventIngestion: Promise<void> | undefined
|
||||
private isProcessingPendingMessages = false
|
||||
private pendingPoll: Promise<void> | undefined
|
||||
private nextPoll: NodeJS.Timeout | undefined
|
||||
|
||||
convoId: string
|
||||
convo: ChatBskyConvoDefs.ConvoView | undefined
|
||||
@@ -245,7 +218,6 @@ export class Convo {
|
||||
snapshot: ConvoState | undefined
|
||||
|
||||
constructor(params: ConvoParams) {
|
||||
this.id = nanoid(3)
|
||||
this.convoId = params.convoId
|
||||
this.agent = params.agent
|
||||
this.__tempFromUserDid = params.__tempFromUserDid
|
||||
@@ -255,14 +227,6 @@ export class Convo {
|
||||
this.sendMessage = this.sendMessage.bind(this)
|
||||
this.deleteMessage = this.deleteMessage.bind(this)
|
||||
this.fetchMessageHistory = this.fetchMessageHistory.bind(this)
|
||||
|
||||
if (DEBUG_ACTIVE_CHAT) {
|
||||
logger.error(`Convo: another chat was already active`, {
|
||||
convoId: this.convoId,
|
||||
})
|
||||
} else {
|
||||
DEBUG_ACTIVE_CHAT = this.convoId
|
||||
}
|
||||
}
|
||||
|
||||
private commit() {
|
||||
@@ -307,6 +271,7 @@ export class Convo {
|
||||
}
|
||||
case ConvoStatus.Suspended:
|
||||
case ConvoStatus.Backgrounded:
|
||||
case ConvoStatus.Resuming:
|
||||
case ConvoStatus.Ready: {
|
||||
return {
|
||||
status: this.status,
|
||||
@@ -352,309 +317,122 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(action: ConvoDispatch) {
|
||||
const prevStatus = this.status
|
||||
async init() {
|
||||
logger.debug('Convo: init', {}, logger.DebugContext.convo)
|
||||
|
||||
switch (this.status) {
|
||||
case ConvoStatus.Uninitialized: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Init: {
|
||||
this.status = ConvoStatus.Initializing
|
||||
this.setup()
|
||||
break
|
||||
}
|
||||
if (
|
||||
this.status === ConvoStatus.Uninitialized ||
|
||||
this.status === ConvoStatus.Error
|
||||
) {
|
||||
try {
|
||||
this.status = ConvoStatus.Initializing
|
||||
this.commit()
|
||||
|
||||
await this.refreshConvo()
|
||||
this.status = ConvoStatus.Ready
|
||||
this.commit()
|
||||
|
||||
await this.fetchMessageHistory()
|
||||
|
||||
this.pollEvents()
|
||||
} catch (e: any) {
|
||||
logger.error('Convo: failed to init')
|
||||
this.error = {
|
||||
exception: e,
|
||||
code: ConvoError.InitFailed,
|
||||
retry: () => {
|
||||
this.error = undefined
|
||||
this.init()
|
||||
},
|
||||
}
|
||||
break
|
||||
this.status = ConvoStatus.Error
|
||||
this.commit()
|
||||
}
|
||||
case ConvoStatus.Initializing: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Ready: {
|
||||
this.status = ConvoStatus.Ready
|
||||
this.pollInterval = ACTIVE_POLL_INTERVAL
|
||||
this.fetchMessageHistory().then(() => {
|
||||
this.restartPoll()
|
||||
})
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Background: {
|
||||
this.status = ConvoStatus.Backgrounded
|
||||
this.pollInterval = BACKGROUND_POLL_INTERVAL
|
||||
this.fetchMessageHistory().then(() => {
|
||||
this.restartPoll()
|
||||
})
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Suspend: {
|
||||
this.status = ConvoStatus.Suspended
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Error: {
|
||||
this.status = ConvoStatus.Error
|
||||
this.error = action.payload
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case ConvoStatus.Ready: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Resume: {
|
||||
this.refreshConvo()
|
||||
this.restartPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Background: {
|
||||
this.status = ConvoStatus.Backgrounded
|
||||
this.pollInterval = BACKGROUND_POLL_INTERVAL
|
||||
this.restartPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Suspend: {
|
||||
this.status = ConvoStatus.Suspended
|
||||
this.cancelNextPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Error: {
|
||||
this.status = ConvoStatus.Error
|
||||
this.error = action.payload
|
||||
this.cancelNextPoll()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case ConvoStatus.Backgrounded: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Resume: {
|
||||
this.status = ConvoStatus.Ready
|
||||
this.pollInterval = ACTIVE_POLL_INTERVAL
|
||||
this.refreshConvo()
|
||||
// TODO truncate history if needed
|
||||
this.restartPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Suspend: {
|
||||
this.status = ConvoStatus.Suspended
|
||||
this.cancelNextPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Error: {
|
||||
this.status = ConvoStatus.Error
|
||||
this.error = action.payload
|
||||
this.cancelNextPoll()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case ConvoStatus.Suspended: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Init: {
|
||||
this.status = ConvoStatus.Ready
|
||||
this.pollInterval = ACTIVE_POLL_INTERVAL
|
||||
this.refreshConvo()
|
||||
// TODO truncate history if needed
|
||||
this.restartPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Resume: {
|
||||
this.status = ConvoStatus.Ready
|
||||
this.pollInterval = ACTIVE_POLL_INTERVAL
|
||||
this.refreshConvo()
|
||||
this.restartPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Error: {
|
||||
this.status = ConvoStatus.Error
|
||||
this.error = action.payload
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case ConvoStatus.Error: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Init: {
|
||||
this.reset()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Resume: {
|
||||
this.reset()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Suspend: {
|
||||
this.status = ConvoStatus.Suspended
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Error: {
|
||||
this.status = ConvoStatus.Error
|
||||
this.error = action.payload
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
} else {
|
||||
logger.warn(`Convo: cannot init from ${this.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Convo: dispatch '${action.event}'`,
|
||||
{
|
||||
id: this.id,
|
||||
prev: prevStatus,
|
||||
next: this.status,
|
||||
},
|
||||
logger.DebugContext.convo,
|
||||
)
|
||||
async resume() {
|
||||
logger.debug('Convo: resume', {}, logger.DebugContext.convo)
|
||||
|
||||
if (
|
||||
this.status === ConvoStatus.Suspended ||
|
||||
this.status === ConvoStatus.Backgrounded
|
||||
) {
|
||||
const fromStatus = this.status
|
||||
|
||||
try {
|
||||
this.status = ConvoStatus.Resuming
|
||||
this.commit()
|
||||
|
||||
await this.refreshConvo()
|
||||
this.status = ConvoStatus.Ready
|
||||
this.commit()
|
||||
|
||||
// throw new Error('UNCOMMENT TO TEST RESUME FAILURE')
|
||||
|
||||
this.pollInterval = ACTIVE_POLL_INTERVAL
|
||||
this.pollEvents()
|
||||
} catch (e) {
|
||||
logger.error('Convo: failed to resume')
|
||||
|
||||
this.footerItems.set(ConvoItemError.ResumeFailed, {
|
||||
type: 'error-recoverable',
|
||||
key: ConvoItemError.ResumeFailed,
|
||||
code: ConvoItemError.ResumeFailed,
|
||||
retry: () => {
|
||||
this.footerItems.delete(ConvoItemError.ResumeFailed)
|
||||
this.resume()
|
||||
},
|
||||
})
|
||||
|
||||
this.status = fromStatus
|
||||
this.commit()
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Convo: cannot resume from ${this.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
async background() {
|
||||
logger.debug('Convo: backgrounded', {}, logger.DebugContext.convo)
|
||||
this.status = ConvoStatus.Backgrounded
|
||||
this.pollInterval = BACKGROUND_POLL_INTERVAL
|
||||
this.commit()
|
||||
}
|
||||
|
||||
private reset() {
|
||||
this.convo = undefined
|
||||
this.sender = undefined
|
||||
this.recipients = undefined
|
||||
this.snapshot = undefined
|
||||
|
||||
this.status = ConvoStatus.Uninitialized
|
||||
this.error = undefined
|
||||
this.historyCursor = undefined
|
||||
this.eventsCursor = undefined
|
||||
|
||||
this.pastMessages = new Map()
|
||||
this.newMessages = new Map()
|
||||
this.pendingMessages = new Map()
|
||||
this.deletedMessages = new Set()
|
||||
this.footerItems = new Map()
|
||||
this.headerItems = new Map()
|
||||
|
||||
this.dispatch({event: ConvoDispatchEvent.Init})
|
||||
}
|
||||
|
||||
private async setup() {
|
||||
try {
|
||||
const {convo, sender, recipients} = await this.fetchConvo()
|
||||
|
||||
this.convo = convo
|
||||
this.sender = sender
|
||||
this.recipients = recipients
|
||||
|
||||
/*
|
||||
* Some validation prior to `Ready` status
|
||||
*/
|
||||
if (!this.convo) {
|
||||
throw new Error('Convo: could not find convo')
|
||||
}
|
||||
if (!this.sender) {
|
||||
throw new Error('Convo: could not find sender in convo')
|
||||
}
|
||||
if (!this.recipients) {
|
||||
throw new Error('Convo: could not find recipients in convo')
|
||||
}
|
||||
|
||||
// await new Promise(y => setTimeout(y, 2000))
|
||||
// throw new Error('UNCOMMENT TO TEST INIT FAILURE')
|
||||
this.dispatch({event: ConvoDispatchEvent.Ready})
|
||||
} catch (e: any) {
|
||||
logger.error('Convo: setup() failed')
|
||||
|
||||
this.dispatch({
|
||||
event: ConvoDispatchEvent.Error,
|
||||
payload: {
|
||||
exception: e,
|
||||
code: ConvoErrorCode.InitFailed,
|
||||
retry: () => {
|
||||
this.reset()
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.dispatch({event: ConvoDispatchEvent.Init})
|
||||
}
|
||||
|
||||
resume() {
|
||||
this.dispatch({event: ConvoDispatchEvent.Resume})
|
||||
}
|
||||
|
||||
background() {
|
||||
this.dispatch({event: ConvoDispatchEvent.Background})
|
||||
}
|
||||
|
||||
suspend() {
|
||||
this.dispatch({event: ConvoDispatchEvent.Suspend})
|
||||
DEBUG_ACTIVE_CHAT = undefined
|
||||
}
|
||||
|
||||
private pendingFetchConvo:
|
||||
| Promise<{
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
sender: AppBskyActorDefs.ProfileViewBasic | undefined
|
||||
recipients: AppBskyActorDefs.ProfileViewBasic[]
|
||||
}>
|
||||
| undefined
|
||||
async fetchConvo() {
|
||||
if (this.pendingFetchConvo) return this.pendingFetchConvo
|
||||
|
||||
this.pendingFetchConvo = new Promise<{
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
sender: AppBskyActorDefs.ProfileViewBasic | undefined
|
||||
recipients: AppBskyActorDefs.ProfileViewBasic[]
|
||||
}>(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await this.agent.api.chat.bsky.convo.getConvo(
|
||||
{
|
||||
convoId: this.convoId,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.__tempFromUserDid,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const convo = response.data.convo
|
||||
|
||||
resolve({
|
||||
convo,
|
||||
sender: convo.members.find(m => m.did === this.__tempFromUserDid),
|
||||
recipients: convo.members.filter(
|
||||
m => m.did !== this.__tempFromUserDid,
|
||||
),
|
||||
})
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
} finally {
|
||||
this.pendingFetchConvo = undefined
|
||||
}
|
||||
})
|
||||
|
||||
return this.pendingFetchConvo
|
||||
async suspend() {
|
||||
logger.debug('Convo: suspended', {}, logger.DebugContext.convo)
|
||||
this.status = ConvoStatus.Suspended
|
||||
this.commit()
|
||||
}
|
||||
|
||||
async refreshConvo() {
|
||||
try {
|
||||
const {convo, sender, recipients} = await this.fetchConvo()
|
||||
// throw new Error('UNCOMMENT TO TEST REFRESH FAILURE')
|
||||
this.convo = convo || this.convo
|
||||
this.sender = sender || this.sender
|
||||
this.recipients = recipients || this.recipients
|
||||
} catch (e: any) {
|
||||
logger.error(`Convo: failed to refresh convo`)
|
||||
|
||||
this.footerItems.set(ConvoItemError.Network, {
|
||||
type: 'error-recoverable',
|
||||
key: ConvoItemError.Network,
|
||||
code: ConvoItemError.Network,
|
||||
retry: () => {
|
||||
this.footerItems.delete(ConvoItemError.Network)
|
||||
this.resume()
|
||||
const response = await this.agent.api.chat.bsky.convo.getConvo(
|
||||
{
|
||||
convoId: this.convoId,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.__tempFromUserDid,
|
||||
},
|
||||
})
|
||||
this.commit()
|
||||
},
|
||||
)
|
||||
this.convo = response.data.convo
|
||||
this.sender = this.convo.members.find(m => m.did === this.__tempFromUserDid)
|
||||
this.recipients = this.convo.members.filter(
|
||||
m => m.did !== this.__tempFromUserDid,
|
||||
)
|
||||
|
||||
/*
|
||||
* Prevent invalid states
|
||||
*/
|
||||
if (!this.sender) {
|
||||
throw new Error('Convo: could not find sender in convo')
|
||||
}
|
||||
if (!this.recipients) {
|
||||
throw new Error('Convo: could not find recipients in convo')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,142 +517,116 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
|
||||
private restartPoll() {
|
||||
this.cancelNextPoll()
|
||||
this.pollLatestEvents()
|
||||
}
|
||||
private async pollEvents() {
|
||||
if (
|
||||
this.status === ConvoStatus.Ready ||
|
||||
this.status === ConvoStatus.Backgrounded
|
||||
) {
|
||||
if (this.pendingEventIngestion) return
|
||||
|
||||
private cancelNextPoll() {
|
||||
if (this.nextPoll) clearTimeout(this.nextPoll)
|
||||
}
|
||||
/*
|
||||
* Represents a failed state, which is retryable.
|
||||
*/
|
||||
if (this.pollingFailure) return
|
||||
|
||||
private pollLatestEvents() {
|
||||
/*
|
||||
* Uncomment to view poll events
|
||||
*/
|
||||
logger.debug('Convo: poll events', {id: this.id}, logger.DebugContext.convo)
|
||||
|
||||
try {
|
||||
this.fetchLatestEvents().then(({events}) => {
|
||||
this.applyLatestEvents(events)
|
||||
})
|
||||
this.nextPoll = setTimeout(() => {
|
||||
this.pollLatestEvents()
|
||||
setTimeout(async () => {
|
||||
this.pendingEventIngestion = this.ingestLatestEvents()
|
||||
await this.pendingEventIngestion
|
||||
this.pendingEventIngestion = undefined
|
||||
this.pollEvents()
|
||||
}, this.pollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
async ingestLatestEvents() {
|
||||
try {
|
||||
// throw new Error('UNCOMMENT TO TEST POLL FAILURE')
|
||||
const response = await this.agent.api.chat.bsky.convo.getLog(
|
||||
{
|
||||
cursor: this.eventsCursor,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.__tempFromUserDid,
|
||||
},
|
||||
},
|
||||
)
|
||||
const {logs} = response.data
|
||||
|
||||
let needsCommit = false
|
||||
|
||||
for (const log of logs) {
|
||||
/*
|
||||
* If there's a rev, we should handle it. If there's not a rev, we don't
|
||||
* know what it is.
|
||||
*/
|
||||
if (typeof log.rev === 'string') {
|
||||
/*
|
||||
* We only care about new events
|
||||
*/
|
||||
if (log.rev > (this.eventsCursor = this.eventsCursor || log.rev)) {
|
||||
/*
|
||||
* Update rev regardless of if it's a log type we care about or not
|
||||
*/
|
||||
this.eventsCursor = log.rev
|
||||
|
||||
/*
|
||||
* This is VERY important. We don't want to insert any messages from
|
||||
* your other chats.
|
||||
*/
|
||||
if (log.convoId !== this.convoId) continue
|
||||
|
||||
if (
|
||||
ChatBskyConvoDefs.isLogCreateMessage(log) &&
|
||||
ChatBskyConvoDefs.isMessageView(log.message)
|
||||
) {
|
||||
if (this.newMessages.has(log.message.id)) {
|
||||
// Trust the log as the source of truth on ordering
|
||||
this.newMessages.delete(log.message.id)
|
||||
}
|
||||
this.newMessages.set(log.message.id, log.message)
|
||||
needsCommit = true
|
||||
} else if (
|
||||
ChatBskyConvoDefs.isLogDeleteMessage(log) &&
|
||||
ChatBskyConvoDefs.isDeletedMessageView(log.message)
|
||||
) {
|
||||
/*
|
||||
* Update if we have this in state. If we don't, don't worry about it.
|
||||
*/
|
||||
if (this.pastMessages.has(log.message.id)) {
|
||||
/*
|
||||
* For now, we remove deleted messages from the thread, if we receive one.
|
||||
*
|
||||
* To support them, it'd look something like this:
|
||||
* this.pastMessages.set(log.message.id, log.message)
|
||||
*/
|
||||
this.pastMessages.delete(log.message.id)
|
||||
this.newMessages.delete(log.message.id)
|
||||
this.deletedMessages.delete(log.message.id)
|
||||
needsCommit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsCommit) {
|
||||
this.commit()
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error('Convo: poll events failed')
|
||||
|
||||
this.cancelNextPoll()
|
||||
|
||||
logger.error('Convo: failed to poll events')
|
||||
this.pollingFailure = true
|
||||
this.footerItems.set(ConvoItemError.PollFailed, {
|
||||
type: 'error-recoverable',
|
||||
key: ConvoItemError.PollFailed,
|
||||
code: ConvoItemError.PollFailed,
|
||||
retry: () => {
|
||||
this.footerItems.delete(ConvoItemError.PollFailed)
|
||||
this.pollingFailure = false
|
||||
this.commit()
|
||||
this.pollLatestEvents()
|
||||
this.pollEvents()
|
||||
},
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private pendingFetchLatestEvents:
|
||||
| Promise<{
|
||||
events: ChatBskyConvoGetLog.OutputSchema['logs']
|
||||
}>
|
||||
| undefined
|
||||
async fetchLatestEvents() {
|
||||
if (this.pendingFetchLatestEvents) return this.pendingFetchLatestEvents
|
||||
|
||||
this.pendingFetchLatestEvents = new Promise<{
|
||||
events: ChatBskyConvoGetLog.OutputSchema['logs']
|
||||
}>(async (resolve, reject) => {
|
||||
try {
|
||||
// throw new Error('UNCOMMENT TO TEST POLL FAILURE')
|
||||
const response = await this.agent.api.chat.bsky.convo.getLog(
|
||||
{
|
||||
cursor: this.eventsCursor,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.__tempFromUserDid,
|
||||
},
|
||||
},
|
||||
)
|
||||
const {logs} = response.data
|
||||
resolve({events: logs})
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
} finally {
|
||||
this.pendingFetchLatestEvents = undefined
|
||||
}
|
||||
})
|
||||
|
||||
return this.pendingFetchLatestEvents
|
||||
}
|
||||
|
||||
private applyLatestEvents(events: ChatBskyConvoGetLog.OutputSchema['logs']) {
|
||||
let needsCommit = false
|
||||
|
||||
for (const ev of events) {
|
||||
/*
|
||||
* If there's a rev, we should handle it. If there's not a rev, we don't
|
||||
* know what it is.
|
||||
*/
|
||||
if (typeof ev.rev === 'string') {
|
||||
/*
|
||||
* We only care about new events
|
||||
*/
|
||||
if (ev.rev > (this.eventsCursor = this.eventsCursor || ev.rev)) {
|
||||
/*
|
||||
* Update rev regardless of if it's a ev type we care about or not
|
||||
*/
|
||||
this.eventsCursor = ev.rev
|
||||
|
||||
/*
|
||||
* This is VERY important. We don't want to insert any messages from
|
||||
* your other chats.
|
||||
*/
|
||||
if (ev.convoId !== this.convoId) continue
|
||||
|
||||
if (
|
||||
ChatBskyConvoDefs.isLogCreateMessage(ev) &&
|
||||
ChatBskyConvoDefs.isMessageView(ev.message)
|
||||
) {
|
||||
if (this.newMessages.has(ev.message.id)) {
|
||||
// Trust the ev as the source of truth on ordering
|
||||
this.newMessages.delete(ev.message.id)
|
||||
}
|
||||
this.newMessages.set(ev.message.id, ev.message)
|
||||
needsCommit = true
|
||||
} else if (
|
||||
ChatBskyConvoDefs.isLogDeleteMessage(ev) &&
|
||||
ChatBskyConvoDefs.isDeletedMessageView(ev.message)
|
||||
) {
|
||||
/*
|
||||
* Update if we have this in state. If we don't, don't worry about it.
|
||||
*/
|
||||
if (this.pastMessages.has(ev.message.id)) {
|
||||
/*
|
||||
* For now, we remove deleted messages from the thread, if we receive one.
|
||||
*
|
||||
* To support them, it'd look something like this:
|
||||
* this.pastMessages.set(ev.message.id, ev.message)
|
||||
*/
|
||||
this.pastMessages.delete(ev.message.id)
|
||||
this.newMessages.delete(ev.message.id)
|
||||
this.deletedMessages.delete(ev.message.id)
|
||||
needsCommit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsCommit) {
|
||||
this.commit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,466 +0,0 @@
|
||||
import {BskyAgent, ChatBskyConvoGetLog} from '@atproto-labs/api'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {
|
||||
MessagesEventBusDispatch,
|
||||
MessagesEventBusDispatchEvent,
|
||||
MessagesEventBusError,
|
||||
MessagesEventBusErrorCode,
|
||||
MessagesEventBusParams,
|
||||
MessagesEventBusState,
|
||||
MessagesEventBusStatus,
|
||||
} from '#/state/messages/events/types'
|
||||
|
||||
const LOGGER_CONTEXT = 'MessagesEventBus'
|
||||
|
||||
const ACTIVE_POLL_INTERVAL = 60e3
|
||||
const BACKGROUND_POLL_INTERVAL = 60e3
|
||||
|
||||
export class MessagesEventBus {
|
||||
private id: string
|
||||
|
||||
private agent: BskyAgent
|
||||
private __tempFromUserDid: string
|
||||
private emitter = new EventEmitter()
|
||||
|
||||
private status: MessagesEventBusStatus = MessagesEventBusStatus.Uninitialized
|
||||
private pollInterval = ACTIVE_POLL_INTERVAL
|
||||
private error: MessagesEventBusError | undefined
|
||||
private latestRev: string | undefined = undefined
|
||||
|
||||
snapshot: MessagesEventBusState | undefined
|
||||
|
||||
constructor(params: MessagesEventBusParams) {
|
||||
this.id = nanoid(3)
|
||||
this.agent = params.agent
|
||||
this.__tempFromUserDid = params.__tempFromUserDid
|
||||
|
||||
this.subscribe = this.subscribe.bind(this)
|
||||
this.getSnapshot = this.getSnapshot.bind(this)
|
||||
this.init = this.init.bind(this)
|
||||
this.suspend = this.suspend.bind(this)
|
||||
this.resume = this.resume.bind(this)
|
||||
this.setPollInterval = this.setPollInterval.bind(this)
|
||||
this.trail = this.trail.bind(this)
|
||||
this.trailConvo = this.trailConvo.bind(this)
|
||||
}
|
||||
|
||||
private commit() {
|
||||
this.snapshot = undefined
|
||||
this.subscribers.forEach(subscriber => subscriber())
|
||||
}
|
||||
|
||||
private subscribers: (() => void)[] = []
|
||||
|
||||
subscribe(subscriber: () => void) {
|
||||
if (this.subscribers.length === 0) this.init()
|
||||
|
||||
this.subscribers.push(subscriber)
|
||||
|
||||
return () => {
|
||||
this.subscribers = this.subscribers.filter(s => s !== subscriber)
|
||||
if (this.subscribers.length === 0) this.suspend()
|
||||
}
|
||||
}
|
||||
|
||||
getSnapshot(): MessagesEventBusState {
|
||||
if (!this.snapshot) this.snapshot = this.generateSnapshot()
|
||||
// logger.debug(`${LOGGER_CONTEXT}: snapshotted`, {}, logger.DebugContext.convo)
|
||||
return this.snapshot
|
||||
}
|
||||
|
||||
private generateSnapshot(): MessagesEventBusState {
|
||||
switch (this.status) {
|
||||
case MessagesEventBusStatus.Initializing: {
|
||||
return {
|
||||
status: MessagesEventBusStatus.Initializing,
|
||||
rev: undefined,
|
||||
error: undefined,
|
||||
setPollInterval: this.setPollInterval,
|
||||
trail: this.trail,
|
||||
trailConvo: this.trailConvo,
|
||||
}
|
||||
}
|
||||
case MessagesEventBusStatus.Ready: {
|
||||
return {
|
||||
status: this.status,
|
||||
rev: this.latestRev!,
|
||||
error: undefined,
|
||||
setPollInterval: this.setPollInterval,
|
||||
trail: this.trail,
|
||||
trailConvo: this.trailConvo,
|
||||
}
|
||||
}
|
||||
case MessagesEventBusStatus.Suspended: {
|
||||
return {
|
||||
status: this.status,
|
||||
rev: this.latestRev,
|
||||
error: undefined,
|
||||
setPollInterval: this.setPollInterval,
|
||||
trail: this.trail,
|
||||
trailConvo: this.trailConvo,
|
||||
}
|
||||
}
|
||||
case MessagesEventBusStatus.Error: {
|
||||
return {
|
||||
status: MessagesEventBusStatus.Error,
|
||||
rev: this.latestRev,
|
||||
error: this.error || {
|
||||
code: MessagesEventBusErrorCode.Unknown,
|
||||
retry: () => {
|
||||
this.init()
|
||||
},
|
||||
},
|
||||
setPollInterval: this.setPollInterval,
|
||||
trail: this.trail,
|
||||
trailConvo: this.trailConvo,
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
status: MessagesEventBusStatus.Uninitialized,
|
||||
rev: undefined,
|
||||
error: undefined,
|
||||
setPollInterval: this.setPollInterval,
|
||||
trail: this.trail,
|
||||
trailConvo: this.trailConvo,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(action: MessagesEventBusDispatch) {
|
||||
const prevStatus = this.status
|
||||
|
||||
switch (this.status) {
|
||||
case MessagesEventBusStatus.Uninitialized: {
|
||||
switch (action.event) {
|
||||
case MessagesEventBusDispatchEvent.Init: {
|
||||
this.status = MessagesEventBusStatus.Initializing
|
||||
this.setup()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case MessagesEventBusStatus.Initializing: {
|
||||
switch (action.event) {
|
||||
case MessagesEventBusDispatchEvent.Ready: {
|
||||
this.status = MessagesEventBusStatus.Ready
|
||||
this.setPollInterval(ACTIVE_POLL_INTERVAL)
|
||||
break
|
||||
}
|
||||
case MessagesEventBusDispatchEvent.Background: {
|
||||
this.status = MessagesEventBusStatus.Backgrounded
|
||||
this.setPollInterval(BACKGROUND_POLL_INTERVAL)
|
||||
break
|
||||
}
|
||||
case MessagesEventBusDispatchEvent.Suspend: {
|
||||
this.status = MessagesEventBusStatus.Suspended
|
||||
break
|
||||
}
|
||||
case MessagesEventBusDispatchEvent.Error: {
|
||||
this.status = MessagesEventBusStatus.Error
|
||||
this.error = action.payload
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case MessagesEventBusStatus.Ready: {
|
||||
switch (action.event) {
|
||||
case MessagesEventBusDispatchEvent.Background: {
|
||||
this.status = MessagesEventBusStatus.Backgrounded
|
||||
this.setPollInterval(BACKGROUND_POLL_INTERVAL)
|
||||
break
|
||||
}
|
||||
case MessagesEventBusDispatchEvent.Suspend: {
|
||||
this.status = MessagesEventBusStatus.Suspended
|
||||
this.stopPoll()
|
||||
break
|
||||
}
|
||||
case MessagesEventBusDispatchEvent.Error: {
|
||||
this.status = MessagesEventBusStatus.Error
|
||||
this.error = action.payload
|
||||
this.stopPoll()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case MessagesEventBusStatus.Backgrounded: {
|
||||
switch (action.event) {
|
||||
case MessagesEventBusDispatchEvent.Resume: {
|
||||
this.status = MessagesEventBusStatus.Ready
|
||||
this.setPollInterval(ACTIVE_POLL_INTERVAL)
|
||||
break
|
||||
}
|
||||
case MessagesEventBusDispatchEvent.Suspend: {
|
||||
this.status = MessagesEventBusStatus.Suspended
|
||||
this.stopPoll()
|
||||
break
|
||||
}
|
||||
case MessagesEventBusDispatchEvent.Error: {
|
||||
this.status = MessagesEventBusStatus.Error
|
||||
this.error = action.payload
|
||||
this.stopPoll()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case MessagesEventBusStatus.Suspended: {
|
||||
switch (action.event) {
|
||||
case MessagesEventBusDispatchEvent.Resume: {
|
||||
this.status = MessagesEventBusStatus.Ready
|
||||
this.setPollInterval(ACTIVE_POLL_INTERVAL)
|
||||
break
|
||||
}
|
||||
case MessagesEventBusDispatchEvent.Background: {
|
||||
this.status = MessagesEventBusStatus.Backgrounded
|
||||
this.setPollInterval(BACKGROUND_POLL_INTERVAL)
|
||||
break
|
||||
}
|
||||
case MessagesEventBusDispatchEvent.Error: {
|
||||
this.status = MessagesEventBusStatus.Error
|
||||
this.error = action.payload
|
||||
this.stopPoll()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case MessagesEventBusStatus.Error: {
|
||||
switch (action.event) {
|
||||
case MessagesEventBusDispatchEvent.Resume:
|
||||
case MessagesEventBusDispatchEvent.Init: {
|
||||
this.status = MessagesEventBusStatus.Initializing
|
||||
this.error = undefined
|
||||
this.latestRev = undefined
|
||||
this.setup()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`${LOGGER_CONTEXT}: dispatch '${action.event}'`,
|
||||
{
|
||||
id: this.id,
|
||||
prev: prevStatus,
|
||||
next: this.status,
|
||||
},
|
||||
logger.DebugContext.convo,
|
||||
)
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
private async setup() {
|
||||
logger.debug(`${LOGGER_CONTEXT}: setup`, {}, logger.DebugContext.convo)
|
||||
|
||||
try {
|
||||
await this.initializeLatestRev()
|
||||
this.dispatch({event: MessagesEventBusDispatchEvent.Ready})
|
||||
} catch (e: any) {
|
||||
logger.error(e, {
|
||||
context: `${LOGGER_CONTEXT}: setup failed`,
|
||||
})
|
||||
|
||||
this.dispatch({
|
||||
event: MessagesEventBusDispatchEvent.Error,
|
||||
payload: {
|
||||
exception: e,
|
||||
code: MessagesEventBusErrorCode.InitFailed,
|
||||
retry: () => {
|
||||
this.init()
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
logger.debug(`${LOGGER_CONTEXT}: init`, {}, logger.DebugContext.convo)
|
||||
this.dispatch({event: MessagesEventBusDispatchEvent.Init})
|
||||
}
|
||||
|
||||
background() {
|
||||
logger.debug(`${LOGGER_CONTEXT}: background`, {}, logger.DebugContext.convo)
|
||||
this.dispatch({event: MessagesEventBusDispatchEvent.Background})
|
||||
}
|
||||
|
||||
suspend() {
|
||||
logger.debug(`${LOGGER_CONTEXT}: suspend`, {}, logger.DebugContext.convo)
|
||||
this.dispatch({event: MessagesEventBusDispatchEvent.Suspend})
|
||||
}
|
||||
|
||||
resume() {
|
||||
logger.debug(`${LOGGER_CONTEXT}: resume`, {}, logger.DebugContext.convo)
|
||||
this.dispatch({event: MessagesEventBusDispatchEvent.Resume})
|
||||
}
|
||||
|
||||
setPollInterval(interval: number) {
|
||||
this.pollInterval = interval
|
||||
this.resetPoll()
|
||||
}
|
||||
|
||||
trail(handler: (events: ChatBskyConvoGetLog.OutputSchema['logs']) => void) {
|
||||
this.emitter.on('events', handler)
|
||||
return () => {
|
||||
this.emitter.off('events', handler)
|
||||
}
|
||||
}
|
||||
|
||||
trailConvo(
|
||||
convoId: string,
|
||||
handler: (events: ChatBskyConvoGetLog.OutputSchema['logs']) => void,
|
||||
) {
|
||||
const handle = (events: ChatBskyConvoGetLog.OutputSchema['logs']) => {
|
||||
const convoEvents = events.filter(ev => {
|
||||
if (typeof ev.convoId === 'string' && ev.convoId === convoId) {
|
||||
return ev.convoId === convoId
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (convoEvents.length > 0) {
|
||||
handler(convoEvents)
|
||||
}
|
||||
}
|
||||
|
||||
this.emitter.on('events', handle)
|
||||
return () => {
|
||||
this.emitter.off('events', handle)
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeLatestRev() {
|
||||
logger.debug(
|
||||
`${LOGGER_CONTEXT}: initialize latest rev`,
|
||||
{},
|
||||
logger.DebugContext.convo,
|
||||
)
|
||||
|
||||
const response = await this.agent.api.chat.bsky.convo.listConvos(
|
||||
{
|
||||
limit: 1,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.__tempFromUserDid,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const {convos} = response.data
|
||||
|
||||
for (const convo of convos) {
|
||||
if (convo.rev > (this.latestRev = this.latestRev || convo.rev)) {
|
||||
this.latestRev = convo.rev
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Polling
|
||||
*/
|
||||
|
||||
private isPolling = false
|
||||
private pollIntervalRef: NodeJS.Timeout | undefined
|
||||
|
||||
private resetPoll() {
|
||||
this.stopPoll()
|
||||
this.startPoll()
|
||||
}
|
||||
|
||||
private startPoll() {
|
||||
if (!this.isPolling) this.poll()
|
||||
|
||||
this.pollIntervalRef = setInterval(() => {
|
||||
if (this.isPolling) return
|
||||
this.poll()
|
||||
}, this.pollInterval)
|
||||
}
|
||||
|
||||
private stopPoll() {
|
||||
if (this.pollIntervalRef) clearInterval(this.pollIntervalRef)
|
||||
}
|
||||
|
||||
private async poll() {
|
||||
if (this.isPolling) return
|
||||
|
||||
this.isPolling = true
|
||||
|
||||
logger.debug(`${LOGGER_CONTEXT}: poll`, {}, logger.DebugContext.convo)
|
||||
|
||||
try {
|
||||
const response = await this.agent.api.chat.bsky.convo.getLog(
|
||||
{
|
||||
cursor: this.latestRev,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.__tempFromUserDid,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const {logs: events} = response.data
|
||||
|
||||
let needsEmit = false
|
||||
let batch: ChatBskyConvoGetLog.OutputSchema['logs'] = []
|
||||
|
||||
for (const ev of events) {
|
||||
/*
|
||||
* If there's a rev, we should handle it. If there's not a rev, we don't
|
||||
* know what it is.
|
||||
*/
|
||||
if (typeof ev.rev === 'string') {
|
||||
/*
|
||||
* We only care about new events
|
||||
*/
|
||||
if (ev.rev > (this.latestRev = this.latestRev || ev.rev)) {
|
||||
/*
|
||||
* Update rev regardless of if it's a ev type we care about or not
|
||||
*/
|
||||
this.latestRev = ev.rev
|
||||
needsEmit = true
|
||||
batch.push(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsEmit) {
|
||||
try {
|
||||
this.emitter.emit('events', batch)
|
||||
} catch (e: any) {
|
||||
logger.error(e, {
|
||||
context: `${LOGGER_CONTEXT}: process latest events`,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error(e, {context: `${LOGGER_CONTEXT}: poll events failed`})
|
||||
|
||||
this.dispatch({
|
||||
event: MessagesEventBusDispatchEvent.Error,
|
||||
payload: {
|
||||
exception: e,
|
||||
code: MessagesEventBusErrorCode.PollFailed,
|
||||
retry: () => {
|
||||
this.init()
|
||||
},
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
this.isPolling = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import React from 'react'
|
||||
import {AppState} from 'react-native'
|
||||
import {BskyAgent} from '@atproto-labs/api'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {MessagesEventBus} from '#/state/messages/events/agent'
|
||||
import {MessagesEventBusState} from '#/state/messages/events/types'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||
import {IS_DEV} from '#/env'
|
||||
|
||||
const MessagesEventBusContext =
|
||||
React.createContext<MessagesEventBusState | null>(null)
|
||||
|
||||
export function useMessagesEventBus() {
|
||||
const ctx = React.useContext(MessagesEventBusContext)
|
||||
if (!ctx) {
|
||||
throw new Error('useChat must be used within a ChatProvider')
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function MessagesEventBusProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const {serviceUrl} = useDmServiceUrlStorage()
|
||||
const {getAgent} = useAgent()
|
||||
const [bus] = React.useState(
|
||||
() =>
|
||||
new MessagesEventBus({
|
||||
agent: new BskyAgent({
|
||||
service: serviceUrl,
|
||||
}),
|
||||
__tempFromUserDid: getAgent().session?.did!,
|
||||
}),
|
||||
)
|
||||
const service = React.useSyncExternalStore(bus.subscribe, bus.getSnapshot)
|
||||
|
||||
if (isWeb && IS_DEV) {
|
||||
// @ts-ignore
|
||||
window.messagesEventBus = service
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleAppStateChange = (nextAppState: string) => {
|
||||
if (nextAppState === 'active') {
|
||||
bus.resume()
|
||||
} else {
|
||||
bus.background()
|
||||
}
|
||||
}
|
||||
|
||||
const sub = AppState.addEventListener('change', handleAppStateChange)
|
||||
|
||||
return () => {
|
||||
sub.remove()
|
||||
}
|
||||
}, [bus])
|
||||
|
||||
return (
|
||||
<MessagesEventBusContext.Provider value={service}>
|
||||
{children}
|
||||
</MessagesEventBusContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import {BskyAgent, ChatBskyConvoGetLog} from '@atproto-labs/api'
|
||||
|
||||
export type MessagesEventBusParams = {
|
||||
agent: BskyAgent
|
||||
__tempFromUserDid: string
|
||||
}
|
||||
|
||||
export enum MessagesEventBusStatus {
|
||||
Uninitialized = 'uninitialized',
|
||||
Initializing = 'initializing',
|
||||
Ready = 'ready',
|
||||
Error = 'error',
|
||||
Backgrounded = 'backgrounded',
|
||||
Suspended = 'suspended',
|
||||
}
|
||||
|
||||
export enum MessagesEventBusDispatchEvent {
|
||||
Init = 'init',
|
||||
Ready = 'ready',
|
||||
Error = 'error',
|
||||
Background = 'background',
|
||||
Suspend = 'suspend',
|
||||
Resume = 'resume',
|
||||
}
|
||||
|
||||
export enum MessagesEventBusErrorCode {
|
||||
Unknown = 'unknown',
|
||||
InitFailed = 'initFailed',
|
||||
PollFailed = 'pollFailed',
|
||||
}
|
||||
|
||||
export type MessagesEventBusError = {
|
||||
code: MessagesEventBusErrorCode
|
||||
exception?: Error
|
||||
retry: () => void
|
||||
}
|
||||
|
||||
export type MessagesEventBusDispatch =
|
||||
| {
|
||||
event: MessagesEventBusDispatchEvent.Init
|
||||
}
|
||||
| {
|
||||
event: MessagesEventBusDispatchEvent.Ready
|
||||
}
|
||||
| {
|
||||
event: MessagesEventBusDispatchEvent.Background
|
||||
}
|
||||
| {
|
||||
event: MessagesEventBusDispatchEvent.Suspend
|
||||
}
|
||||
| {
|
||||
event: MessagesEventBusDispatchEvent.Resume
|
||||
}
|
||||
| {
|
||||
event: MessagesEventBusDispatchEvent.Error
|
||||
payload: MessagesEventBusError
|
||||
}
|
||||
|
||||
export type TrailHandler = (
|
||||
events: ChatBskyConvoGetLog.OutputSchema['logs'],
|
||||
) => void
|
||||
|
||||
export type MessagesEventBusState =
|
||||
| {
|
||||
status: MessagesEventBusStatus.Uninitialized
|
||||
rev: undefined
|
||||
error: undefined
|
||||
setPollInterval: (interval: number) => void
|
||||
trail: (handler: TrailHandler) => () => void
|
||||
trailConvo: (convoId: string, handler: TrailHandler) => () => void
|
||||
}
|
||||
| {
|
||||
status: MessagesEventBusStatus.Initializing
|
||||
rev: undefined
|
||||
error: undefined
|
||||
setPollInterval: (interval: number) => void
|
||||
trail: (handler: TrailHandler) => () => void
|
||||
trailConvo: (convoId: string, handler: TrailHandler) => () => void
|
||||
}
|
||||
| {
|
||||
status: MessagesEventBusStatus.Ready
|
||||
rev: string
|
||||
error: undefined
|
||||
setPollInterval: (interval: number) => void
|
||||
trail: (handler: TrailHandler) => () => void
|
||||
trailConvo: (convoId: string, handler: TrailHandler) => () => void
|
||||
}
|
||||
| {
|
||||
status: MessagesEventBusStatus.Backgrounded
|
||||
rev: string | undefined
|
||||
error: undefined
|
||||
setPollInterval: (interval: number) => void
|
||||
trail: (handler: TrailHandler) => () => void
|
||||
trailConvo: (convoId: string, handler: TrailHandler) => () => void
|
||||
}
|
||||
| {
|
||||
status: MessagesEventBusStatus.Suspended
|
||||
rev: string | undefined
|
||||
error: undefined
|
||||
setPollInterval: (interval: number) => void
|
||||
trail: (handler: TrailHandler) => () => void
|
||||
trailConvo: (convoId: string, handler: TrailHandler) => () => void
|
||||
}
|
||||
| {
|
||||
status: MessagesEventBusStatus.Error
|
||||
rev: string | undefined
|
||||
error: MessagesEventBusError
|
||||
setPollInterval: (interval: number) => void
|
||||
trail: (handler: TrailHandler) => () => void
|
||||
trailConvo: (convoId: string, handler: TrailHandler) => () => void
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {useContext, useState, useSyncExternalStore} from 'react'
|
||||
import {AppState} from 'react-native'
|
||||
import {BskyAgent} from '@atproto-labs/api'
|
||||
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {Convo, ConvoParams, ConvoState} from '#/state/messages/convo'
|
||||
import {useAgent} from '#/state/session'
|
||||
@@ -21,7 +20,6 @@ export function ChatProvider({
|
||||
children,
|
||||
convoId,
|
||||
}: Pick<ConvoParams, 'convoId'> & {children: React.ReactNode}) {
|
||||
const isScreenFocused = useIsFocused()
|
||||
const {serviceUrl} = useDmServiceUrlStorage()
|
||||
const {getAgent} = useAgent()
|
||||
const [convo] = useState(
|
||||
@@ -46,23 +44,5 @@ export function ChatProvider({
|
||||
}, [convo]),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleAppStateChange = (nextAppState: string) => {
|
||||
if (isScreenFocused) {
|
||||
if (nextAppState === 'active') {
|
||||
convo.resume()
|
||||
} else {
|
||||
convo.background()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sub = AppState.addEventListener('change', handleAppStateChange)
|
||||
|
||||
return () => {
|
||||
sub.remove()
|
||||
}
|
||||
}, [convo, isScreenFocused])
|
||||
|
||||
return <ChatContext.Provider value={service}>{children}</ChatContext.Provider>
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export interface EditImageModal {
|
||||
export interface CropImageModal {
|
||||
name: 'crop-image'
|
||||
uri: string
|
||||
dimensions?: {width: number; height: number}
|
||||
onSelect: (img?: RNImage) => void
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,7 @@ export function useActorAutocompleteQuery(
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
prefix = prefix.toLowerCase().trim()
|
||||
if (prefix.endsWith('.')) {
|
||||
// Going from "foo" to "foo." should not clear matches.
|
||||
prefix = prefix.slice(0, -1)
|
||||
}
|
||||
prefix = prefix.toLowerCase()
|
||||
|
||||
return useQuery<AppBskyActorDefs.ProfileViewBasic[]>({
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
|
||||
@@ -70,12 +70,10 @@ export interface FeedPostSliceItem {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
|
||||
feedContext: string | undefined
|
||||
moderation: ModerationDecision
|
||||
}
|
||||
|
||||
export interface FeedPostSlice {
|
||||
_isFeedPostSlice: boolean
|
||||
_reactKey: string
|
||||
rootUri: string
|
||||
isThread: boolean
|
||||
@@ -278,7 +276,6 @@ export function usePostFeedQuery(
|
||||
|
||||
return {
|
||||
_reactKey: slice._reactKey,
|
||||
_isFeedPostSlice: true,
|
||||
rootUri: slice.rootItem.post.uri,
|
||||
isThread:
|
||||
slice.items.length > 1 &&
|
||||
@@ -303,7 +300,6 @@ export function usePostFeedQuery(
|
||||
i === 0 && slice.source
|
||||
? slice.source
|
||||
: item.reason,
|
||||
feedContext: item.feedContext,
|
||||
moderation: moderations[i],
|
||||
}
|
||||
}
|
||||
@@ -511,9 +507,3 @@ export function resetProfilePostsQueries(
|
||||
})
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
export function isFeedPostSlice(v: any): v is FeedPostSlice {
|
||||
return (
|
||||
v && typeof v === 'object' && '_isFeedPostSlice' in v && v._isFeedPostSlice
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
|
||||
import {
|
||||
createGIFDescription,
|
||||
parseAltFromGIFDescription,
|
||||
} from '#/lib/gif-alt-text'
|
||||
import {LikelyType} from '#/lib/link-meta/link-meta'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
@@ -215,25 +211,11 @@ export const ComposePost = observer(function ComposePost({
|
||||
[gallery, track],
|
||||
)
|
||||
|
||||
const isAltTextRequiredAndMissing = useMemo(() => {
|
||||
if (!requireAltTextEnabled) return false
|
||||
|
||||
if (gallery.needsAltText) return true
|
||||
if (extGif) {
|
||||
if (!extLink?.meta?.description) return true
|
||||
|
||||
const parsedAlt = parseAltFromGIFDescription(extLink.meta.description)
|
||||
if (!parsedAlt.isPreferred) return true
|
||||
}
|
||||
return false
|
||||
}, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled])
|
||||
|
||||
const onPressPublish = async () => {
|
||||
if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isAltTextRequiredAndMissing) {
|
||||
if (requireAltTextEnabled && gallery.needsAltText) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -316,8 +298,10 @@ export const ComposePost = observer(function ComposePost({
|
||||
}
|
||||
|
||||
const canPost = useMemo(
|
||||
() => graphemeLength <= MAX_GRAPHEME_LENGTH && !isAltTextRequiredAndMissing,
|
||||
[graphemeLength, isAltTextRequiredAndMissing],
|
||||
() =>
|
||||
graphemeLength <= MAX_GRAPHEME_LENGTH &&
|
||||
(!requireAltTextEnabled || !gallery.needsAltText),
|
||||
[graphemeLength, requireAltTextEnabled, gallery.needsAltText],
|
||||
)
|
||||
const selectTextInputPlaceholder = replyTo
|
||||
? _(msg`Write your reply`)
|
||||
@@ -344,7 +328,7 @@ export const ComposePost = observer(function ComposePost({
|
||||
image: gif.media_formats.preview.url,
|
||||
likelyType: LikelyType.HTML,
|
||||
title: gif.content_description,
|
||||
description: createGIFDescription(gif.content_description),
|
||||
description: '',
|
||||
},
|
||||
})
|
||||
setExtGif(gif)
|
||||
@@ -359,11 +343,11 @@ export const ComposePost = observer(function ComposePost({
|
||||
? {
|
||||
...ext,
|
||||
meta: {
|
||||
...ext.meta,
|
||||
description: createGIFDescription(
|
||||
ext.meta.title ?? '',
|
||||
altText,
|
||||
),
|
||||
...ext?.meta,
|
||||
description:
|
||||
altText.trim().length === 0
|
||||
? ''
|
||||
: `Alt text: ${altText.trim()}`,
|
||||
},
|
||||
}
|
||||
: ext,
|
||||
@@ -449,7 +433,7 @@ export const ComposePost = observer(function ComposePost({
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
{isAltTextRequiredAndMissing && (
|
||||
{requireAltTextEnabled && gallery.needsAltText && (
|
||||
<View style={[styles.reminderLine, pal.viewLight]}>
|
||||
<View style={styles.errorIcon}>
|
||||
<FontAwesomeIcon
|
||||
|
||||
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {ExternalEmbedDraft} from '#/lib/api'
|
||||
import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants'
|
||||
import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
|
||||
import {
|
||||
EmbedPlayerParams,
|
||||
parseEmbedPlayerFromUrl,
|
||||
@@ -60,7 +59,6 @@ export function GifAltText({
|
||||
|
||||
if (!gif || !params) return null
|
||||
|
||||
const parsedAlt = parseAltFromGIFDescription(link.description)
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
@@ -82,7 +80,7 @@ export function GifAltText({
|
||||
a.align_center,
|
||||
{backgroundColor: 'rgba(0, 0, 0, 0.75)'},
|
||||
]}>
|
||||
{parsedAlt.isPreferred ? (
|
||||
{link.description ? (
|
||||
<Check size="xs" fill={t.palette.white} style={a.ml_xs} />
|
||||
) : (
|
||||
<Plus size="sm" fill={t.palette.white} />
|
||||
@@ -104,7 +102,7 @@ export function GifAltText({
|
||||
onSubmit={onPressSubmit}
|
||||
link={link}
|
||||
params={params}
|
||||
initialValue={parsedAlt.isPreferred ? parsedAlt.alt : ''}
|
||||
initalValue={link.description.replace('Alt text: ', '')}
|
||||
key={link.uri}
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
@@ -116,16 +114,15 @@ function AltTextInner({
|
||||
onSubmit,
|
||||
link,
|
||||
params,
|
||||
initialValue: initalValue,
|
||||
initalValue,
|
||||
}: {
|
||||
onSubmit: (text: string) => void
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
params: EmbedPlayerParams
|
||||
initialValue: string
|
||||
initalValue: string
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const [altText, setAltText] = useState(initalValue)
|
||||
const control = Dialog.useDialogContext()
|
||||
|
||||
const onPressSubmit = useCallback(() => {
|
||||
onSubmit(altText)
|
||||
@@ -150,11 +147,6 @@ function AltTextInner({
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
autoFocus
|
||||
onKeyPress={({nativeEvent}) => {
|
||||
if (nativeEvent.key === 'Escape') {
|
||||
control.close()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
|
||||
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
|
||||
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
|
||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
@@ -52,7 +51,6 @@ export function FeedPage({
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {screen, track} = useAnalytics()
|
||||
const headerOffset = useHeaderOffset()
|
||||
const feedFeedback = useFeedFeedback(feed, hasSession)
|
||||
const scrollElRef = React.useRef<ListMethods>(null)
|
||||
const [hasNew, setHasNew] = React.useState(false)
|
||||
const gate = useGate()
|
||||
@@ -115,22 +113,20 @@ export function FeedPage({
|
||||
return (
|
||||
<View testID={testID} style={s.h100pct}>
|
||||
<MainScrollProvider>
|
||||
<FeedFeedbackProvider value={feedFeedback}>
|
||||
<Feed
|
||||
testID={testID ? `${testID}-feed` : undefined}
|
||||
enabled={isPageFocused}
|
||||
feed={feed}
|
||||
feedParams={feedParams}
|
||||
pollInterval={POLL_FREQ}
|
||||
disablePoll={hasNew}
|
||||
scrollElRef={scrollElRef}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
onHasNew={setHasNew}
|
||||
renderEmptyState={renderEmptyState}
|
||||
renderEndOfFeed={renderEndOfFeed}
|
||||
headerOffset={headerOffset}
|
||||
/>
|
||||
</FeedFeedbackProvider>
|
||||
<Feed
|
||||
testID={testID ? `${testID}-feed` : undefined}
|
||||
enabled={isPageFocused}
|
||||
feed={feed}
|
||||
feedParams={feedParams}
|
||||
pollInterval={POLL_FREQ}
|
||||
disablePoll={hasNew}
|
||||
scrollElRef={scrollElRef}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
onHasNew={setHasNew}
|
||||
renderEmptyState={renderEmptyState}
|
||||
renderEndOfFeed={renderEndOfFeed}
|
||||
headerOffset={headerOffset}
|
||||
/>
|
||||
</MainScrollProvider>
|
||||
{(isScrolledDown || adjustedHasNew) && (
|
||||
<LoadLatestBtn
|
||||
|
||||
@@ -6,11 +6,12 @@ import {RichText} from '#/components/RichText'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {s} from 'lib/styles'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg, Plural} from '@lingui/macro'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {
|
||||
usePinFeedMutation,
|
||||
@@ -264,11 +265,10 @@ export function FeedSourceCardLoaded({
|
||||
|
||||
{showLikes && feed.type === 'feed' ? (
|
||||
<Text type="sm-medium" style={[pal.text, pal.textLight]}>
|
||||
<Plural
|
||||
value={feed.likeCount || 0}
|
||||
one="Liked by # user"
|
||||
other="Liked by # users"
|
||||
/>
|
||||
<Trans>
|
||||
Liked by {feed.likeCount || 0}{' '}
|
||||
{pluralize(feed.likeCount || 0, 'user')}
|
||||
</Trans>
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
|
||||
@@ -78,7 +78,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
|
||||
|
||||
try {
|
||||
await saveImageToMediaLibrary({uri})
|
||||
Toast.show(_(msg`Saved to your camera roll`))
|
||||
Toast.show(_(msg`Saved to your camera roll.`))
|
||||
} catch (e: any) {
|
||||
Toast.show(_(msg`Failed to save image: ${String(e)}`))
|
||||
}
|
||||
|
||||
@@ -507,9 +507,7 @@ function CustomHandleForm({
|
||||
<Text type="xl-medium" style={[s.white, s.textCenter]}>
|
||||
{canSave
|
||||
? _(msg`Update to ${handle}`)
|
||||
: isDNSForm
|
||||
? _(msg`Verify DNS Record`)
|
||||
: _(msg`Verify Text File`)}
|
||||
: _(msg`Verify ${isDNSForm ? 'DNS Record' : 'Text File'}`)}
|
||||
</Text>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -84,26 +84,26 @@ export function Component({}: {}) {
|
||||
<ScrollView style={[pal.view]} keyboardShouldPersistTaps="handled">
|
||||
<View style={[styles.titleContainer, pal.view]}>
|
||||
<Text type="title-xl" style={[s.textCenter, pal.text]}>
|
||||
<Trans>
|
||||
Delete Account{' '}
|
||||
<Text type="title-xl" style={[pal.text, s.bold]}>
|
||||
"
|
||||
</Text>
|
||||
<Text
|
||||
type="title-xl"
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
isMobile ? styles.titleMobile : styles.titleDesktop,
|
||||
pal.text,
|
||||
s.bold,
|
||||
]}>
|
||||
{currentAccount?.handle}
|
||||
</Text>
|
||||
<Text type="title-xl" style={[pal.text, s.bold]}>
|
||||
"
|
||||
</Text>
|
||||
</Trans>
|
||||
<Trans>Delete Account</Trans>
|
||||
</Text>
|
||||
<View style={[pal.view, s.flexRow]}>
|
||||
<Text type="title-xl" style={[pal.text, s.bold]}>
|
||||
{' "'}
|
||||
</Text>
|
||||
<Text
|
||||
type="title-xl"
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
isMobile ? styles.titleMobile : styles.titleDesktop,
|
||||
pal.text,
|
||||
s.bold,
|
||||
]}>
|
||||
{currentAccount?.handle}
|
||||
</Text>
|
||||
<Text type="title-xl" style={[pal.text, s.bold]}>
|
||||
{'"'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{!isEmailSent ? (
|
||||
<>
|
||||
|
||||
@@ -131,10 +131,10 @@ export function Component({
|
||||
) : (
|
||||
<View>
|
||||
<Text style={[pal.textLight]}>
|
||||
<Text type="md-bold" style={[pal.textLight, s.mr5]}>
|
||||
<Trans>Not Applicable.</Trans>
|
||||
</Text>
|
||||
<Trans>
|
||||
<Text type="md-bold" style={[pal.textLight]}>
|
||||
Not Applicable.
|
||||
</Text>{' '}
|
||||
This warning is only available for posts with media attached.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
@@ -14,13 +14,11 @@ import {Dimensions} from 'lib/media/types'
|
||||
import {getDataUriSize} from 'lib/media/util'
|
||||
import {gradients, s} from 'lib/styles'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {calculateDimensions} from './cropImageUtil'
|
||||
|
||||
enum AspectRatio {
|
||||
Square = 'square',
|
||||
Wide = 'wide',
|
||||
Tall = 'tall',
|
||||
Custom = 'custom',
|
||||
}
|
||||
|
||||
const DIMS: Record<string, Dimensions> = {
|
||||
@@ -33,24 +31,17 @@ export const snapPoints = ['0%']
|
||||
|
||||
export function Component({
|
||||
uri,
|
||||
dimensions,
|
||||
onSelect,
|
||||
}: {
|
||||
uri: string
|
||||
dimensions?: Dimensions
|
||||
onSelect: (img?: RNImage) => void
|
||||
}) {
|
||||
const {closeModal} = useModalControls()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const defaultAspectStyle = dimensions
|
||||
? AspectRatio.Custom
|
||||
: AspectRatio.Square
|
||||
const [as, setAs] = React.useState<AspectRatio>(defaultAspectStyle)
|
||||
const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square)
|
||||
const [scale, setScale] = React.useState<number>(1)
|
||||
const editorRef = React.useRef<ImageEditor>(null)
|
||||
const imageEditorWidth = dimensions ? dimensions.width : DIMS[as].width
|
||||
const imageEditorHeight = dimensions ? dimensions.height : DIMS[as].height
|
||||
|
||||
const doSetAs = (v: AspectRatio) => () => setAs(v)
|
||||
|
||||
@@ -66,8 +57,8 @@ export function Component({
|
||||
path: dataUri,
|
||||
mime: 'image/jpeg',
|
||||
size: getDataUriSize(dataUri),
|
||||
width: imageEditorWidth,
|
||||
height: imageEditorHeight,
|
||||
width: DIMS[as].width,
|
||||
height: DIMS[as].height,
|
||||
})
|
||||
} else {
|
||||
onSelect(undefined)
|
||||
@@ -82,18 +73,7 @@ export function Component({
|
||||
cropperStyle = styles.cropperWide
|
||||
} else if (as === AspectRatio.Tall) {
|
||||
cropperStyle = styles.cropperTall
|
||||
} else if (as === AspectRatio.Custom) {
|
||||
const cropperDimensions = calculateDimensions(
|
||||
550,
|
||||
imageEditorHeight,
|
||||
imageEditorWidth,
|
||||
)
|
||||
cropperStyle = {
|
||||
width: cropperDimensions.width,
|
||||
height: cropperDimensions.height,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={[styles.cropper, pal.borderDark, cropperStyle]}>
|
||||
@@ -101,8 +81,8 @@ export function Component({
|
||||
ref={editorRef}
|
||||
style={styles.imageEditor}
|
||||
image={uri}
|
||||
width={imageEditorWidth}
|
||||
height={imageEditorHeight}
|
||||
width={DIMS[as].width}
|
||||
height={DIMS[as].height}
|
||||
scale={scale}
|
||||
border={0}
|
||||
/>
|
||||
@@ -117,40 +97,36 @@ export function Component({
|
||||
maximumValue={3}
|
||||
containerStyle={styles.slider}
|
||||
/>
|
||||
{as === AspectRatio.Custom ? null : (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Wide)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Wide`)}
|
||||
accessibilityHint={_(msg`Sets image aspect ratio to wide`)}>
|
||||
<RectWideIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Wide ? s.blue3 : pal.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Tall)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Tall`)}
|
||||
accessibilityHint={_(msg`Sets image aspect ratio to tall`)}>
|
||||
<RectTallIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Tall ? s.blue3 : pal.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Square)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Square`)}
|
||||
accessibilityHint={_(msg`Sets image aspect ratio to square`)}>
|
||||
<SquareIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Square ? s.blue3 : pal.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Wide)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Wide`)}
|
||||
accessibilityHint={_(msg`Sets image aspect ratio to wide`)}>
|
||||
<RectWideIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Wide ? s.blue3 : pal.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Tall)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Tall`)}
|
||||
accessibilityHint={_(msg`Sets image aspect ratio to tall`)}>
|
||||
<RectTallIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Tall ? s.blue3 : pal.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={doSetAs(AspectRatio.Square)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Square`)}
|
||||
accessibilityHint={_(msg`Sets image aspect ratio to square`)}>
|
||||
<SquareIcon
|
||||
size={24}
|
||||
style={as === AspectRatio.Square ? s.blue3 : pal.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.btns}>
|
||||
<TouchableOpacity
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export const calculateDimensions = (
|
||||
maxWidth: number,
|
||||
originalHeight: number,
|
||||
originalWidth: number,
|
||||
) => {
|
||||
const aspectRatio = originalWidth / originalHeight
|
||||
const newHeight = maxWidth / aspectRatio
|
||||
const newWidth = maxWidth
|
||||
return {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
FontAwesomeIconStyle,
|
||||
Props,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, plural, Trans} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -33,6 +33,7 @@ import {HeartIconSolid} from 'lib/icons'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {niceDate} from 'lib/strings/time'
|
||||
import {colors, s} from 'lib/styles'
|
||||
import {isWeb} from 'platform/detection'
|
||||
@@ -175,7 +176,6 @@ let FeedItem = ({
|
||||
return null
|
||||
}
|
||||
|
||||
let formattedCount = authors.length > 1 ? formatCount(authors.length - 1) : ''
|
||||
return (
|
||||
<Link
|
||||
testID={`feedItem-by-${item.notification.author.handle}`}
|
||||
@@ -236,10 +236,8 @@ let FeedItem = ({
|
||||
<Trans>and</Trans>{' '}
|
||||
</Text>
|
||||
<Text style={[pal.text, s.bold]}>
|
||||
{plural(authors.length - 1, {
|
||||
one: `${formattedCount} other`,
|
||||
other: `${formattedCount} others`,
|
||||
})}
|
||||
{formatCount(authors.length - 1)}{' '}
|
||||
{pluralize(authors.length - 1, 'other')}
|
||||
</Text>
|
||||
</>
|
||||
) : undefined}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||
@@ -24,7 +24,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {countLines} from 'lib/strings/helpers'
|
||||
import {countLines, pluralize} from 'lib/strings/helpers'
|
||||
import {niceDate} from 'lib/strings/time'
|
||||
import {s} from 'lib/styles'
|
||||
import {isWeb} from 'platform/detection'
|
||||
@@ -336,11 +336,7 @@ let PostThreadItemLoaded = ({
|
||||
<Text type="xl-bold" style={pal.text}>
|
||||
{formatCount(post.repostCount)}
|
||||
</Text>{' '}
|
||||
<Plural
|
||||
value={post.repostCount}
|
||||
one="repost"
|
||||
other="reposts"
|
||||
/>
|
||||
{pluralize(post.repostCount, 'repost')}
|
||||
</Text>
|
||||
</Link>
|
||||
) : null}
|
||||
@@ -356,7 +352,7 @@ let PostThreadItemLoaded = ({
|
||||
<Text type="xl-bold" style={pal.text}>
|
||||
{formatCount(post.likeCount)}
|
||||
</Text>{' '}
|
||||
<Plural value={post.likeCount} one="like" other="likes" />
|
||||
{pluralize(post.likeCount, 'like')}
|
||||
</Text>
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {listenPostCreated} from '#/state/events'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {
|
||||
FeedDescriptor,
|
||||
@@ -89,7 +88,6 @@ let Feed = ({
|
||||
const queryClient = useQueryClient()
|
||||
const {currentAccount} = useSession()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
const feedFeedback = useFeedFeedbackContext()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const checkForNewRef = React.useRef<(() => void) | null>(null)
|
||||
const lastFetchRef = React.useRef<number>(Date.now())
|
||||
@@ -355,7 +353,6 @@ let Feed = ({
|
||||
}
|
||||
initialNumToRender={initialNumToRender}
|
||||
windowSize={11}
|
||||
onItemSeen={feedFeedback.onItemSeen}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -16,7 +16,6 @@ import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {isReasonFeedSource, ReasonFeedSource} from 'lib/api/feed/types'
|
||||
import {MAX_POST_LINES} from 'lib/constants'
|
||||
@@ -46,7 +45,6 @@ export function FeedItem({
|
||||
post,
|
||||
record,
|
||||
reason,
|
||||
feedContext,
|
||||
moderation,
|
||||
isThreadChild,
|
||||
isThreadLastChild,
|
||||
@@ -55,7 +53,6 @@ export function FeedItem({
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
|
||||
feedContext: string | undefined
|
||||
moderation: ModerationDecision
|
||||
isThreadChild?: boolean
|
||||
isThreadLastChild?: boolean
|
||||
@@ -81,7 +78,6 @@ export function FeedItem({
|
||||
post={postShadowed}
|
||||
record={record}
|
||||
reason={reason}
|
||||
feedContext={feedContext}
|
||||
richText={richText}
|
||||
moderation={moderation}
|
||||
isThreadChild={isThreadChild}
|
||||
@@ -97,7 +93,6 @@ let FeedItemInner = ({
|
||||
post,
|
||||
record,
|
||||
reason,
|
||||
feedContext,
|
||||
richText,
|
||||
moderation,
|
||||
isThreadChild,
|
||||
@@ -107,7 +102,6 @@ let FeedItemInner = ({
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
record: AppBskyFeedPost.Record
|
||||
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
|
||||
feedContext: string | undefined
|
||||
richText: RichTextAPI
|
||||
moderation: ModerationDecision
|
||||
isThreadChild?: boolean
|
||||
@@ -122,7 +116,6 @@ let FeedItemInner = ({
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey)
|
||||
}, [post.uri, post.author])
|
||||
const {sendInteraction} = useFeedFeedbackContext()
|
||||
|
||||
const replyAuthorDid = useMemo(() => {
|
||||
if (!record?.reply) {
|
||||
@@ -133,11 +126,6 @@ let FeedItemInner = ({
|
||||
}, [record?.reply])
|
||||
|
||||
const onPressReply = React.useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionReply',
|
||||
feedContext,
|
||||
})
|
||||
openComposer({
|
||||
replyTo: {
|
||||
uri: post.uri,
|
||||
@@ -148,40 +136,11 @@ let FeedItemInner = ({
|
||||
moderation,
|
||||
},
|
||||
})
|
||||
}, [post, record, openComposer, moderation, sendInteraction, feedContext])
|
||||
|
||||
const onOpenAuthor = React.useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughAuthor',
|
||||
feedContext,
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
|
||||
const onOpenReposter = React.useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughReposter',
|
||||
feedContext,
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
|
||||
const onOpenEmbed = React.useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughEmbed',
|
||||
feedContext,
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
}, [post, record, openComposer, moderation])
|
||||
|
||||
const onBeforePress = React.useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughItem',
|
||||
feedContext,
|
||||
})
|
||||
precacheProfile(queryClient, post.author)
|
||||
}, [queryClient, post, sendInteraction, feedContext])
|
||||
}, [queryClient, post.author])
|
||||
|
||||
const outerStyles = [
|
||||
styles.outer,
|
||||
@@ -248,8 +207,7 @@ let FeedItemInner = ({
|
||||
msg`Reposted by ${sanitizeDisplayName(
|
||||
reason.by.displayName || reason.by.handle,
|
||||
)}`,
|
||||
)}
|
||||
onBeforePress={onOpenReposter}>
|
||||
)}>
|
||||
<FontAwesomeIcon
|
||||
icon="retweet"
|
||||
style={{
|
||||
@@ -277,7 +235,6 @@ let FeedItemInner = ({
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
href={makeProfileLink(reason.by)}
|
||||
onBeforePress={onOpenReposter}
|
||||
/>
|
||||
</ProfileHoverCard>
|
||||
</Trans>
|
||||
@@ -294,7 +251,6 @@ let FeedItemInner = ({
|
||||
profile={post.author}
|
||||
moderation={moderation.ui('avatar')}
|
||||
type={post.author.associated?.labeler ? 'labeler' : 'user'}
|
||||
onBeforePress={onOpenAuthor}
|
||||
/>
|
||||
{isThreadParent && (
|
||||
<View
|
||||
@@ -316,7 +272,6 @@ let FeedItemInner = ({
|
||||
authorHasWarning={!!post.author.labels?.length}
|
||||
timestamp={post.indexedAt}
|
||||
postHref={href}
|
||||
onOpenAuthor={onOpenAuthor}
|
||||
/>
|
||||
{!isThreadChild && replyAuthorDid !== '' && (
|
||||
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
|
||||
@@ -353,7 +308,6 @@ let FeedItemInner = ({
|
||||
richText={richText}
|
||||
postEmbed={post.embed}
|
||||
postAuthor={post.author}
|
||||
onOpenEmbed={onOpenEmbed}
|
||||
/>
|
||||
<PostCtrls
|
||||
post={post}
|
||||
@@ -361,7 +315,6 @@ let FeedItemInner = ({
|
||||
richText={richText}
|
||||
onPressReply={onPressReply}
|
||||
logContext="FeedItem"
|
||||
feedContext={feedContext}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -375,13 +328,11 @@ let PostContent = ({
|
||||
richText,
|
||||
postEmbed,
|
||||
postAuthor,
|
||||
onOpenEmbed,
|
||||
}: {
|
||||
moderation: ModerationDecision
|
||||
richText: RichTextAPI
|
||||
postEmbed: AppBskyFeedDefs.PostView['embed']
|
||||
postAuthor: AppBskyFeedDefs.PostView['author']
|
||||
onOpenEmbed: () => void
|
||||
}): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
@@ -422,11 +373,7 @@ let PostContent = ({
|
||||
) : undefined}
|
||||
{postEmbed ? (
|
||||
<View style={[a.pb_sm]}>
|
||||
<PostEmbeds
|
||||
embed={postEmbed}
|
||||
moderation={moderation}
|
||||
onOpen={onOpenEmbed}
|
||||
/>
|
||||
<PostEmbeds embed={postEmbed} moderation={moderation} />
|
||||
</View>
|
||||
) : null}
|
||||
</ContentHider>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React, {memo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import Svg, {Circle, Line} from 'react-native-svg'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {FeedPostSlice} from '#/state/queries/post-feed'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import Svg, {Circle, Line} from 'react-native-svg'
|
||||
import {FeedItem} from './FeedItem'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
|
||||
if (slice.isThread && slice.items.length > 3) {
|
||||
@@ -21,7 +20,6 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
|
||||
post={slice.items[0].post}
|
||||
record={slice.items[0].record}
|
||||
reason={slice.items[0].reason}
|
||||
feedContext={slice.items[0].feedContext}
|
||||
moderation={slice.items[0].moderation}
|
||||
isThreadParent={isThreadParentAt(slice.items, 0)}
|
||||
isThreadChild={isThreadChildAt(slice.items, 0)}
|
||||
@@ -31,7 +29,6 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
|
||||
post={slice.items[1].post}
|
||||
record={slice.items[1].record}
|
||||
reason={slice.items[1].reason}
|
||||
feedContext={slice.items[1].feedContext}
|
||||
moderation={slice.items[1].moderation}
|
||||
isThreadParent={isThreadParentAt(slice.items, 1)}
|
||||
isThreadChild={isThreadChildAt(slice.items, 1)}
|
||||
@@ -42,7 +39,6 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
|
||||
post={slice.items[last].post}
|
||||
record={slice.items[last].record}
|
||||
reason={slice.items[last].reason}
|
||||
feedContext={slice.items[last].feedContext}
|
||||
moderation={slice.items[last].moderation}
|
||||
isThreadParent={isThreadParentAt(slice.items, last)}
|
||||
isThreadChild={isThreadChildAt(slice.items, last)}
|
||||
@@ -60,7 +56,6 @@ let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
|
||||
post={slice.items[i].post}
|
||||
record={slice.items[i].record}
|
||||
reason={slice.items[i].reason}
|
||||
feedContext={slice.items[i].feedContext}
|
||||
moderation={slice.items[i].moderation}
|
||||
isThreadParent={isThreadParentAt(slice.items, i)}
|
||||
isThreadChild={isThreadChildAt(slice.items, i)}
|
||||
|
||||
@@ -6,16 +6,16 @@ import {
|
||||
AppBskyGraphDefs,
|
||||
AtUri,
|
||||
} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {makeListLink, makeProfileLink} from '#/lib/routes/links'
|
||||
import {colors} from '#/lib/styles'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {makeProfileLink, makeListLink} from '#/lib/routes/links'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
|
||||
import {colors} from '#/lib/styles'
|
||||
|
||||
export function WhoCanReply({
|
||||
post,
|
||||
@@ -143,7 +143,6 @@ function Rule({
|
||||
<Trans>
|
||||
users followed by{' '}
|
||||
<TextLink
|
||||
type="sm"
|
||||
href={makeProfileLink(post.author)}
|
||||
text={`@${post.author.handle}`}
|
||||
style={pal.link}
|
||||
@@ -158,7 +157,6 @@ function Rule({
|
||||
return (
|
||||
<Trans>
|
||||
<TextLink
|
||||
type="sm"
|
||||
href={makeListLink(listUrip.hostname, listUrip.rkey)}
|
||||
text={list.name}
|
||||
style={pal.link}
|
||||
|
||||
@@ -220,7 +220,6 @@ export const TextLink = memo(function TextLink({
|
||||
)
|
||||
},
|
||||
[
|
||||
onBeforePress,
|
||||
onPress,
|
||||
closeModal,
|
||||
openModal,
|
||||
@@ -230,6 +229,7 @@ export const TextLink = memo(function TextLink({
|
||||
disableMismatchWarning,
|
||||
navigationAction,
|
||||
openLink,
|
||||
onBeforePress,
|
||||
],
|
||||
)
|
||||
const hrefAttrs = useMemo(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {memo} from 'react'
|
||||
import {FlatListProps, RefreshControl, ViewToken} from 'react-native'
|
||||
import {FlatListProps, RefreshControl} from 'react-native'
|
||||
import {runOnJS, useSharedValue} from 'react-native-reanimated'
|
||||
|
||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||
@@ -23,7 +23,6 @@ export type ListProps<ItemT> = Omit<
|
||||
headerOffset?: number
|
||||
refreshing?: boolean
|
||||
onRefresh?: () => void
|
||||
onItemSeen?: (item: ItemT) => void
|
||||
containWeb?: boolean
|
||||
}
|
||||
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
|
||||
@@ -35,7 +34,6 @@ function ListImpl<ItemT>(
|
||||
onScrolledDownChange,
|
||||
refreshing,
|
||||
onRefresh,
|
||||
onItemSeen,
|
||||
headerOffset,
|
||||
style,
|
||||
...props
|
||||
@@ -75,25 +73,6 @@ function ListImpl<ItemT>(
|
||||
},
|
||||
})
|
||||
|
||||
const [onViewableItemsChanged, viewabilityConfig] = React.useMemo(() => {
|
||||
if (!onItemSeen) {
|
||||
return [undefined, undefined]
|
||||
}
|
||||
return [
|
||||
(info: {viewableItems: Array<ViewToken>; changed: Array<ViewToken>}) => {
|
||||
for (const item of info.changed) {
|
||||
if (item.isViewable) {
|
||||
onItemSeen(item.item)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
itemVisiblePercentThreshold: 40,
|
||||
minimumViewTime: 2e3,
|
||||
},
|
||||
]
|
||||
}, [onItemSeen])
|
||||
|
||||
let refreshControl
|
||||
if (refreshing !== undefined || onRefresh !== undefined) {
|
||||
refreshControl = (
|
||||
@@ -123,8 +102,6 @@ function ListImpl<ItemT>(
|
||||
refreshControl={refreshControl}
|
||||
onScroll={scrollHandler}
|
||||
scrollEventThrottle={1}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
viewabilityConfig={viewabilityConfig}
|
||||
style={style}
|
||||
ref={ref}
|
||||
/>
|
||||
|
||||
+32
-119
@@ -20,17 +20,11 @@ export type ListProps<ItemT> = Omit<
|
||||
headerOffset?: number
|
||||
refreshing?: boolean
|
||||
onRefresh?: () => void
|
||||
onItemSeen?: (item: ItemT) => void
|
||||
desktopFixedHeight: any // TODO: Better types.
|
||||
containWeb?: boolean
|
||||
}
|
||||
export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
|
||||
|
||||
const ON_ITEM_SEEN_WAIT_DURATION = 2e3 // post must be "seen" 2 seconds before capturing
|
||||
const ON_ITEM_SEEN_INTERSECTION_OPTS = {
|
||||
rootMargin: '-200px 0px -200px 0px',
|
||||
} // post must be 200px visible to be "seen"
|
||||
|
||||
function ListImpl<ItemT>(
|
||||
{
|
||||
ListHeaderComponent,
|
||||
@@ -49,7 +43,6 @@ function ListImpl<ItemT>(
|
||||
onRefresh: _unsupportedOnRefresh,
|
||||
onScrolledDownChange,
|
||||
onContentSizeChange,
|
||||
onItemSeen,
|
||||
renderItem,
|
||||
extraData,
|
||||
style,
|
||||
@@ -99,24 +92,12 @@ function ListImpl<ItemT>(
|
||||
if (!element) return
|
||||
|
||||
return {
|
||||
get scrollWidth() {
|
||||
return element.scrollWidth
|
||||
},
|
||||
get scrollHeight() {
|
||||
return element.scrollHeight
|
||||
},
|
||||
get clientWidth() {
|
||||
return element.clientWidth
|
||||
},
|
||||
get clientHeight() {
|
||||
return element.clientHeight
|
||||
},
|
||||
get scrollY() {
|
||||
return element.scrollTop
|
||||
},
|
||||
get scrollX() {
|
||||
return element.scrollLeft
|
||||
},
|
||||
scrollWidth: element.scrollWidth,
|
||||
scrollHeight: element.scrollHeight,
|
||||
clientWidth: element.clientWidth,
|
||||
clientHeight: element.clientHeight,
|
||||
scrollY: element.scrollTop,
|
||||
scrollX: element.scrollLeft,
|
||||
scrollTo(options?: ScrollToOptions) {
|
||||
element.scrollTo(options)
|
||||
},
|
||||
@@ -132,24 +113,12 @@ function ListImpl<ItemT>(
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
get scrollWidth() {
|
||||
return document.documentElement.scrollWidth
|
||||
},
|
||||
get scrollHeight() {
|
||||
return document.documentElement.scrollHeight
|
||||
},
|
||||
get clientWidth() {
|
||||
return window.innerWidth
|
||||
},
|
||||
get clientHeight() {
|
||||
return window.innerHeight
|
||||
},
|
||||
get scrollY() {
|
||||
return window.scrollY
|
||||
},
|
||||
get scrollX() {
|
||||
return window.scrollX
|
||||
},
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
scrollHeight: document.documentElement.scrollHeight,
|
||||
clientWidth: window.innerWidth,
|
||||
clientHeight: window.innerHeight,
|
||||
scrollY: window.scrollY,
|
||||
scrollX: window.scrollX,
|
||||
scrollTo(options: ScrollToOptions) {
|
||||
window.scrollTo(options)
|
||||
},
|
||||
@@ -166,7 +135,7 @@ function ListImpl<ItemT>(
|
||||
}
|
||||
}, [containWeb])
|
||||
|
||||
const nativeRef = React.useRef<HTMLDivElement>(null)
|
||||
const nativeRef = React.useRef(null)
|
||||
React.useImperativeHandle(
|
||||
ref,
|
||||
() =>
|
||||
@@ -288,15 +257,9 @@ function ListImpl<ItemT>(
|
||||
return (
|
||||
<View
|
||||
{...props}
|
||||
style={[
|
||||
style,
|
||||
containWeb && {
|
||||
flex: 1,
|
||||
// @ts-expect-error web only
|
||||
'overflow-y': 'scroll',
|
||||
},
|
||||
]}
|
||||
ref={nativeRef as any}>
|
||||
// @ts-ignore web only
|
||||
style={[style, containWeb && {flex: 1, 'overflow-y': 'scroll'}]}
|
||||
ref={nativeRef}>
|
||||
<Visibility
|
||||
onVisibleChange={setIsInsideVisibleTree}
|
||||
style={
|
||||
@@ -314,34 +277,30 @@ function ListImpl<ItemT>(
|
||||
pal.border,
|
||||
]}>
|
||||
<Visibility
|
||||
root={containWeb ? nativeRef : null}
|
||||
root={containWeb ? nativeRef.current : null}
|
||||
onVisibleChange={handleAboveTheFoldVisibleChange}
|
||||
style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
|
||||
/>
|
||||
{onStartReached && (
|
||||
<Visibility
|
||||
root={containWeb ? nativeRef : null}
|
||||
root={containWeb ? nativeRef.current : null}
|
||||
onVisibleChange={onHeadVisibilityChange}
|
||||
topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
|
||||
/>
|
||||
)}
|
||||
{header}
|
||||
{(data as Array<ItemT>).map((item, index) => {
|
||||
const key = keyExtractor!(item, index)
|
||||
return (
|
||||
<Row<ItemT>
|
||||
key={key}
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
extraData={extraData}
|
||||
onItemSeen={onItemSeen}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{(data as Array<ItemT>).map((item, index) => (
|
||||
<Row<ItemT>
|
||||
key={keyExtractor!(item, index)}
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
extraData={extraData}
|
||||
/>
|
||||
))}
|
||||
{onEndReached && (
|
||||
<Visibility
|
||||
root={containWeb ? nativeRef : null}
|
||||
root={containWeb ? nativeRef.current : null}
|
||||
onVisibleChange={onTailVisibilityChange}
|
||||
bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
|
||||
/>
|
||||
@@ -383,7 +342,6 @@ let Row = function RowImpl<ItemT>({
|
||||
index,
|
||||
renderItem,
|
||||
extraData: _unused,
|
||||
onItemSeen,
|
||||
}: {
|
||||
item: ItemT
|
||||
index: number
|
||||
@@ -392,57 +350,12 @@ let Row = function RowImpl<ItemT>({
|
||||
| undefined
|
||||
| ((data: {index: number; item: any; separators: any}) => React.ReactNode)
|
||||
extraData: any
|
||||
onItemSeen: ((item: any) => void) | undefined
|
||||
}): React.ReactNode {
|
||||
const rowRef = React.useRef(null)
|
||||
const intersectionTimeout = React.useRef<NodeJS.Timer | undefined>(undefined)
|
||||
|
||||
const handleIntersection = useNonReactiveCallback(
|
||||
(entries: IntersectionObserverEntry[]) => {
|
||||
batchedUpdates(() => {
|
||||
if (!onItemSeen) {
|
||||
return
|
||||
}
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
if (!intersectionTimeout.current) {
|
||||
intersectionTimeout.current = setTimeout(() => {
|
||||
intersectionTimeout.current = undefined
|
||||
onItemSeen!(item)
|
||||
}, ON_ITEM_SEEN_WAIT_DURATION)
|
||||
}
|
||||
} else {
|
||||
if (intersectionTimeout.current) {
|
||||
clearTimeout(intersectionTimeout.current)
|
||||
intersectionTimeout.current = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!onItemSeen) {
|
||||
return
|
||||
}
|
||||
const observer = new IntersectionObserver(
|
||||
handleIntersection,
|
||||
ON_ITEM_SEEN_INTERSECTION_OPTS,
|
||||
)
|
||||
const row: Element | null = rowRef.current!
|
||||
observer.observe(row)
|
||||
return () => {
|
||||
observer.unobserve(row)
|
||||
}
|
||||
}, [handleIntersection, onItemSeen])
|
||||
|
||||
if (!renderItem) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.row} ref={rowRef}>
|
||||
<View style={styles.row}>
|
||||
{renderItem({item, index, separators: null as any})}
|
||||
</View>
|
||||
)
|
||||
@@ -450,13 +363,13 @@ let Row = function RowImpl<ItemT>({
|
||||
Row = React.memo(Row)
|
||||
|
||||
let Visibility = ({
|
||||
root,
|
||||
root = null,
|
||||
topMargin = '0px',
|
||||
bottomMargin = '0px',
|
||||
onVisibleChange,
|
||||
style,
|
||||
}: {
|
||||
root?: React.RefObject<HTMLDivElement> | null
|
||||
root?: Element | null
|
||||
topMargin?: string
|
||||
bottomMargin?: string
|
||||
onVisibleChange: (isVisible: boolean) => void
|
||||
@@ -480,7 +393,7 @@ let Visibility = ({
|
||||
|
||||
React.useEffect(() => {
|
||||
const observer = new IntersectionObserver(handleIntersection, {
|
||||
root: root?.current ?? null,
|
||||
root,
|
||||
rootMargin: `${topMargin} 0px ${bottomMargin} 0px`,
|
||||
})
|
||||
const tail: Element | null = tailRef.current!
|
||||
|
||||
@@ -28,7 +28,6 @@ interface PostMetaOpts {
|
||||
avatarSize?: number
|
||||
displayNameType?: TypographyVariant
|
||||
displayNameStyle?: StyleProp<TextStyle>
|
||||
onOpenAuthor?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
@@ -44,12 +43,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
: undefined
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const onOpenAuthor = opts.onOpenAuthor
|
||||
const onBeforePressAuthor = useCallback(() => {
|
||||
precacheProfile(queryClient, opts.author)
|
||||
onOpenAuthor?.()
|
||||
}, [queryClient, opts.author, onOpenAuthor])
|
||||
const onBeforePressPost = useCallback(() => {
|
||||
const onBeforePress = useCallback(() => {
|
||||
precacheProfile(queryClient, opts.author)
|
||||
}, [queryClient, opts.author])
|
||||
|
||||
@@ -83,7 +77,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
</>
|
||||
}
|
||||
href={profileLink}
|
||||
onBeforePress={onBeforePressAuthor}
|
||||
onBeforePress={onBeforePress}
|
||||
onPointerEnter={onPointerEnter}
|
||||
/>
|
||||
<TextLinkOnWebOnly
|
||||
@@ -92,7 +86,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
style={[pal.textLight, {flexShrink: 4}]}
|
||||
text={'\xa0' + sanitizeHandle(handle, '@')}
|
||||
href={profileLink}
|
||||
onBeforePress={onBeforePressAuthor}
|
||||
onBeforePress={onBeforePress}
|
||||
onPointerEnter={onPointerEnter}
|
||||
anchorNoUnderline
|
||||
/>
|
||||
@@ -118,7 +112,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
title={niceDate(opts.timestamp)}
|
||||
accessibilityHint=""
|
||||
href={opts.postHref}
|
||||
onBeforePress={onBeforePressPost}
|
||||
onBeforePress={onBeforePress}
|
||||
/>
|
||||
)}
|
||||
</TimeElapsed>
|
||||
|
||||
@@ -8,7 +8,6 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {
|
||||
useCameraPermission,
|
||||
@@ -50,7 +49,6 @@ interface EditableUserAvatarProps extends BaseUserAvatarProps {
|
||||
|
||||
interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
|
||||
moderation?: ModerationUI
|
||||
onBeforePress?: () => void
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
}
|
||||
|
||||
@@ -284,21 +282,15 @@ let EditableUserAvatar = ({
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const croppedImage = await openCropper({
|
||||
mediaType: 'photo',
|
||||
cropperCircleOverlay: true,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
path: item.path,
|
||||
})
|
||||
const croppedImage = await openCropper({
|
||||
mediaType: 'photo',
|
||||
cropperCircleOverlay: true,
|
||||
height: item.height,
|
||||
width: item.width,
|
||||
path: item.path,
|
||||
})
|
||||
|
||||
onSelectNewAvatar(croppedImage)
|
||||
} catch (e: any) {
|
||||
if (!String(e).includes('Canceled')) {
|
||||
logger.error('Failed to crop banner', {error: e})
|
||||
}
|
||||
}
|
||||
onSelectNewAvatar(croppedImage)
|
||||
}, [onSelectNewAvatar, requestPhotoAccessIfNeeded])
|
||||
|
||||
const onRemoveAvatar = React.useCallback(() => {
|
||||
@@ -383,16 +375,14 @@ export {EditableUserAvatar}
|
||||
let PreviewableUserAvatar = ({
|
||||
moderation,
|
||||
profile,
|
||||
onBeforePress,
|
||||
...rest
|
||||
}: PreviewableUserAvatarProps): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
onBeforePress?.()
|
||||
precacheProfile(queryClient, profile)
|
||||
}, [profile, queryClient, onBeforePress])
|
||||
}, [profile, queryClient])
|
||||
|
||||
return (
|
||||
<ProfileHoverCard did={profile.did}>
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {Image} from 'expo-image'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {Image} from 'expo-image'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {
|
||||
useCameraPermission,
|
||||
usePhotoLibraryPermission,
|
||||
} from 'lib/hooks/usePermissions'
|
||||
import {colors} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {useTheme as useAlfTheme, tokens} from '#/alf'
|
||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||
import {
|
||||
usePhotoLibraryPermission,
|
||||
useCameraPermission,
|
||||
} from 'lib/hooks/usePermissions'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isAndroid, isNative} from 'platform/detection'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {EventStopper} from 'view/com/util/EventStopper'
|
||||
import {tokens, useTheme as useAlfTheme} from '#/alf'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {
|
||||
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
|
||||
Camera_Stroke2_Corner0_Rounded as Camera,
|
||||
} from '#/components/icons/Camera'
|
||||
import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||
|
||||
export function UserBanner({
|
||||
type,
|
||||
@@ -65,20 +64,14 @@ export function UserBanner({
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
onSelectNewBanner?.(
|
||||
await openCropper({
|
||||
mediaType: 'photo',
|
||||
path: items[0].path,
|
||||
width: 3000,
|
||||
height: 1000,
|
||||
}),
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (!String(e).includes('Canceled')) {
|
||||
logger.error('Failed to crop banner', {error: e})
|
||||
}
|
||||
}
|
||||
onSelectNewBanner?.(
|
||||
await openCropper({
|
||||
mediaType: 'photo',
|
||||
path: items[0].path,
|
||||
width: 3000,
|
||||
height: 1000,
|
||||
}),
|
||||
)
|
||||
}, [onSelectNewBanner, requestPhotoAccessIfNeeded])
|
||||
|
||||
const onRemoveBanner = React.useCallback(() => {
|
||||
|
||||
@@ -18,7 +18,6 @@ import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
|
||||
@@ -37,10 +36,6 @@ import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons
|
||||
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
|
||||
import {
|
||||
EmojiSad_Stroke2_Corner0_Rounded as EmojiSad,
|
||||
EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile,
|
||||
} from '#/components/icons/Emoji'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||
@@ -58,7 +53,6 @@ let PostDropdownBtn = ({
|
||||
postAuthor,
|
||||
postCid,
|
||||
postUri,
|
||||
postFeedContext,
|
||||
record,
|
||||
richText,
|
||||
style,
|
||||
@@ -69,7 +63,6 @@ let PostDropdownBtn = ({
|
||||
postAuthor: AppBskyActorDefs.ProfileViewBasic
|
||||
postCid: string
|
||||
postUri: string
|
||||
postFeedContext: string | undefined
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
@@ -88,7 +81,6 @@ let PostDropdownBtn = ({
|
||||
const postDeleteMutation = usePostDeleteMutation()
|
||||
const hiddenPosts = useHiddenPosts()
|
||||
const {hidePost} = useHiddenPostsApi()
|
||||
const feedFeedback = useFeedFeedbackContext()
|
||||
const openLink = useOpenLink()
|
||||
const navigation = useNavigation()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
@@ -191,24 +183,6 @@ let PostDropdownBtn = ({
|
||||
shareUrl(url)
|
||||
}, [href])
|
||||
|
||||
const onPressShowMore = React.useCallback(() => {
|
||||
feedFeedback.sendInteraction({
|
||||
event: 'app.bsky.feed.defs#requestMore',
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
})
|
||||
Toast.show('Feedback sent!')
|
||||
}, [feedFeedback, postUri, postFeedContext])
|
||||
|
||||
const onPressShowLess = React.useCallback(() => {
|
||||
feedFeedback.sendInteraction({
|
||||
event: 'app.bsky.feed.defs#requestLess',
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
})
|
||||
Toast.show('Feedback sent!')
|
||||
}, [feedFeedback, postUri, postFeedContext])
|
||||
|
||||
const canEmbed = isWeb && gtMobile && !hideInPWI
|
||||
|
||||
return (
|
||||
@@ -288,32 +262,10 @@ let PostDropdownBtn = ({
|
||||
)}
|
||||
</Menu.Group>
|
||||
|
||||
{hasSession && feedFeedback.enabled && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownShowMoreBtn"
|
||||
label={_(msg`Show more like this`)}
|
||||
onPress={onPressShowMore}>
|
||||
<Menu.ItemText>{_(msg`Show more like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSmile} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownShowLessBtn"
|
||||
label={_(msg`Show less like this`)}
|
||||
onPress={onPressShowLess}>
|
||||
<Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSad} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteThreadBtn"
|
||||
@@ -356,6 +308,7 @@ let PostDropdownBtn = ({
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Group>
|
||||
{!isAuthor && (
|
||||
<Menu.Item
|
||||
|
||||
@@ -12,18 +12,18 @@ import {
|
||||
AtUri,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, plural} from '@lingui/macro'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
|
||||
import {CommentBottomArrow, HeartIcon, HeartIconSolid} from '#/lib/icons'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {pluralize} from '#/lib/strings/helpers'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {s} from '#/lib/styles'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
usePostLikeMutationQueue,
|
||||
@@ -44,7 +44,6 @@ let PostCtrls = ({
|
||||
post,
|
||||
record,
|
||||
richText,
|
||||
feedContext,
|
||||
style,
|
||||
onPressReply,
|
||||
logContext,
|
||||
@@ -53,7 +52,6 @@ let PostCtrls = ({
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
feedContext?: string | undefined
|
||||
style?: StyleProp<ViewStyle>
|
||||
onPressReply: () => void
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
|
||||
@@ -69,7 +67,6 @@ let PostCtrls = ({
|
||||
)
|
||||
const requireAuth = useRequireAuth()
|
||||
const loggedOutWarningPromptControl = useDialogControl()
|
||||
const {sendInteraction} = useFeedFeedbackContext()
|
||||
const playHaptic = useHaptics()
|
||||
|
||||
const shouldShowLoggedOutWarning = React.useMemo(() => {
|
||||
@@ -89,11 +86,6 @@ let PostCtrls = ({
|
||||
try {
|
||||
if (!post.viewer?.like) {
|
||||
playHaptic()
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionLike',
|
||||
feedContext,
|
||||
})
|
||||
await queueLike()
|
||||
} else {
|
||||
await queueUnlike()
|
||||
@@ -103,26 +95,13 @@ let PostCtrls = ({
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}, [
|
||||
playHaptic,
|
||||
post.uri,
|
||||
post.viewer?.like,
|
||||
queueLike,
|
||||
queueUnlike,
|
||||
sendInteraction,
|
||||
feedContext,
|
||||
])
|
||||
}, [playHaptic, post.viewer?.like, queueLike, queueUnlike])
|
||||
|
||||
const onRepost = useCallback(async () => {
|
||||
closeModal()
|
||||
try {
|
||||
if (!post.viewer?.repost) {
|
||||
playHaptic()
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionRepost',
|
||||
feedContext,
|
||||
})
|
||||
await queueRepost()
|
||||
} else {
|
||||
await queueUnrepost()
|
||||
@@ -132,24 +111,10 @@ let PostCtrls = ({
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}, [
|
||||
closeModal,
|
||||
post.uri,
|
||||
post.viewer?.repost,
|
||||
playHaptic,
|
||||
queueRepost,
|
||||
queueUnrepost,
|
||||
sendInteraction,
|
||||
feedContext,
|
||||
])
|
||||
}, [closeModal, post.viewer?.repost, playHaptic, queueRepost, queueUnrepost])
|
||||
|
||||
const onQuote = useCallback(() => {
|
||||
closeModal()
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionQuote',
|
||||
feedContext,
|
||||
})
|
||||
openComposer({
|
||||
quote: {
|
||||
uri: post.uri,
|
||||
@@ -169,8 +134,6 @@ let PostCtrls = ({
|
||||
post.indexedAt,
|
||||
record.text,
|
||||
playHaptic,
|
||||
sendInteraction,
|
||||
feedContext,
|
||||
])
|
||||
|
||||
const onShare = useCallback(() => {
|
||||
@@ -178,12 +141,7 @@ let PostCtrls = ({
|
||||
const href = makeProfileLink(post.author, 'post', urip.rkey)
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionShare',
|
||||
feedContext,
|
||||
})
|
||||
}, [post.uri, post.author, sendInteraction, feedContext])
|
||||
}, [post.uri, post.author])
|
||||
|
||||
return (
|
||||
<View style={[styles.ctrls, style]}>
|
||||
@@ -201,10 +159,9 @@ let PostCtrls = ({
|
||||
}
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={plural(post.replyCount || 0, {
|
||||
one: 'Reply (# reply)',
|
||||
other: 'Reply (# replies)',
|
||||
})}
|
||||
accessibilityLabel={`Reply (${post.replyCount} ${
|
||||
post.replyCount === 1 ? 'reply' : 'replies'
|
||||
})`}
|
||||
accessibilityHint=""
|
||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
|
||||
<CommentBottomArrow
|
||||
@@ -236,17 +193,9 @@ let PostCtrls = ({
|
||||
requireAuth(() => onPressToggleLike())
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
post.viewer?.like
|
||||
? plural(post.likeCount || 0, {
|
||||
one: 'Unlike (# like)',
|
||||
other: 'Unlike (# likes)',
|
||||
})
|
||||
: plural(post.likeCount || 0, {
|
||||
one: 'Like (# like)',
|
||||
other: 'Like (# likes)',
|
||||
})
|
||||
}
|
||||
accessibilityLabel={`${
|
||||
post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`)
|
||||
} (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`}
|
||||
accessibilityHint=""
|
||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
|
||||
{post.viewer?.like ? (
|
||||
@@ -311,7 +260,6 @@ let PostCtrls = ({
|
||||
postAuthor={post.author}
|
||||
postCid={post.cid}
|
||||
postUri={post.uri}
|
||||
postFeedContext={feedContext}
|
||||
record={record}
|
||||
richText={richText}
|
||||
style={styles.btnPad}
|
||||
|
||||
@@ -4,10 +4,11 @@ import {RepostIcon} from 'lib/icons'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {Text} from '../text/Text'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useRequireAuth} from '#/state/session'
|
||||
import {msg, plural} from '@lingui/macro'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
interface Props {
|
||||
@@ -58,7 +59,7 @@ let RepostButton = ({
|
||||
isReposted
|
||||
? _(msg`Undo repost`)
|
||||
: _(msg({message: 'Repost', context: 'action'}))
|
||||
} (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`}
|
||||
} (${repostCount} ${pluralize(repostCount || 0, 'repost')})`}
|
||||
accessibilityHint=""
|
||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
|
||||
<RepostIcon
|
||||
|
||||
@@ -19,12 +19,10 @@ import {Text} from '../text/Text'
|
||||
|
||||
export const ExternalLinkEmbed = ({
|
||||
link,
|
||||
onOpen,
|
||||
style,
|
||||
hideAlt,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
hideAlt?: boolean
|
||||
}) => {
|
||||
@@ -46,7 +44,7 @@ export const ExternalLinkEmbed = ({
|
||||
|
||||
return (
|
||||
<View style={[a.flex_col, a.rounded_sm, a.overflow_hidden, a.mt_sm]}>
|
||||
<LinkWrapper link={link} onOpen={onOpen} style={style}>
|
||||
<LinkWrapper link={link} style={style}>
|
||||
{link.thumb && !embedPlayerParams ? (
|
||||
<Image
|
||||
style={{
|
||||
@@ -99,12 +97,10 @@ export const ExternalLinkEmbed = ({
|
||||
|
||||
function LinkWrapper({
|
||||
link,
|
||||
onOpen,
|
||||
style,
|
||||
children,
|
||||
}: {
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
@@ -129,7 +125,6 @@ function LinkWrapper({
|
||||
style,
|
||||
]}
|
||||
hoverStyle={t.atoms.border_contrast_high}
|
||||
onBeforePress={onOpen}
|
||||
onLongPress={onShareExternal}>
|
||||
{children}
|
||||
</Link>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {EmbedPlayerParams} from 'lib/strings/embed-player'
|
||||
import {useAutoplayDisabled} from 'state/preferences'
|
||||
@@ -117,11 +116,6 @@ export function GifEmbed({
|
||||
playerRef.current?.toggleAsync()
|
||||
}, [])
|
||||
|
||||
const parsedAlt = React.useMemo(
|
||||
() => parseAltFromGIFDescription(link.description),
|
||||
[link],
|
||||
)
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[a.rounded_sm, a.overflow_hidden, a.mt_sm, {maxWidth: '100%'}]}>
|
||||
@@ -146,10 +140,12 @@ export function GifEmbed({
|
||||
onPlayerStateChange={onPlayerStateChange}
|
||||
ref={playerRef}
|
||||
accessibilityHint={_(msg`Animated GIF`)}
|
||||
accessibilityLabel={parsedAlt.alt}
|
||||
accessibilityLabel={link.description.replace('Alt text: ', '')}
|
||||
/>
|
||||
|
||||
{!hideAlt && parsedAlt.isPreferred && <AltText text={parsedAlt.alt} />}
|
||||
{!hideAlt && link.description.startsWith('Alt text: ') && (
|
||||
<AltText text={link.description.replace('Alt text: ', '')} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -178,7 +174,7 @@ function AltText({text}: {text: string}) {
|
||||
<Prompt.TitleText>
|
||||
<Trans>Alt Text</Trans>
|
||||
</Prompt.TitleText>
|
||||
<Prompt.DescriptionText selectable>{text}</Prompt.DescriptionText>
|
||||
<Prompt.DescriptionText>{text}</Prompt.DescriptionText>
|
||||
<Prompt.Actions>
|
||||
<Prompt.Action
|
||||
onPress={control.close}
|
||||
|
||||
@@ -42,11 +42,9 @@ import {PostEmbeds} from '.'
|
||||
|
||||
export function MaybeQuoteEmbed({
|
||||
embed,
|
||||
onOpen,
|
||||
style,
|
||||
}: {
|
||||
embed: AppBskyEmbedRecord.View
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
@@ -59,7 +57,6 @@ export function MaybeQuoteEmbed({
|
||||
<QuoteEmbedModerated
|
||||
viewRecord={embed.record}
|
||||
postRecord={embed.record.value}
|
||||
onOpen={onOpen}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
@@ -88,12 +85,10 @@ export function MaybeQuoteEmbed({
|
||||
function QuoteEmbedModerated({
|
||||
viewRecord,
|
||||
postRecord,
|
||||
onOpen,
|
||||
style,
|
||||
}: {
|
||||
viewRecord: AppBskyEmbedRecord.ViewRecord
|
||||
postRecord: AppBskyFeedPost.Record
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
@@ -113,25 +108,16 @@ function QuoteEmbedModerated({
|
||||
embeds: viewRecord.embeds,
|
||||
}
|
||||
|
||||
return (
|
||||
<QuoteEmbed
|
||||
quote={quote}
|
||||
moderation={moderation}
|
||||
onOpen={onOpen}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
return <QuoteEmbed quote={quote} moderation={moderation} style={style} />
|
||||
}
|
||||
|
||||
export function QuoteEmbed({
|
||||
quote,
|
||||
moderation,
|
||||
onOpen,
|
||||
style,
|
||||
}: {
|
||||
quote: ComposerOptsQuote
|
||||
moderation?: ModerationDecision
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
@@ -164,8 +150,7 @@ export function QuoteEmbed({
|
||||
|
||||
const onBeforePress = React.useCallback(() => {
|
||||
precacheProfile(queryClient, quote.author)
|
||||
onOpen?.()
|
||||
}, [queryClient, quote.author, onOpen])
|
||||
}, [queryClient, quote.author])
|
||||
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentList')}>
|
||||
|
||||
@@ -38,12 +38,10 @@ type Embed =
|
||||
export function PostEmbeds({
|
||||
embed,
|
||||
moderation,
|
||||
onOpen,
|
||||
style,
|
||||
}: {
|
||||
embed?: Embed
|
||||
moderation?: ModerationDecision
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
@@ -54,12 +52,8 @@ export function PostEmbeds({
|
||||
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
|
||||
return (
|
||||
<View style={style}>
|
||||
<PostEmbeds
|
||||
embed={embed.media}
|
||||
moderation={moderation}
|
||||
onOpen={onOpen}
|
||||
/>
|
||||
<MaybeQuoteEmbed embed={embed.record} onOpen={onOpen} />
|
||||
<PostEmbeds embed={embed.media} moderation={moderation} />
|
||||
<MaybeQuoteEmbed embed={embed.record} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -86,7 +80,7 @@ export function PostEmbeds({
|
||||
|
||||
// quote post
|
||||
// =
|
||||
return <MaybeQuoteEmbed embed={embed} style={style} onOpen={onOpen} />
|
||||
return <MaybeQuoteEmbed embed={embed} style={style} />
|
||||
}
|
||||
|
||||
// image embed
|
||||
@@ -157,7 +151,7 @@ export function PostEmbeds({
|
||||
const link = embed.external
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<ExternalLinkEmbed link={link} onOpen={onOpen} style={style} />
|
||||
<ExternalLinkEmbed link={link} style={style} />
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -804,7 +804,6 @@ function MockPostFeedItem({
|
||||
record={post.record as AppBskyFeedPost.Record}
|
||||
moderation={moderation}
|
||||
reason={undefined}
|
||||
feedContext={''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -740,7 +740,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 16,
|
||||
paddingHorizontal: 18,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import debounce from 'lodash.debounce'
|
||||
import {Trans, msg, Plural} from '@lingui/macro'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
@@ -27,6 +27,7 @@ function RepliesThresholdInput({
|
||||
initialValue: number
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
|
||||
const preValue = React.useRef(initialValue)
|
||||
@@ -63,12 +64,13 @@ function RepliesThresholdInput({
|
||||
thumbTintColor={colors.blue3}
|
||||
/>
|
||||
<Text type="xs" style={pal.text}>
|
||||
<Plural
|
||||
value={value}
|
||||
_0="Show all replies"
|
||||
one="Show replies with at least # like"
|
||||
other="Show replies with at least # likes"
|
||||
/>
|
||||
{value === 0
|
||||
? _(msg`Show all replies`)
|
||||
: _(
|
||||
msg`Show replies with at least ${value} ${
|
||||
value > 1 ? `likes` : `like`
|
||||
}`,
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {useCallback, useMemo} from 'react'
|
||||
import {Pressable, StyleSheet, View} from 'react-native'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
@@ -10,7 +10,6 @@ import {HITSLOP_20} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||
import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
|
||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
@@ -36,6 +35,7 @@ import {makeCustomFeedLink} from 'lib/routes/links'
|
||||
import {CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {makeRecordUri} from 'lib/strings/url-helpers'
|
||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||
import {s} from 'lib/styles'
|
||||
@@ -463,8 +463,6 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
|
||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const isScreenFocused = useIsFocused()
|
||||
const {hasSession} = useSession()
|
||||
const feedFeedback = useFeedFeedback(feed, hasSession)
|
||||
|
||||
const onScrollToTop = useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({
|
||||
@@ -492,19 +490,17 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
|
||||
|
||||
return (
|
||||
<View>
|
||||
<FeedFeedbackProvider value={feedFeedback}>
|
||||
<Feed
|
||||
enabled={isFocused}
|
||||
feed={feed}
|
||||
pollInterval={60e3}
|
||||
disablePoll={hasNew}
|
||||
scrollElRef={scrollElRef}
|
||||
onHasNew={setHasNew}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
renderEmptyState={renderPostsEmpty}
|
||||
headerOffset={headerHeight}
|
||||
/>
|
||||
</FeedFeedbackProvider>
|
||||
<Feed
|
||||
enabled={isFocused}
|
||||
feed={feed}
|
||||
pollInterval={60e3}
|
||||
disablePoll={hasNew}
|
||||
scrollElRef={scrollElRef}
|
||||
onHasNew={setHasNew}
|
||||
onScrolledDownChange={setIsScrolledDown}
|
||||
renderEmptyState={renderPostsEmpty}
|
||||
headerOffset={headerHeight}
|
||||
/>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onScrollToTop}
|
||||
@@ -601,11 +597,7 @@ function AboutSection({
|
||||
label={_(msg`View users who like this feed`)}
|
||||
to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
|
||||
style={[t.atoms.text_contrast_medium, a.font_bold]}>
|
||||
<Plural
|
||||
value={likeCount}
|
||||
one="Liked by # user"
|
||||
other="Liked by # users"
|
||||
/>
|
||||
{_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)}
|
||||
</InlineLinkText>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -22,15 +22,9 @@ export function ListContained() {
|
||||
<>
|
||||
<View style={{width: '100%', height: 300}}>
|
||||
<ScrollProvider
|
||||
onScroll={e => {
|
||||
onScroll={() => {
|
||||
'worklet'
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
contentOffset: e.contentOffset,
|
||||
layoutMeasurement: e.layoutMeasurement,
|
||||
contentSize: e.contentSize,
|
||||
}),
|
||||
)
|
||||
console.log('onScroll')
|
||||
}}>
|
||||
<List
|
||||
data={data}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
} from 'lib/icons'
|
||||
import {getTabState, TabState} from 'lib/routes/helpers'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {colors, s} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {isWeb} from 'platform/detection'
|
||||
@@ -89,26 +90,15 @@ let DrawerProfileCard = ({
|
||||
@{account.handle}
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}>
|
||||
<Trans>
|
||||
<Text type="xl-medium" style={pal.text}>
|
||||
{formatCountShortOnly(profile?.followersCount ?? 0)}
|
||||
</Text>{' '}
|
||||
<Plural
|
||||
value={profile?.followersCount || 0}
|
||||
one="follower"
|
||||
other="followers"
|
||||
/>
|
||||
</Trans>{' '}
|
||||
·{' '}
|
||||
<Text type="xl-medium" style={pal.text}>
|
||||
{formatCountShortOnly(profile?.followersCount ?? 0)}
|
||||
</Text>{' '}
|
||||
{pluralize(profile?.followersCount || 0, 'follower')} ·{' '}
|
||||
<Trans>
|
||||
<Text type="xl-medium" style={pal.text}>
|
||||
{formatCountShortOnly(profile?.followsCount ?? 0)}
|
||||
</Text>{' '}
|
||||
<Plural
|
||||
value={profile?.followsCount || 0}
|
||||
one="following"
|
||||
other="following"
|
||||
/>
|
||||
following
|
||||
</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
+1
-15
@@ -51,14 +51,6 @@
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#preload {
|
||||
width: 100px;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||
button, input, textarea {
|
||||
font: inherit;
|
||||
@@ -309,13 +301,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</noscript>
|
||||
|
||||
<!-- The root element for your Expo app. -->
|
||||
<div id="root">
|
||||
<div id="preload">
|
||||
<!-- Bluesky SVG -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 320"><path fill="#0085ff" d="M180 142c-16.3-31.7-60.7-90.8-102-120C38.5-5.9 23.4-1 13.5 3.4 2.1 8.6 0 26.2 0 36.5c0 10.4 5.7 84.8 9.4 97.2 12.2 41 55.7 55 95.7 50.5-58.7 8.6-110.8 30-42.4 106.1 75.1 77.9 103-16.7 117.3-64.6 14.3 48 30.8 139 116 64.6 64-64.6 17.6-97.5-41.1-106.1 40 4.4 83.5-9.5 95.7-50.5 3.7-12.4 9.4-86.8 9.4-97.2 0-10.3-2-27.9-13.5-33C336.5-1 321.5-6 282 22c-41.3 29.2-85.7 88.3-102 120Z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2958,15 +2958,15 @@
|
||||
mv "~2"
|
||||
safe-json-stringify "~1"
|
||||
|
||||
"@expo/cli@0.17.10":
|
||||
version "0.17.10"
|
||||
resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.17.10.tgz#7dd5e2b4a01f5d29698c431729a19878fbd806f5"
|
||||
integrity sha512-Jw2wY+lsavP9GRqwwLqF/SvB7w2GZ4sWBMcBKTZ8F0lWjwmLGAUt4WYquf20agdmnY/oZUHvWNkrz/t3SflhnA==
|
||||
"@expo/cli@0.17.8":
|
||||
version "0.17.8"
|
||||
resolved "https://registry.yarnpkg.com/@expo/cli/-/cli-0.17.8.tgz#4abe0d8c604b73a6e1d0a10f34e993cbf1cbad42"
|
||||
integrity sha512-yfkoghCltbGPDbRI71Qu3puInjXx4wO82+uhW82qbWLvosfIN7ep5Gr0Lq54liJpvlUG6M0IXM1GiGqcCyP12w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.0"
|
||||
"@expo/code-signing-certificates" "0.0.5"
|
||||
"@expo/config" "~8.5.0"
|
||||
"@expo/config-plugins" "~7.9.0"
|
||||
"@expo/config-plugins" "~7.8.0"
|
||||
"@expo/devcert" "^1.0.0"
|
||||
"@expo/env" "~0.2.2"
|
||||
"@expo/image-utils" "^0.4.0"
|
||||
@@ -2975,7 +2975,7 @@
|
||||
"@expo/osascript" "^2.0.31"
|
||||
"@expo/package-manager" "^1.1.1"
|
||||
"@expo/plist" "^0.1.0"
|
||||
"@expo/prebuild-config" "6.8.1"
|
||||
"@expo/prebuild-config" "6.7.4"
|
||||
"@expo/rudder-sdk-node" "1.1.1"
|
||||
"@expo/spawn-async" "1.5.0"
|
||||
"@expo/xcpretty" "^4.3.0"
|
||||
@@ -3070,10 +3070,10 @@
|
||||
xcode "^3.0.1"
|
||||
xml2js "0.6.0"
|
||||
|
||||
"@expo/config-plugins@7.9.1", "@expo/config-plugins@~7.9.0":
|
||||
version "7.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-7.9.1.tgz#fe4f7e4f9d4e87f2dcf2344ffdc59eb466dd5d2e"
|
||||
integrity sha512-ICt6Jed1J0tPYMQrJ8K5Qusgih2I6pZ2PU4VSvxsN3T4n97L13XpYV1vyq1Uc/HMl3UhOwldipmgpEbCfeDqsQ==
|
||||
"@expo/config-plugins@7.8.4":
|
||||
version "7.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-7.8.4.tgz#533b5d536c1dc8b5544d64878b51bda28f2e1a1f"
|
||||
integrity sha512-hv03HYxb/5kX8Gxv/BTI8TLc9L06WzqAfHRRXdbar4zkLcP2oTzvsLEF4/L/TIpD3rsnYa0KU42d0gWRxzPCJg==
|
||||
dependencies:
|
||||
"@expo/config-types" "^50.0.0-alpha.1"
|
||||
"@expo/fingerprint" "^0.6.0"
|
||||
@@ -3114,6 +3114,29 @@
|
||||
xcode "^3.0.1"
|
||||
xml2js "0.4.23"
|
||||
|
||||
"@expo/config-plugins@~7.8.2":
|
||||
version "7.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-7.8.2.tgz#c00ce93c4d6c2cb9e345ed9cd56ceeea05ab8ddb"
|
||||
integrity sha512-XM2eXA5EvcpmXFCui48+bVy8GTskYSjPf2yC+LliYv8PDcedu7+pdgmbnvH4eZCyHfTMO8/UiF+w8e5WgOEj5A==
|
||||
dependencies:
|
||||
"@expo/config-types" "^50.0.0-alpha.1"
|
||||
"@expo/fingerprint" "^0.6.0"
|
||||
"@expo/json-file" "~8.3.0"
|
||||
"@expo/plist" "^0.1.0"
|
||||
"@expo/sdk-runtime-versions" "^1.0.0"
|
||||
"@react-native/normalize-color" "^2.0.0"
|
||||
chalk "^4.1.2"
|
||||
debug "^4.3.1"
|
||||
find-up "~5.0.0"
|
||||
getenv "^1.0.0"
|
||||
glob "7.1.6"
|
||||
resolve-from "^5.0.0"
|
||||
semver "^7.5.3"
|
||||
slash "^3.0.0"
|
||||
slugify "^1.6.6"
|
||||
xcode "^3.0.1"
|
||||
xml2js "0.6.0"
|
||||
|
||||
"@expo/config-types@^47.0.0":
|
||||
version "47.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-47.0.0.tgz#99eeabe0bba7a776e0f252b78beb0c574692c38d"
|
||||
@@ -3124,13 +3147,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-50.0.0.tgz#b534d3ec997ec60f8af24f6ad56244c8afc71a0b"
|
||||
integrity sha512-0kkhIwXRT6EdFDwn+zTg9R2MZIAEYGn1MVkyRohAd+C9cXOb5RA8WLQi7vuxKF9m1SMtNAUrf0pO+ENK0+/KSw==
|
||||
|
||||
"@expo/config@8.5.6":
|
||||
version "8.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config/-/config-8.5.6.tgz#e37ba437a1718ed4629e1dd130a7aace25312b89"
|
||||
integrity sha512-wF5awSg6MNn1cb1lIgjnhOn5ov2TEUTnkAVCsOl0QqDwcP+YIerteSFwjn9V52UZvg58L+LKxpCuGbw5IHavbg==
|
||||
"@expo/config@8.5.4":
|
||||
version "8.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config/-/config-8.5.4.tgz#bb5eb06caa36e4e35dc8c7647fae63e147b830ca"
|
||||
integrity sha512-ggOLJPHGzJSJHVBC1LzwXwR6qUn8Mw7hkc5zEKRIdhFRuIQ6s2FE4eOvP87LrNfDF7eZGa6tJQYsiHSmZKG+8Q==
|
||||
dependencies:
|
||||
"@babel/code-frame" "~7.10.4"
|
||||
"@expo/config-plugins" "~7.9.0"
|
||||
"@expo/config-plugins" "~7.8.2"
|
||||
"@expo/config-types" "^50.0.0"
|
||||
"@expo/json-file" "^8.2.37"
|
||||
getenv "^1.0.0"
|
||||
@@ -3295,10 +3318,10 @@
|
||||
json5 "^2.2.2"
|
||||
write-file-atomic "^2.3.0"
|
||||
|
||||
"@expo/metro-config@0.17.7":
|
||||
version "0.17.7"
|
||||
resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.17.7.tgz#c877a9558f3b97447cc9cf382971403834d84b46"
|
||||
integrity sha512-3vAdinAjMeRwdhGWWLX6PziZdAPvnyJ6KVYqnJErHHqH0cA6dgAENT3Vq6PEM1H2HgczKr2d5yG9AMgwy848ow==
|
||||
"@expo/metro-config@0.17.6":
|
||||
version "0.17.6"
|
||||
resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.17.6.tgz#f1f4ef056aa357c1dba3841de465f5d319f17216"
|
||||
integrity sha512-WaC1C+sLX/Wa7irwUigLhng3ckmXIEQefZczB8DfYmleV6uhfWWo2kz/HijFBpV7FKs2cW6u8J/aBQpFkxlcqg==
|
||||
dependencies:
|
||||
"@babel/core" "^7.20.0"
|
||||
"@babel/generator" "^7.20.5"
|
||||
@@ -3422,22 +3445,6 @@
|
||||
semver "7.5.3"
|
||||
xml2js "0.6.0"
|
||||
|
||||
"@expo/prebuild-config@6.8.1":
|
||||
version "6.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-6.8.1.tgz#5d562b1d6b2e5e4727a3c61acf1a4ed6117b94d8"
|
||||
integrity sha512-ptK9e0dcj1eYlAWV+fG+QkuAWcLAT1AmtEbj++tn7ZjEj8+LkXRM73LCOEGaF0Er8i8ZWNnaVsgGW4vjgP5ZsA==
|
||||
dependencies:
|
||||
"@expo/config" "~8.5.0"
|
||||
"@expo/config-plugins" "~7.9.0"
|
||||
"@expo/config-types" "^50.0.0-alpha.1"
|
||||
"@expo/image-utils" "^0.4.0"
|
||||
"@expo/json-file" "^8.2.37"
|
||||
debug "^4.3.1"
|
||||
fs-extra "^9.0.0"
|
||||
resolve-from "^5.0.0"
|
||||
semver "7.5.3"
|
||||
xml2js "0.6.0"
|
||||
|
||||
"@expo/rudder-sdk-node@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz#6aa575f346833eb6290282118766d4919c808c6a"
|
||||
@@ -3582,54 +3589,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
|
||||
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
|
||||
|
||||
"@formatjs/ecma402-abstract@1.18.0":
|
||||
version "1.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.0.tgz#e2120e7101020140661b58430a7ff4262705a2f2"
|
||||
integrity sha512-PEVLoa3zBevWSCZzPIM/lvPCi8P5l4G+NXQMc/CjEiaCWgyHieUoo0nM7Bs0n/NbuQ6JpXEolivQ9pKSBHaDlA==
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher" "0.5.2"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@formatjs/intl-enumerator@1.4.3":
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.4.3.tgz#8d278c273485d7c6219916509fbd51ce3142064d"
|
||||
integrity sha512-0NpTmAQnDokPoB5aVtXvOdtrUq/uEuPPhBUAr57TYYDjI5MwfFXt8F6JCm6s6CPI0inL8+nxPLjjqH0qyNnP4Q==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@formatjs/intl-getcanonicallocales@2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.3.0.tgz#b6c6fa1c664e30a61f27fa6399a76159d82a5842"
|
||||
integrity sha512-BOXbLwqQ7nKua/l7tKqDLRN84WupDXFDhGJQMFvsMVA2dKuOdRaWTxWpL3cJ7qPkoNw11Jf+Xpj4OSPBBvW0eQ==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@formatjs/intl-locale@^3.4.3":
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-3.4.3.tgz#fdd2a3978b03aa76965abbca86526bb1d02973b6"
|
||||
integrity sha512-g/35yMikkkRmLYmqE4W74gvZyKa768oC9OmUFzfLmH3CVYF3v2kvAZI0WsxWLbxYj8TT7wBDeLIL3aIlRw4Osw==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.18.0"
|
||||
"@formatjs/intl-enumerator" "1.4.3"
|
||||
"@formatjs/intl-getcanonicallocales" "2.3.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@formatjs/intl-localematcher@0.5.2":
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.2.tgz#5fcf029fd218905575e5080fa33facdcb623d532"
|
||||
integrity sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw==
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@formatjs/intl-pluralrules@^5.2.10":
|
||||
version "5.2.10"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.10.tgz#379fc06133625df0cae715c1d902001974ff3279"
|
||||
integrity sha512-wfJypePrbOByaZVPP1moLXHgS9LeAvi9coP95XZX7ySVrwdDGPnxz9Pw+o7J1o8AjLxjiqGrvAi74key5zzIjQ==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.18.0"
|
||||
"@formatjs/intl-localematcher" "0.5.2"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@fortawesome/fontawesome-common-types@6.4.2":
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5"
|
||||
@@ -7729,13 +7688,6 @@
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash.throttle@^4.1.9":
|
||||
version "4.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz#f17a6ae084f7c0117bd7df145b379537bc9615c5"
|
||||
integrity sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*":
|
||||
version "4.14.197"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.197.tgz#e95c5ddcc814ec3e84c891910a01e0c8a378c54b"
|
||||
@@ -9013,10 +8965,10 @@ babel-preset-expo@^10.0.0:
|
||||
babel-plugin-react-native-web "~0.18.10"
|
||||
react-refresh "0.14.0"
|
||||
|
||||
babel-preset-expo@~10.0.2:
|
||||
version "10.0.2"
|
||||
resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-10.0.2.tgz#5aae992b8c85dce6cf98334c9991d3052c567950"
|
||||
integrity sha512-hg06qdSTK7MjKmFXSiq6cFoIbI3n3uT8a3NI2EZoISWhu+tedCj4DQduwi+3adFuRuYvAwECI0IYn/5iGh5zWQ==
|
||||
babel-preset-expo@~10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-10.0.1.tgz#a0e7ad0119f46e58cb3f0738c3ca0c6e97b69c11"
|
||||
integrity sha512-uWIGmLfbP3dS5+8nesxaW6mQs41d4iP7X82ZwRdisB/wAhKQmuJM9Y1jQe4006uNYkw6Phf2TT03ykLVro7KuQ==
|
||||
dependencies:
|
||||
"@babel/plugin-proposal-decorators" "^7.12.9"
|
||||
"@babel/plugin-transform-export-namespace-from" "^7.22.11"
|
||||
@@ -11924,7 +11876,7 @@ expo-eas-client@~0.11.0:
|
||||
resolved "https://registry.yarnpkg.com/expo-eas-client/-/expo-eas-client-0.11.0.tgz#0f25aa497849cade7ebef55c0631093a87e58b07"
|
||||
integrity sha512-99W0MUGe3U4/MY1E9UeJ4uKNI39mN8/sOGA0Le8XC47MTbwbLoVegHR3C5y2fXLwLn7EpfNxAn5nlxYjY3gD2A==
|
||||
|
||||
expo-file-system@^16.0.9, expo-file-system@~16.0.9:
|
||||
expo-file-system@^16.0.9:
|
||||
version "16.0.9"
|
||||
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-16.0.9.tgz#cbd6c4b228b60a6b6c71fd1b91fe57299fb24da7"
|
||||
integrity sha512-3gRPvKVv7/Y7AdD9eHMIdfg5YbUn2zbwKofjsloTI5sEC57SLUFJtbLvUCz9Pk63DaSQ7WIE1JM0EASyvuPbuw==
|
||||
@@ -11934,6 +11886,11 @@ expo-file-system@~16.0.0:
|
||||
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-16.0.1.tgz#326b7c2f6e53e1a0eaafc9769578aafb3f9c9f43"
|
||||
integrity sha512-/U6ufN2wRPgg4m2a9sqbL3dThqQsysT022qulEXWnUTmNaqnzYSk9ihjDWqoqjXLi9slQLsyok5t6CNzhM7HPw==
|
||||
|
||||
expo-file-system@~16.0.8:
|
||||
version "16.0.8"
|
||||
resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-16.0.8.tgz#13c79a8e06e42a8e76e9297df6920597a011d989"
|
||||
integrity sha512-yDbVT0TUKd7ewQjaY5THum2VRFx2n/biskGhkUmLh3ai21xjIVtaeIzHXyv9ir537eVgt4ReqDNWi7jcXjdUcA==
|
||||
|
||||
expo-font@~11.10.3:
|
||||
version "11.10.3"
|
||||
resolved "https://registry.yarnpkg.com/expo-font/-/expo-font-11.10.3.tgz#a3115ebda8e09bd7cb8052619a4bbe606f0c17f4"
|
||||
@@ -12027,10 +11984,10 @@ expo-modules-autolinking@1.10.3:
|
||||
find-up "^5.0.0"
|
||||
fs-extra "^9.1.0"
|
||||
|
||||
expo-modules-core@1.11.13:
|
||||
version "1.11.13"
|
||||
resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.11.13.tgz#a8e63ad844e966dce78dea40b50839af6c3bc518"
|
||||
integrity sha512-2H5qrGUvmLzmJNPDOnovH1Pfk5H/S/V0BifBmOQyDc9aUh9LaDwkqnChZGIXv8ZHDW8JRlUW0QqyWxTggkbw1A==
|
||||
expo-modules-core@1.11.12:
|
||||
version "1.11.12"
|
||||
resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.11.12.tgz#d5c7b3ed7ab57d4fb6885a0d8e10287dcf1ffe5f"
|
||||
integrity sha512-/e8g4kis0pFLer7C0PLyx98AfmztIM6gU9jLkYnB1pU9JAfQf904XEi3bmszO7uoteBQwSL6FLp1m3TePKhDaA==
|
||||
dependencies:
|
||||
invariant "^2.2.4"
|
||||
|
||||
@@ -12133,24 +12090,24 @@ expo-web-browser@~12.8.2:
|
||||
compare-urls "^2.0.0"
|
||||
url "^0.11.0"
|
||||
|
||||
expo@^50.0.17:
|
||||
version "50.0.17"
|
||||
resolved "https://registry.yarnpkg.com/expo/-/expo-50.0.17.tgz#ab0998d7e7c18e8d12efd9091f9688978b0e89ed"
|
||||
integrity sha512-eD8Nh10BgVwecU7EVyogx7X314ajxVpJdFwkXhi341AD61S2WPX31NMHW82XGXas6dbDjdbgtaOMo5H/vylB7Q==
|
||||
expo@^50.0.8:
|
||||
version "50.0.14"
|
||||
resolved "https://registry.yarnpkg.com/expo/-/expo-50.0.14.tgz#ddcae86aa0ba8d1be3da9ad1bdda23fa539dc97d"
|
||||
integrity sha512-yLPdxCMVAbmeEIpzzyAuJ79wvr6ToDDtQmuLDMAgWtjqP8x3CGddXxUe07PpKEQgzwJabdHvCLP5Bv94wMFIjQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.20.0"
|
||||
"@expo/cli" "0.17.10"
|
||||
"@expo/config" "8.5.6"
|
||||
"@expo/config-plugins" "7.9.1"
|
||||
"@expo/metro-config" "0.17.7"
|
||||
"@expo/cli" "0.17.8"
|
||||
"@expo/config" "8.5.4"
|
||||
"@expo/config-plugins" "7.8.4"
|
||||
"@expo/metro-config" "0.17.6"
|
||||
"@expo/vector-icons" "^14.0.0"
|
||||
babel-preset-expo "~10.0.2"
|
||||
babel-preset-expo "~10.0.1"
|
||||
expo-asset "~9.0.2"
|
||||
expo-file-system "~16.0.9"
|
||||
expo-file-system "~16.0.8"
|
||||
expo-font "~11.10.3"
|
||||
expo-keep-awake "~12.8.2"
|
||||
expo-modules-autolinking "1.10.3"
|
||||
expo-modules-core "1.11.13"
|
||||
expo-modules-core "1.11.12"
|
||||
fbemitter "^3.0.0"
|
||||
whatwg-url-without-unicode "8.0.0-3"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user