Compare commits

..

6 Commits

Author SHA1 Message Date
Eric Bailey 6363d9da81 Adjust spacing on mobile 2024-10-31 14:09:13 -05:00
Eric Bailey 12a69ae2d1 Ok nevermind 2024-10-31 12:29:17 -05:00
Eric Bailey bf3e3eac7f Fix types, add default 2024-10-31 12:22:29 -05:00
Eric Bailey 644c747a95 Handle errors, short posts 2024-10-31 12:04:45 -05:00
Eric Bailey 265d0f2122 Clean up 2024-10-31 11:36:34 -05:00
Eric Bailey d9fcb4c217 Add GitHub Gist embed support 2024-10-31 10:52:24 -05:00
243 changed files with 37284 additions and 43500 deletions
+1 -3
View File
@@ -1,5 +1,3 @@
const reactCompilerConfig = require('./react-compiler.config.js')
module.exports = {
root: true,
extends: [
@@ -81,7 +79,7 @@ module.exports = {
},
],
'simple-import-sort/exports': 'error',
'react-compiler/react-compiler': ['error', reactCompilerConfig],
'react-compiler/react-compiler': 'warn',
'no-restricted-imports': [
'error',
{
@@ -3,6 +3,8 @@ on:
push:
branches:
- main
- bnewbold/embedr
- bnewbold/embedr-rebase
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
-4
View File
@@ -16,10 +16,6 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Install node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Yarn install
uses: Wandalen/wretry.action@master
with:
+1 -1
View File
@@ -1 +1 @@
20
18
+2 -2
View File
@@ -5,7 +5,7 @@ WORKDIR /usr/src/social-app
ENV DEBIAN_FRONTEND=noninteractive
# Node
ENV NODE_VERSION=20
ENV NODE_VERSION=18
ENV NVM_DIR=/usr/share/nvm
# Go
@@ -17,7 +17,7 @@ ENV GOEXPERIMENT="loopvar"
# Expo
ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev}
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER ${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev}
COPY . .
+1 -1
View File
@@ -5,7 +5,7 @@ WORKDIR /usr/src/social-app
ENV DEBIAN_FRONTEND=noninteractive
# Node
ENV NODE_VERSION=20
ENV NODE_VERSION=18
ENV NVM_DIR=/usr/share/nvm
# Go
+2 -2
View File
@@ -17,8 +17,8 @@ appId: xyz.blueskyweb.app
- inputText: "Post with an image"
- tapOn:
id: "openGalleryBtn"
- tapOn: "Content warnings"
- tapOn: "Content warnings"
- tapOn:
id: "labelsBtn"
- tapOn: "Porn"
- tapOn:
label: "Tap on confirm"
+1
View File
@@ -29,6 +29,7 @@ appId: xyz.blueskyweb.app
- pressKey: Enter
- tapOn:
id: "nextBtn"
- tapOn: "Not now"
- tapOn:
id: "handleInput"
- inputText: "e2e-test"
-4
View File
@@ -23,7 +23,6 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- inputText: "They good"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- tapOn: "About"
@@ -46,7 +45,6 @@ appId: xyz.blueskyweb.app
- eraseText
- inputText: "They bad"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- assertVisible: Bad Ppl
@@ -62,7 +60,6 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- eraseText
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- assertNotVisible:
@@ -90,7 +87,6 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- inputText: "They good"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- tapOn: "About"
+1
View File
@@ -23,4 +23,5 @@ appId: xyz.blueskyweb.app
id: "loginPasswordInput"
- inputText: "hunter2"
- pressKey: Enter
- tapOn: "Not now"
- assertVisible: "Following"
-1
View File
@@ -22,7 +22,6 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- inputText: "Shhh"
- tapOn: "Save"
- tapOn: "Save"
# view modlist
- assertVisible: "Muted Users"
+1 -1
View File
@@ -13,7 +13,7 @@ appId: xyz.blueskyweb.app
- tapOn: "Select an avatar"
- waitForAnimationToEnd
- tapOn:
point: "50%,22%"
point: "16%,22%"
- waitForAnimationToEnd
- tapOn: "Choose"
- waitForAnimationToEnd
-12
View File
@@ -335,10 +335,8 @@ describe('shortenLinks', () => {
describe('parseEmbedPlayerFromUrl', () => {
const inputs = [
'https://youtu.be/videoId',
'https://youtu.be/videoId?t=1s',
'https://www.youtube.com/watch?v=videoId',
'https://www.youtube.com/watch?v=videoId&feature=share',
'https://www.youtube.com/watch?v=videoId&t=1s',
'https://youtube.com/watch?v=videoId',
'https://youtube.com/watch?v=videoId&feature=share',
'https://youtube.com/shorts/videoId',
@@ -443,11 +441,6 @@ describe('parseEmbedPlayerFromUrl', () => {
source: 'youtube',
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
},
{
type: 'youtube_video',
source: 'youtube',
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=1',
},
{
type: 'youtube_video',
source: 'youtube',
@@ -458,11 +451,6 @@ describe('parseEmbedPlayerFromUrl', () => {
source: 'youtube',
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
},
{
type: 'youtube_video',
source: 'youtube',
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=1',
},
{
type: 'youtube_video',
source: 'youtube',
-2
View File
@@ -99,8 +99,6 @@ module.exports = function (config) {
dark: DARK_SPLASH_CONFIG,
},
entitlements: {
'com.apple.developer.kernel.increased-memory-limit': true,
'com.apple.developer.kernel.extended-virtual-addressing': true,
'com.apple.security.application-groups': 'group.app.bsky',
},
privacyManifests: {
@@ -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="M13.5 4a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM10 5a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM8 6a1 1 0 0 0 0 2v2.64q-.319.374-.711.8l-.129.142c-.312.342-.649.711-.974 1.092-.731.857-1.488 1.866-1.89 2.99A4.9 4.9 0 0 0 4 17.298 4.7 4.7 0 0 0 8.702 22h6.596A4.7 4.7 0 0 0 20 17.298a4.8 4.8 0 0 0-.297-1.634c-.401-1.124-1.157-2.133-1.89-2.99-.324-.38-.66-.75-.973-1.092h0l-.129-.141c-.26-.286-.5-.55-.711-.8V8a1 1 0 1 0 0-2H8Zm2 5.35V8h4v3.35l.22.275c.306.383.661.777 1.013 1.163l.13.143h0c.315.345.628.688.93 1.042.372.435.704.861.974 1.28l-.159.025c-.845.13-1.838.242-2.581.222-.842-.022-1.475-.217-2.227-.454l-.027-.008c-.746-.235-1.61-.507-2.746-.538-.743-.02-1.617.064-2.38.165q.26-.342.56-.692c.302-.354.615-.697.93-1.042l.13-.143c.352-.386.707-.78 1.014-1.163L10 11.35Zm7.41 5.905q.316-.048.586-.095.004.07.004.138A2.7 2.7 0 0 1 15.298 20H8.702A2.7 2.7 0 0 1 6 17.298q0-.213.039-.434c.236-.043.53-.093.853-.142.845-.13 1.837-.242 2.581-.222.842.022 1.475.217 2.227.454l.027.008c.746.235 1.61.507 2.746.538.931.024 2.07-.113 2.937-.245Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -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="M15.439 3.148a1 1 0 0 1 .41.645l.568 3.22a7 7 0 1 1-6.174 10.97L4.32 19.027a1 1 0 0 1-1.159-.811L1.078 6.398a1 1 0 0 1 .81-1.158l12.803-2.258a1 1 0 0 1 .748.166ZM9.325 16.114A7 7 0 0 1 9 14c0-1.56.51-3 1.372-4.164l-6.456 1.139 1.041 5.909 4.368-.77ZM3.568 9.005l10.833-1.91-.347-1.97L3.22 7.036l.347 1.97ZM16 9a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm0 2a1 1 0 0 1 1 1v1.586l1.374 1.374a1 1 0 0 1-1.414 1.414l-1.667-1.667A1 1 0 0 1 15 14v-2a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 589 B

@@ -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="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 14c-2.95 0-5.163 1.733-6.08 4.21a.47.47 0 0 0 .09.493.9.9 0 0 0 .687.297H11a1 1 0 1 1 0 2H6.697a2.9 2.9 0 0 1-2.219-1.011 2.46 2.46 0 0 1-.433-2.473C5.235 14.296 8.168 12 12 12c.787 0 1.54.097 2.252.282a1 1 0 1 1-.504 1.936A7 7 0 0 0 12 14Zm6 0a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 569 B

+1 -3
View File
@@ -1,5 +1,3 @@
const reactCompilerConfig = require('./react-compiler.config.js')
module.exports = function (api) {
api.cache(true)
const isTestEnv = process.env.NODE_ENV === 'test'
@@ -19,7 +17,7 @@ module.exports = function (api) {
],
plugins: [
'macros',
['babel-plugin-react-compiler', reactCompilerConfig],
['babel-plugin-react-compiler', {target: '18'}],
[
'module:react-native-dotenv',
{
+1 -3
View File
@@ -23,9 +23,7 @@ func ExpandPostText(post *appbsky.FeedPost) string {
}
// bail out if bounds checks fail
if facet.Index.ByteStart > facet.Index.ByteEnd ||
int(facet.Index.ByteStart)+charsAdded > len(postText) ||
int(facet.Index.ByteEnd)+charsAdded > len(postText) {
if int(facet.Index.ByteStart)+charsAdded > len(postText) || int(facet.Index.ByteEnd)+charsAdded > len(postText) {
return false
}
linkText := postText[int(facet.Index.ByteStart)+charsAdded : int(facet.Index.ByteEnd)+charsAdded]
+2 -2
View File
@@ -112,8 +112,8 @@ func serve(cctx *cli.Context) error {
Skipper: middleware.DefaultSkipper,
Store: middleware.NewRateLimiterMemoryStoreWithConfig(
middleware.RateLimiterMemoryStoreConfig{
Rate: 20, // requests per second
Burst: 150, // allow bursts
Rate: 10, // requests per second
Burst: 30, // allow bursts
ExpiresIn: 3 * time.Minute, // garbage collect entries older than 3 minutes
},
),
-1
View File
@@ -38,7 +38,6 @@
<meta name="article:published_time" content="{{ postView.IndexedAt }}">
<meta name="article:published_time" content="{{ postView.IndexedAt }}">
<link rel="alternate" type="application/json+oembed" href="https://embed.bsky.app/oembed?format=json&url={{ postView.Uri | urlencode }}" />
<link rel="alternate" href="{{ postView.Uri }}" />
{% endif -%}
{%- endblock %}
-1
View File
@@ -37,7 +37,6 @@
{%- if requestHost %}
<link rel="alternate" type="application/rss+xml" href="https://{{ requestHost }}/profile/{{ profileView.Did }}/rss">
{% endif %}
<link rel="alternate" href="at://{{ profileView.Did }}/app.bsky.actor.profile/self" />
{% endif -%}
{%- endblock %}
+1 -28
View File
@@ -6,42 +6,15 @@ cfg.resolver.sourceExts = process.env.RN_SRC_EXT
? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts)
: cfg.resolver.sourceExts
if (cfg.resolver.resolveRequest) {
throw Error('Update this override because it is conflicting now.')
}
cfg.resolver.resolveRequest = (context, moduleName, platform) => {
// HACK: manually resolve a few packages that use `exports` in `package.json`.
// A proper solution is to enable `unstable_enablePackageExports` but this needs careful testing.
if (moduleName.startsWith('multiformats/hashes/hasher')) {
return context.resolveRequest(
context,
'multiformats/dist/src/hashes/hasher',
platform,
)
}
if (moduleName.startsWith('multiformats/cid')) {
return context.resolveRequest(
context,
'multiformats/dist/src/cid',
platform,
)
}
if (moduleName === '@ipld/dag-cbor') {
return context.resolveRequest(context, '@ipld/dag-cbor/src', platform)
}
return context.resolveRequest(context, moduleName, platform)
}
cfg.transformer.getTransformOptions = async () => ({
transform: {
experimentalImportSupport: true,
inlineRequires: true,
nonInlinedRequires: [
// We can remove this option and rely on the default after
// https://github.com/facebook/metro/pull/1390 is released.
// https://github.com/facebook/metro/pull/1126 is released.
'React',
'react',
'react-compiler-runtime',
'react/jsx-dev-runtime',
'react/jsx-runtime',
'react-native',
@@ -64,14 +64,11 @@ class SheetViewController: UIViewController {
func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) {
if let sheet = self.sheetPresentationController {
// Capture `self` weakly to prevent retain cycles.
// Also, capture `sheet` weakly to avoid potential strong references held by animateChanges.
sheet.animateChanges { [weak self, weak sheet] in
guard let weakSelf = self, let weakSheet = sheet else { return }
weakSelf.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion)
if #available(iOS 16.0, *) {
weakSheet.invalidateDetents()
}
sheet.animateChanges {
self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion)
if #available(iOS 16.0, *) {
sheet.invalidateDetents()
}
}
}
}
+24 -1
View File
@@ -1 +1,24 @@
export {BottomSheetNativeComponent as BottomSheet} from './BottomSheetNativeComponent'
import React from 'react'
import {BottomSheetViewProps} from './BottomSheet.types'
import {BottomSheetNativeComponent} from './BottomSheetNativeComponent'
import {useBottomSheetPortal_INTERNAL} from './BottomSheetPortal'
export const BottomSheet = React.forwardRef<
BottomSheetNativeComponent,
BottomSheetViewProps
>(function BottomSheet(props, ref) {
const Portal = useBottomSheetPortal_INTERNAL()
if (__DEV__ && !Portal) {
throw new Error(
'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.',
)
}
return (
<Portal>
<BottomSheetNativeComponent {...props} ref={ref} />
</Portal>
)
})
@@ -11,7 +11,6 @@ import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
import {BottomSheetPortalProvider} from './BottomSheetPortal'
import {Context as PortalContext} from './BottomSheetPortal'
const screenHeight = Dimensions.get('screen').height
@@ -35,8 +34,6 @@ export class BottomSheetNativeComponent extends React.Component<
> {
ref = React.createRef<any>()
static contextType = PortalContext
constructor(props: BottomSheetViewProps) {
super(props)
this.state = {
@@ -70,17 +67,6 @@ export class BottomSheetNativeComponent extends React.Component<
}
render() {
const Portal = this.context as React.ContextType<typeof PortalContext>
if (!Portal) {
throw new Error(
'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.',
)
}
if (!this.state.open) {
return null
}
const {children, backgroundColor, ...rest} = this.props
const cornerRadius = rest.cornerRadius ?? 0
@@ -97,41 +83,43 @@ export class BottomSheetNativeComponent extends React.Component<
}
}
if (!this.state.open) {
return null
}
return (
<Portal>
<NativeView
{...rest}
onStateChange={this.onStateChange}
ref={this.ref}
style={{
position: 'absolute',
height: screenHeight,
width: '100%',
}}
containerBackgroundColor={backgroundColor}>
<NativeView
{...rest}
onStateChange={this.onStateChange}
ref={this.ref}
style={{
position: 'absolute',
height: screenHeight,
width: '100%',
}}
containerBackgroundColor={backgroundColor}>
<View
style={[
{
flex: 1,
backgroundColor,
},
Platform.OS === 'android' && {
borderTopLeftRadius: cornerRadius,
borderTopRightRadius: cornerRadius,
},
extraStyles,
]}>
<View
style={[
{
flex: 1,
backgroundColor,
},
Platform.OS === 'android' && {
borderTopLeftRadius: cornerRadius,
borderTopRightRadius: cornerRadius,
},
extraStyles,
]}>
<View
onLayout={e => {
const {height} = e.nativeEvent.layout
this.setState({viewHeight: height})
this.updateLayout()
}}>
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
</View>
onLayout={e => {
const {height} = e.nativeEvent.layout
this.setState({viewHeight: height})
this.updateLayout()
}}>
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
</View>
</NativeView>
</Portal>
</View>
</NativeView>
)
}
}
@@ -4,7 +4,7 @@ import {createPortalGroup_INTERNAL} from './lib/Portal'
type PortalContext = React.ElementType<{children: React.ReactNode}>
export const Context = React.createContext({} as PortalContext)
const Context = React.createContext({} as PortalContext)
export const useBottomSheetPortal_INTERNAL = () => React.useContext(Context)
+9 -11
View File
@@ -3,7 +3,7 @@
"version": "1.94.0",
"private": true,
"engines": {
"node": ">=20"
"node": ">=18"
},
"packageManager": "yarn@1.22.19",
"scripts": {
@@ -68,8 +68,7 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.2",
"@haileyok/bluesky-video": "0.2.4",
"@ipld/dag-cbor": "^9.2.0",
"@haileyok/bluesky-video": "0.2.3",
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.7.1",
"@miblanchard/react-native-slider": "^2.3.1",
@@ -103,7 +102,7 @@
"@tiptap/suggestion": "^2.6.6",
"@types/invariant": "^2.2.37",
"@types/lodash.throttle": "^4.1.9",
"@types/node": "^20.14.3",
"@types/node": "^18.16.2",
"@zxing/text-encoding": "^0.9.0",
"array.prototype.findlast": "^1.2.3",
"await-lock": "^2.2.2",
@@ -159,21 +158,20 @@
"lodash.set": "^4.3.2",
"lodash.shuffle": "^4.2.0",
"lodash.throttle": "^4.1.1",
"multiformats": "^13.1.0",
"nanoid": "^5.0.5",
"normalize-url": "^8.0.0",
"patch-package": "^6.5.1",
"postinstall-postinstall": "^2.1.0",
"psl": "^1.9.0",
"react": "18.2.0",
"react-compiler-runtime": "^19.0.0-beta-a7bf2bd-20241110",
"react-compiler-runtime": "^19.0.0-beta-6fc168f-20241025",
"react-dom": "^18.2.0",
"react-image-crop": "^11.0.7",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.74.1",
"react-native-compressor": "^1.8.24",
"react-native-date-picker": "^4.4.2",
"react-native-drawer-layout": "^4.0.1",
"react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-gesture-handler": "2.20.0",
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "0.41.2",
@@ -184,7 +182,7 @@
"react-native-picker-select": "^9.1.3",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.1",
"react-native-reanimated": "^3.16.1",
"react-native-reanimated": "^3.11.0",
"react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "~3.31.1",
@@ -243,7 +241,7 @@
"babel-loader": "^9.1.2",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-react-compiler": "^19.0.0-beta-a7bf2bd-20241110",
"babel-plugin-react-compiler": "^19.0.0-beta-6fc168f-20241025",
"babel-plugin-react-native-web": "^0.18.12",
"babel-preset-expo": "^10.0.0",
"eslint": "^8.19.0",
@@ -251,10 +249,9 @@
"eslint-plugin-ft-flow": "^2.0.3",
"eslint-plugin-lingui": "^0.2.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-compiler": "^19.0.0-beta-a7bf2bd-20241110",
"eslint-plugin-react-compiler": "^19.0.0-beta-6fc168f-20241025",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"file-loader": "6.2.0",
"html-webpack-plugin": "^5.5.0",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
@@ -266,6 +263,7 @@
"prettier": "^2.8.3",
"react-native-dotenv": "^3.3.1",
"react-refresh": "^0.14.0",
"react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0",
"svgo": "^3.3.2",
"ts-node": "^10.9.1",
-18
View File
@@ -1,18 +0,0 @@
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt
index f339e5f..fa35e82 100644
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt
@@ -16,7 +16,12 @@ class RawImageExporter : ImageExporter {
copyFile(source, output, contentResolver)
val exifInterface = ExifInterface(output.absolutePath)
val imageRotation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)
- val isRotatedLandscape = (imageRotation == ExifInterface.ORIENTATION_ROTATE_90 || imageRotation == ExifInterface.ORIENTATION_ROTATE_270)
+ val isRotatedLandscape = (
+ imageRotation == ExifInterface.ORIENTATION_ROTATE_90 ||
+ imageRotation == ExifInterface.ORIENTATION_ROTATE_270 ||
+ imageRotation == ExifInterface.ORIENTATION_TRANSPOSE ||
+ imageRotation == ExifInterface.ORIENTATION_TRANSVERSE
+ )
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(output.absolutePath, options)
+12 -125
View File
@@ -9,7 +9,7 @@ index caa5540..c5d4e67 100644
- type != nil && [type length] > 0 ? type : @"application/octet-stream",
+ ![type isEqual:[NSNull null]] && [type length] > 0 ? type : @"application/octet-stream",
[data base64EncodedStringWithOptions:0]];
resolve(text);
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
index b0d71dc..41b9a0e 100644
@@ -18,7 +18,7 @@ index b0d71dc..41b9a0e 100644
@@ -377,10 +377,6 @@ - (void)textInputDidBeginEditing
self.backedTextInputView.attributedText = [NSAttributedString new];
}
- if (_selectTextOnFocus) {
- [self.backedTextInputView selectAll:nil];
- }
@@ -35,7 +35,7 @@ index b0d71dc..41b9a0e 100644
+ [self.backedTextInputView selectAll:nil];
+ }
}
- (void)reactBlur
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
index e9b330f..ec5f58c 100644
@@ -48,10 +48,10 @@ index e9b330f..ec5f58c 100644
+@property (nonatomic, copy) UIColor *customTintColor;
+
+- (void)forwarderBeginRefreshing;
@end
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
index b09e653..d2b4e05 100644
index b09e653..288e60c 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
@@ -22,6 +22,7 @@ @implementation RCTRefreshControl {
@@ -60,12 +60,12 @@ index b09e653..d2b4e05 100644
CGFloat _progressViewOffset;
+ UIColor *_customTintColor;
}
- (instancetype)init
@@ -56,6 +57,12 @@ - (void)layoutSubviews
_isInitialRender = false;
}
+- (void)didMoveToSuperview
+{
+ [super didMoveToSuperview];
@@ -78,7 +78,7 @@ index b09e653..d2b4e05 100644
@@ -203,4 +210,58 @@ - (void)refreshControlValueChanged
}
}
+- (void)setCustomTintColor:(UIColor *)customTintColor
+{
+ _customTintColor = customTintColor;
@@ -139,139 +139,26 @@ index 40aaf9c..1c60164 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
@@ -22,11 +22,12 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat)
+RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor)
+
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
index 1aead51..39e6244 100644
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
@@ -159,31 +159,6 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
return !shouldDisableScrollInteraction;
}
-/*
- * Automatically centers the content such that if the content is smaller than the
- * ScrollView, we force it to be centered, but when you zoom or the content otherwise
- * becomes larger than the ScrollView, there is no padding around the content but it
- * can still fill the whole view.
- */
-- (void)setContentOffset:(CGPoint)contentOffset
-{
- UIView *contentView = [self contentView];
- if (contentView && _centerContent && !CGSizeEqualToSize(contentView.frame.size, CGSizeZero)) {
- CGSize subviewSize = contentView.frame.size;
- CGSize scrollViewSize = self.bounds.size;
- if (subviewSize.width <= scrollViewSize.width) {
- contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0;
- }
- if (subviewSize.height <= scrollViewSize.height) {
- contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0;
- }
- }
-
- super.contentOffset = CGPointMake(
- RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
- RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
-}
-
- (void)setFrame:(CGRect)frame
{
// Preserving and revalidating `contentOffset`.
@@ -427,6 +402,11 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
// Does nothing
}
+- (void)setFrame:(CGRect)frame {
+ [super setFrame:frame];
+ [self centerContentIfNeeded];
+}
+
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
{
[super insertReactSubview:view atIndex:atIndex];
@@ -444,6 +424,8 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
[_scrollView addSubview:view];
}
+
+ [self centerContentIfNeeded];
}
- (void)removeReactSubview:(UIView *)subview
@@ -652,9 +634,46 @@ -(void)delegateMethod : (UIScrollView *)scrollView \
}
RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin)
-RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
+-(void)scrollViewDidZoom : (UIScrollView *)scrollView
+{
+ [self centerContentIfNeeded];
+
+ RCT_SEND_SCROLL_EVENT(onScroll, nil);
+ RCT_FORWARD_SCROLL_EVENT(scrollViewDidZoom : scrollView);
+}
+
+/*
+ * Automatically centers the content such that if the content is smaller than the
+ * ScrollView, we force it to be centered, but when you zoom or the content otherwise
+ * becomes larger than the ScrollView, there is no padding around the content but it
+ * can still fill the whole view.
+ *
+ * PATCHED: This deviates from the original React Native implementation to fix two issues:
+ *
+ * - The scroll view was swallowing any taps immediately after pinching
+ * - The scroll view was jittering when crossing the full screen threshold while pinching
+ *
+ * This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/.
+ */
+-(void)centerContentIfNeeded
+{
+ if (_scrollView.centerContent &&
+ !CGSizeEqualToSize(self.contentSize, CGSizeZero) &&
+ !CGSizeEqualToSize(self.bounds.size, CGSizeZero)
+ ) {
+ CGFloat top = 0, left = 0;
+ if (self.contentSize.width < self.bounds.size.width) {
+ left = (self.bounds.size.width - self.contentSize.width) * 0.5f;
+ }
+ if (self.contentSize.height < self.bounds.size.height) {
+ top = (self.bounds.size.height - self.contentSize.height) * 0.5f;
+ }
+ _scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left);
+ }
+}
+
- (void)addScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
{
[_scrollListeners addObject:scrollListener];
@@ -913,6 +932,7 @@ - (void)updateContentSizeIfNeeded
CGSize contentSize = self.contentSize;
if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
_scrollView.contentSize = contentSize;
+ [self centerContentIfNeeded];
}
}
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
index 5f5e1ab..aac00b6 100644
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
@@ -99,8 +99,9 @@ public class JavaTimerManager {
}
// If the JS thread is busy for multiple frames we cancel any other pending runnable.
- if (mCurrentIdleCallbackRunnable != null) {
- mCurrentIdleCallbackRunnable.cancel();
@@ -279,5 +166,5 @@ index 5f5e1ab..aac00b6 100644
+ if (currentRunnable != null) {
+ currentRunnable.cancel();
}
mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
@@ -0,0 +1,209 @@
diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts b/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
index 92ebe62..5f8207e 100644
--- a/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
+++ b/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
@@ -96,7 +96,8 @@ export interface AnimatedComponentRef extends Component {
export interface IAnimatedComponentInternal {
_styles: StyleProps[] | null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
- _viewTag: number;
+ _componentViewTag: number;
+ _eventViewTag: number;
_isFirstRender: boolean;
jestAnimatedStyle: { value: StyleProps };
_component: AnimatedComponentRef | HTMLElement | null;
diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
index 88b3fdf..2488ebc 100644
--- a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
+++ b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
@@ -122,7 +122,8 @@ export function createAnimatedComponent(
{
_styles: StyleProps[] | null = null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
- _viewTag = -1;
+ _componentViewTag = -1;
+ _eventViewTag = -1;
_isFirstRender = true;
jestAnimatedStyle: { value: StyleProps } = { value: {} };
_component: AnimatedComponentRef | HTMLElement | null = null;
@@ -143,7 +144,8 @@ export function createAnimatedComponent(
}
componentDidMount() {
- this._viewTag = this._getViewInfo().viewTag as number;
+ this._setComponentViewTag();
+ this._setEventViewTag();
this._attachNativeEvents();
this._jsPropsUpdater.addOnJSPropsChangeListener(this);
this._attachAnimatedStyles();
@@ -185,7 +187,10 @@ export function createAnimatedComponent(
if (this.props.sharedTransitionTag) {
this._configureSharedTransition(true);
}
- this._sharedElementTransition?.unregisterTransition(this._viewTag, true);
+ this._sharedElementTransition?.unregisterTransition(
+ this._componentViewTag,
+ true
+ );
const exiting = this.props.exiting;
if (
@@ -209,7 +214,7 @@ export function createAnimatedComponent(
: getReduceMotionFromConfig();
if (!reduceMotionInExiting) {
updateLayoutAnimations(
- this._viewTag,
+ this._componentViewTag,
LayoutAnimationType.EXITING,
maybeBuild(
exiting,
@@ -221,12 +226,22 @@ export function createAnimatedComponent(
}
}
- _getEventViewRef() {
- // Make sure to get the scrollable node for components that implement
- // `ScrollResponder.Mixin`.
- return (this._component as AnimatedComponentRef)?.getScrollableNode
- ? (this._component as AnimatedComponentRef).getScrollableNode?.()
- : this._component;
+ _setComponentViewTag() {
+ this._componentViewTag = this._getViewInfo().viewTag as number;
+ }
+
+ _setEventViewTag() {
+ // Setting the tag for registering events - since the event emitting view can be nested inside the main component
+ const componentAnimatedRef = this._component as AnimatedComponentRef;
+ if (componentAnimatedRef.getScrollableNode) {
+ const scrollableNode = componentAnimatedRef.getScrollableNode();
+ this._eventViewTag = findNodeHandle(scrollableNode) ?? -1;
+ } else {
+ this._eventViewTag =
+ findNodeHandle(
+ options?.setNativeProps ? this : componentAnimatedRef
+ ) ?? -1;
+ }
}
_attachNativeEvents() {
@@ -236,7 +251,7 @@ export function createAnimatedComponent(
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
- prop.workletEventHandler.registerForEvents(this._viewTag, key);
+ prop.workletEventHandler.registerForEvents(this._eventViewTag, key);
}
}
}
@@ -248,7 +263,7 @@ export function createAnimatedComponent(
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
- prop.workletEventHandler.unregisterFromEvents(this._viewTag);
+ prop.workletEventHandler.unregisterFromEvents(this._eventViewTag);
}
}
}
@@ -258,15 +273,17 @@ export function createAnimatedComponent(
for (const style of this._styles) {
style.viewsRef.remove(this);
}
- } else if (this._viewTag !== -1 && this._styles !== null) {
+ } else if (this._componentViewTag !== -1 && this._styles !== null) {
for (const style of this._styles) {
- style.viewDescriptors.remove(this._viewTag);
+ style.viewDescriptors.remove(this._componentViewTag);
}
if (this.props.animatedProps?.viewDescriptors) {
- this.props.animatedProps.viewDescriptors.remove(this._viewTag);
+ this.props.animatedProps.viewDescriptors.remove(
+ this._componentViewTag
+ );
}
if (isFabric()) {
- removeFromPropsRegistry(this._viewTag);
+ removeFromPropsRegistry(this._componentViewTag);
}
}
}
@@ -283,15 +300,19 @@ export function createAnimatedComponent(
const newProp = this.props[key];
if (!newProp) {
// Prop got deleted
- prevProp.workletEventHandler.unregisterFromEvents(this._viewTag);
+ prevProp.workletEventHandler.unregisterFromEvents(
+ this._eventViewTag
+ );
} else if (
has('workletEventHandler', newProp) &&
newProp.workletEventHandler instanceof WorkletEventHandler &&
newProp.workletEventHandler !== prevProp.workletEventHandler
) {
// Prop got changed
- prevProp.workletEventHandler.unregisterFromEvents(this._viewTag);
- newProp.workletEventHandler.registerForEvents(this._viewTag);
+ prevProp.workletEventHandler.unregisterFromEvents(
+ this._eventViewTag
+ );
+ newProp.workletEventHandler.registerForEvents(this._eventViewTag);
}
}
}
@@ -304,7 +325,7 @@ export function createAnimatedComponent(
!prevProps[key]
) {
// Prop got added
- newProp.workletEventHandler.registerForEvents(this._viewTag);
+ newProp.workletEventHandler.registerForEvents(this._eventViewTag);
}
}
}
@@ -381,7 +402,7 @@ export function createAnimatedComponent(
adaptViewConfig(viewConfig);
}
- this._viewTag = viewTag as number;
+ this._componentViewTag = viewTag as number;
// remove old styles
if (prevStyles) {
@@ -487,7 +508,11 @@ export function createAnimatedComponent(
AnimatedComponent.displayName
)
: undefined;
- updateLayoutAnimations(this._viewTag, LayoutAnimationType.LAYOUT, layout);
+ updateLayoutAnimations(
+ this._componentViewTag,
+ LayoutAnimationType.LAYOUT,
+ layout
+ );
}
_configureSharedTransition(isUnmounting = false) {
@@ -497,7 +522,7 @@ export function createAnimatedComponent(
const { sharedTransitionTag } = this.props;
if (!sharedTransitionTag) {
this._sharedElementTransition?.unregisterTransition(
- this._viewTag,
+ this._componentViewTag,
isUnmounting
);
this._sharedElementTransition = null;
@@ -508,7 +533,7 @@ export function createAnimatedComponent(
this._sharedElementTransition ??
new SharedTransition();
sharedElementTransition.registerTransition(
- this._viewTag,
+ this._componentViewTag,
sharedTransitionTag,
isUnmounting
);
@@ -527,7 +552,7 @@ export function createAnimatedComponent(
? (ref as HTMLElement)
: findNodeHandle(ref as Component);
- this._viewTag = tag as number;
+ this._componentViewTag = tag as number;
const { layout, entering, exiting, sharedTransitionTag } = this.props;
if (
-9
View File
@@ -1,9 +0,0 @@
'use strict'
module.exports = {
target: '18',
environment: {
enableTreatRefLikeIdentifiersAsRefs: true,
validateRefAccessDuringRender: false, // TODO: Make it `true`.
},
}
+15 -15
View File
@@ -40,11 +40,14 @@ import {
shouldRequestEmailConfirmation,
snoozeEmailConfirmationPrompt,
} from '#/state/shell/reminders'
import {AccessibilitySettingsScreen} from '#/view/screens/AccessibilitySettings'
import {AppPasswords} from '#/view/screens/AppPasswords'
import {CommunityGuidelinesScreen} from '#/view/screens/CommunityGuidelines'
import {CopyrightPolicyScreen} from '#/view/screens/CopyrightPolicy'
import {DebugModScreen} from '#/view/screens/DebugMod'
import {FeedsScreen} from '#/view/screens/Feeds'
import {HomeScreen} from '#/view/screens/Home'
import {LanguageSettingsScreen} from '#/view/screens/LanguageSettings'
import {ListsScreen} from '#/view/screens/Lists'
import {LogScreen} from '#/view/screens/Log'
import {ModerationBlockedAccounts} from '#/view/screens/ModerationBlockedAccounts'
@@ -52,7 +55,11 @@ import {ModerationModlistsScreen} from '#/view/screens/ModerationModlists'
import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts'
import {NotFoundScreen} from '#/view/screens/NotFound'
import {NotificationsScreen} from '#/view/screens/Notifications'
import {NotificationsSettingsScreen} from '#/view/screens/NotificationsSettings'
import {PostThreadScreen} from '#/view/screens/PostThread'
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
import {PreferencesFollowingFeed} from '#/view/screens/PreferencesFollowingFeed'
import {PreferencesThreads} from '#/view/screens/PreferencesThreads'
import {PrivacyPolicyScreen} from '#/view/screens/PrivacyPolicy'
import {ProfileScreen} from '#/view/screens/Profile'
import {ProfileFeedScreen} from '#/view/screens/ProfileFeed'
@@ -62,6 +69,7 @@ import {ProfileFollowsScreen} from '#/view/screens/ProfileFollows'
import {ProfileListScreen} from '#/view/screens/ProfileList'
import {SavedFeeds} from '#/view/screens/SavedFeeds'
import {SearchScreen} from '#/view/screens/Search'
import {SettingsScreen} from '#/view/screens/Settings'
import {Storybook} from '#/view/screens/Storybook'
import {SupportScreen} from '#/view/screens/Support'
import {TermsOfServiceScreen} from '#/view/screens/TermsOfService'
@@ -79,7 +87,6 @@ import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
import {
StarterPackScreen,
StarterPackScreenShort,
@@ -89,16 +96,9 @@ import {useTheme} from '#/alf'
import {router} from '#/routes'
import {Referrer} from '../modules/expo-bluesky-swiss-army'
import {AboutSettingsScreen} from './screens/Settings/AboutSettings'
import {AccessibilitySettingsScreen} from './screens/Settings/AccessibilitySettings'
import {AccountSettingsScreen} from './screens/Settings/AccountSettings'
import {AppPasswordsScreen} from './screens/Settings/AppPasswords'
import {ContentAndMediaSettingsScreen} from './screens/Settings/ContentAndMediaSettings'
import {ExternalMediaPreferencesScreen} from './screens/Settings/ExternalMediaPreferences'
import {FollowingFeedPreferencesScreen} from './screens/Settings/FollowingFeedPreferences'
import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings'
import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings'
import {SettingsScreen} from './screens/Settings/Settings'
import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -285,7 +285,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
/>
<Stack.Screen
name="AppPasswords"
getComponent={() => AppPasswordsScreen}
getComponent={() => AppPasswords}
options={{title: title(msg`App Passwords`), requireAuth: true}}
/>
<Stack.Screen
@@ -295,7 +295,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
/>
<Stack.Screen
name="PreferencesFollowingFeed"
getComponent={() => FollowingFeedPreferencesScreen}
getComponent={() => PreferencesFollowingFeed}
options={{
title: title(msg`Following Feed Preferences`),
requireAuth: true,
@@ -303,12 +303,12 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
/>
<Stack.Screen
name="PreferencesThreads"
getComponent={() => ThreadPreferencesScreen}
getComponent={() => PreferencesThreads}
options={{title: title(msg`Threads Preferences`), requireAuth: true}}
/>
<Stack.Screen
name="PreferencesExternalEmbeds"
getComponent={() => ExternalMediaPreferencesScreen}
getComponent={() => PreferencesExternalEmbeds}
options={{
title: title(msg`External Media Preferences`),
requireAuth: true,
@@ -378,8 +378,8 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
options={{title: title(msg`Chat settings`), requireAuth: true}}
/>
<Stack.Screen
name="NotificationSettings"
getComponent={() => NotificationSettingsScreen}
name="NotificationsSettings"
getComponent={() => NotificationsSettingsScreen}
options={{title: title(msg`Notification settings`), requireAuth: true}}
/>
<Stack.Screen
@@ -696,7 +696,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
onStateChange={() => {
const routeName = getCurrentRouteName()
if (routeName === 'Notifications') {
logEvent('router:navigate:notifications', {})
logEvent('router:navigate:notifications:sampled', {})
}
}}
onReady={() => {
+32 -40
View File
@@ -80,40 +80,40 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return {
transform: [
{
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
},
{
scale: interpolate(
outroLogo.get(),
outroLogo.value,
[0, 0.08, 1],
[1, 0.8, 500],
'clamp',
),
},
],
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
}
})
const bottomLogoAnimation = useAnimatedStyle(() => {
return {
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
}
})
const reducedLogoAnimation = useAnimatedStyle(() => {
return {
transform: [
{
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
},
],
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
}
})
const logoWrapperAnimation = useAnimatedStyle(() => {
return {
opacity: interpolate(
outroAppOpacity.get(),
outroAppOpacity.value,
[0, 0.1, 0.2, 1],
[1, 1, 0, 0],
'clamp',
@@ -125,11 +125,11 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return {
transform: [
{
scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'),
scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'),
},
],
opacity: interpolate(
outroAppOpacity.get(),
outroAppOpacity.value,
[0, 0.1, 0.2, 1],
[0, 0, 1, 1],
'clamp',
@@ -145,37 +145,29 @@ export function Splash(props: React.PropsWithChildren<Props>) {
if (isReady) {
SplashScreen.hideAsync()
.then(() => {
intro.set(() =>
withTiming(
1,
{duration: 400, easing: Easing.out(Easing.cubic)},
async () => {
// set these values to check animation at specific point
// outroLogo.set(0.1)
// outroApp.set(0.1)
outroLogo.set(() =>
withTiming(
1,
{duration: 1200, easing: Easing.in(Easing.cubic)},
() => {
runOnJS(onFinish)()
},
),
)
outroApp.set(() =>
withTiming(1, {
duration: 1200,
easing: Easing.inOut(Easing.cubic),
}),
)
outroAppOpacity.set(() =>
withTiming(1, {
duration: 1200,
easing: Easing.in(Easing.cubic),
}),
)
},
),
intro.value = withTiming(
1,
{duration: 400, easing: Easing.out(Easing.cubic)},
async () => {
// set these values to check animation at specific point
// outroLogo.value = 0.1
// outroApp.value = 0.1
outroLogo.value = withTiming(
1,
{duration: 1200, easing: Easing.in(Easing.cubic)},
() => {
runOnJS(onFinish)()
},
)
outroApp.value = withTiming(1, {
duration: 1200,
easing: Easing.inOut(Easing.cubic),
})
outroAppOpacity.value = withTiming(1, {
duration: 1200,
easing: Easing.in(Easing.cubic),
})
},
)
})
.catch(() => {})
+1 -4
View File
@@ -1,4 +1,4 @@
import {Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native'
import {Platform, StyleSheet, ViewStyle} from 'react-native'
import * as tokens from '#/alf/tokens'
import {native, web} from '#/alf/util/platform'
@@ -887,9 +887,6 @@ export const atoms = {
user_select_all: {
userSelect: 'all',
},
outline_inset_1: {
outlineOffset: '-1px',
} as StyleProp<ViewStyle>,
/*
* Text decoration
+4
View File
@@ -38,6 +38,10 @@ export function setFontFamily(fontFamily: Device['fontFamily']) {
* Unused fonts are commented out, but the files are there if we need them.
*/
export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
if (style.fontFamily === 'monospace') {
return
}
if (fontFamily === 'theme') {
if (isAndroid) {
style.fontFamily =
+28 -25
View File
@@ -103,32 +103,35 @@ export function ThemeProvider({
})
}, [])
const value = React.useMemo<Alf>(
() => ({
themes,
themeName: themeName,
theme: themes[themeName],
fonts: {
scale: fontScale,
scaleMultiplier: fontScaleMultiplier,
family: fontFamily,
setFontScale: setFontScaleAndPersist,
setFontFamily: setFontFamilyAndPersist,
},
flags: {},
}),
[
themeName,
themes,
fontScale,
setFontScaleAndPersist,
fontFamily,
setFontFamilyAndPersist,
fontScaleMultiplier,
],
return (
<Context.Provider
value={React.useMemo<Alf>(
() => ({
themes,
themeName: themeName,
theme: themes[themeName],
fonts: {
scale: fontScale,
scaleMultiplier: fontScaleMultiplier,
family: fontFamily,
setFontScale: setFontScaleAndPersist,
setFontFamily: setFontFamilyAndPersist,
},
flags: {},
}),
[
themeName,
themes,
fontScale,
setFontScaleAndPersist,
fontFamily,
setFontFamilyAndPersist,
fontScaleMultiplier,
],
)}>
{children}
</Context.Provider>
)
return <Context.Provider value={value}>{children}</Context.Provider>
}
export function useAlf() {
+1 -1
View File
@@ -8,7 +8,7 @@ import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {Text as BaseText, TextProps} from '#/components/Typography'
export const colors = {
const colors = {
warning: {
light: '#DFBC00',
dark: '#BFAF1F',
-76
View File
@@ -1,76 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {moderateProfile} from '@atproto/api'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfilesQuery} from '#/state/queries/profile'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
export function AvatarStack({
profiles,
size = 26,
}: {
profiles: string[]
size?: number
}) {
const halfSize = size / 2
const {data, error} = useProfilesQuery({handles: profiles})
const t = useTheme()
const moderationOpts = useModerationOpts()
if (error) {
console.error(error)
return null
}
const isPending = !data || !moderationOpts
const items = isPending
? Array.from({length: profiles.length}).map((_, i) => ({
key: i,
profile: null,
moderation: null,
}))
: data.profiles.map(item => ({
key: item.did,
profile: item,
moderation: moderateProfile(item, moderationOpts),
}))
return (
<View
style={[
a.flex_row,
a.align_center,
a.relative,
{width: size + (items.length - 1) * halfSize},
]}>
{items.map((item, i) => (
<View
key={item.key}
style={[
t.atoms.bg_contrast_25,
a.relative,
{
width: size,
height: size,
left: i * -halfSize,
borderWidth: 1,
borderColor: t.atoms.bg.backgroundColor,
borderRadius: 999,
zIndex: 3 - i,
},
]}>
{item.profile && (
<UserAvatar
size={size - 2}
avatar={item.profile.avatar}
moderation={item.moderation.ui('avatar')}
/>
)}
</View>
))}
</View>
)
}
+3 -3
View File
@@ -29,10 +29,10 @@ export function useDialogControl(): DialogOuterProps['control'] {
const {activeDialogs} = useDialogStateContext()
React.useEffect(() => {
const map = activeDialogs.current
map.set(id, control)
activeDialogs.current.set(id, control)
return () => {
map.delete(id)
// eslint-disable-next-line react-hooks/exhaustive-deps
activeDialogs.current.delete(id)
}
}, [id, activeDialogs])
+1 -1
View File
@@ -15,7 +15,7 @@ import {
useKeyboardHandler,
} from 'react-native-keyboard-controller'
import {runOnJS} from 'react-native-reanimated'
import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+1 -1
View File
@@ -32,7 +32,7 @@ export function Error({
return (
<CenteredView
style={[
a.h_full_vh,
a.flex_1,
a.align_center,
a.gap_5xl,
!gtMobile && a.justify_between,
+4 -3
View File
@@ -17,12 +17,13 @@ export function Loader(props: Props) {
const rotation = useSharedValue(0)
const animatedStyles = useAnimatedStyle(() => ({
transform: [{rotate: rotation.get() + 'deg'}],
transform: [{rotate: rotation.value + 'deg'}],
}))
React.useEffect(() => {
rotation.set(() =>
withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1),
rotation.value = withRepeat(
withTiming(360, {duration: 500, easing: Easing.linear}),
-1,
)
}, [rotation])
+1 -2
View File
@@ -46,7 +46,7 @@ export function Root({
return <Context.Provider value={context}>{children}</Context.Provider>
}
export function Trigger({children, label, role = 'button'}: TriggerProps) {
export function Trigger({children, label}: TriggerProps) {
const {control} = React.useContext(Context)
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
@@ -70,7 +70,6 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
onPressIn,
onPressOut,
accessibilityLabel: label,
accessibilityRole: role,
},
})
}
+1 -2
View File
@@ -111,7 +111,7 @@ const RadixTriggerPassThrough = React.forwardRef(
)
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
export function Trigger({children, label, role = 'button'}: TriggerProps) {
export function Trigger({children, label}: TriggerProps) {
const {control} = React.useContext(Context)
const {
state: hovered,
@@ -155,7 +155,6 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) {
onMouseEnter,
onMouseLeave,
accessibilityLabel: label,
accessibilityRole: role,
},
})
}
-4
View File
@@ -1,7 +1,6 @@
import React from 'react'
import {
AccessibilityProps,
AccessibilityRole,
GestureResponderEvent,
PressableProps,
} from 'react-native'
@@ -37,7 +36,6 @@ export type RadixPassThroughTriggerProps = {
export type TriggerProps = {
children(props: TriggerChildProps): React.ReactNode
label: string
role?: AccessibilityRole
}
export type TriggerChildProps =
| {
@@ -65,7 +63,6 @@ export type TriggerChildProps =
onPressIn: () => void
onPressOut: () => void
accessibilityLabel: string
accessibilityRole: AccessibilityRole
}
}
| {
@@ -86,7 +83,6 @@ export type TriggerChildProps =
onMouseEnter: () => void
onMouseLeave: () => void
accessibilityLabel: string
accessibilityRole: AccessibilityRole
}
}
+2 -2
View File
@@ -283,8 +283,8 @@ export function DescriptionPlaceholder({
export type FollowButtonProps = {
profile: AppBskyActorDefs.ProfileViewBasic
moderationOpts: ModerationOpts
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
logContext: LogEvents['profile:follow:sampled']['logContext'] &
LogEvents['profile:unfollow:sampled']['logContext']
} & Partial<ButtonProps>
export function FollowButton(props: FollowButtonProps) {
+21 -27
View File
@@ -55,15 +55,13 @@ export const ProgressGuideToast = React.forwardRef<
// animate the opacity then set isOpen to false when done
const setIsntOpen = () => setIsOpen(false)
opacity.set(() =>
withTiming(
0,
{
duration: 400,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(setIsntOpen)(),
),
opacity.value = withTiming(
0,
{
duration: 400,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(setIsntOpen)(),
)
}, [setIsOpen, opacity])
@@ -73,24 +71,20 @@ export const ProgressGuideToast = React.forwardRef<
// animate the vertical translation, the opacity, and the checkmark
const playCheckmark = () => animatedCheckRef.current?.play()
opacity.set(0)
opacity.set(() =>
withTiming(
1,
{
duration: 100,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(playCheckmark)(),
),
)
translateY.set(0)
translateY.set(() =>
withTiming(insets.top + 10, {
duration: 500,
opacity.value = 0
opacity.value = withTiming(
1,
{
duration: 100,
easing: Easing.out(Easing.cubic),
}),
},
() => runOnJS(playCheckmark)(),
)
translateY.value = 0
translateY.value = withTiming(insets.top + 10, {
duration: 500,
easing: Easing.out(Easing.cubic),
})
// start the countdown timer to autoclose
timeoutRef.current = setTimeout(close, visibleDuration || 5e3)
@@ -120,8 +114,8 @@ export const ProgressGuideToast = React.forwardRef<
}, [winDim.width])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{translateY: translateY.get()}],
opacity: opacity.get(),
transform: [{translateY: translateY.value}],
opacity: opacity.value,
}))
return (
+1 -2
View File
@@ -128,8 +128,7 @@ export function RichText({
// @ts-ignore TODO
dataSet={WORD_WRAP}
shareOnLongPress
onPress={onLinkPress}
emoji>
onPress={onLinkPress}>
{toShortUrl(segment.text)}
</InlineLinkText>,
)
@@ -14,7 +14,6 @@ import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'
import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {useEmail} from '#/lib/hooks/useEmail'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {NavigationProp} from '#/lib/routes/types'
import {parseStarterPackUri} from '#/lib/strings/starter-pack'
@@ -28,7 +27,6 @@ import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus'
interface SectionRef {
@@ -188,9 +186,6 @@ function Empty() {
const followersDialogControl = useDialogControl()
const errorDialogControl = useDialogControl()
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const [isGenerating, setIsGenerating] = React.useState(false)
const {mutate: generateStarterPack} = useGenerateStarterPackMutation({
@@ -254,13 +249,7 @@ function Empty() {
color="primary"
size="small"
disabled={isGenerating}
onPress={() => {
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
confirmDialogControl.open()
}
}}
onPress={confirmDialogControl.open}
style={{backgroundColor: 'transparent'}}>
<ButtonText style={{color: 'white'}}>
<Trans>Make one for me</Trans>
@@ -273,13 +262,7 @@ function Empty() {
color="primary"
size="small"
disabled={isGenerating}
onPress={() => {
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
navigation.navigate('StarterPackWizard')
}
}}
onPress={() => navigation.navigate('StarterPackWizard')}
style={{
backgroundColor: 'white',
borderColor: 'white',
@@ -335,12 +318,6 @@ function Empty() {
onConfirm={generate}
confirmButtonCta={_(msg`Retry`)}
/>
<VerifyEmailDialog
reasonText={_(
msg`Before creating a starter pack, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</LinearGradientBackground>
)
}
+1 -3
View File
@@ -1,5 +1,3 @@
import {ViewStyleProp} from '#/alf'
export function SubtleWebHover({}: ViewStyleProp & {hover: boolean}) {
export function SubtleWebHover({}: {hover: boolean}) {
return null
}
+5 -7
View File
@@ -2,12 +2,9 @@ import React from 'react'
import {StyleSheet, View} from 'react-native'
import {isTouchDevice} from '#/lib/browser'
import {useTheme, ViewStyleProp} from '#/alf'
import {useTheme} from '#/alf'
export function SubtleWebHover({
style,
hover,
}: ViewStyleProp & {hover: boolean}) {
export function SubtleWebHover({hover}: {hover: boolean}) {
const t = useTheme()
if (isTouchDevice) {
return null
@@ -29,8 +26,9 @@ export function SubtleWebHover({
style={[
t.atoms.bg_contrast_25,
styles.container,
{opacity: hover ? opacity : 0},
style,
{
opacity: hover ? opacity : 0,
},
]}
/>
)
+8 -12
View File
@@ -32,25 +32,21 @@ export const AnimatedCheck = React.forwardRef<
const checkAnim = useSharedValue(0)
const circleAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: 166 - circleAnim.get() * 166,
strokeDashoffset: 166 - circleAnim.value * 166,
}))
const checkAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: 48 - 48 * checkAnim.get(),
strokeDashoffset: 48 - 48 * checkAnim.value,
}))
const play = React.useCallback(
(cb?: () => void) => {
circleAnim.set(0)
checkAnim.set(0)
circleAnim.value = 0
checkAnim.value = 0
circleAnim.set(() =>
withTiming(1, {duration: 500, easing: Easing.linear}),
)
checkAnim.set(() =>
withDelay(
500,
withTiming(1, {duration: 300, easing: Easing.linear}, cb),
),
circleAnim.value = withTiming(1, {duration: 500, easing: Easing.linear})
checkAnim.value = withDelay(
500,
withTiming(1, {duration: 300, easing: Easing.linear}, cb),
)
},
[circleAnim, checkAnim],
+29 -72
View File
@@ -18,14 +18,8 @@ import {Text} from '#/components/Typography'
export function VerifyEmailDialog({
control,
onCloseWithoutVerifying,
onCloseAfterVerifying,
reasonText,
}: {
control: Dialog.DialogControlProps
onCloseWithoutVerifying?: () => void
onCloseAfterVerifying?: () => void
reasonText?: string
}) {
const agent = useAgent()
@@ -36,24 +30,18 @@ export function VerifyEmailDialog({
control={control}
onClose={async () => {
if (!didVerify) {
onCloseWithoutVerifying?.()
return
}
try {
await agent.resumeSession(agent.session!)
onCloseAfterVerifying?.()
} catch (e: unknown) {
logger.error(String(e))
return
}
}}>
<Dialog.Handle />
<Inner
control={control}
setDidVerify={setDidVerify}
reasonText={reasonText}
/>
<Inner control={control} setDidVerify={setDidVerify} />
</Dialog.Outer>
)
}
@@ -61,11 +49,9 @@ export function VerifyEmailDialog({
export function Inner({
control,
setDidVerify,
reasonText,
}: {
control: Dialog.DialogControlProps
setDidVerify: (value: boolean) => void
reasonText?: string
}) {
const {_} = useLingui()
const {currentAccount} = useSession()
@@ -146,63 +132,34 @@ export function Inner({
<ErrorMessage message={error} />
</View>
) : null}
{currentStep === 'StepOne' ? (
<View>
{reasonText ? (
<View style={[a.gap_sm]}>
<Text style={[a.text_md, a.leading_snug]}>{reasonText}</Text>
<Text style={[a.text_md, a.leading_snug]}>
Don't have access to{' '}
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
{currentAccount?.email}
</Text>
?{' '}
<InlineLinkText
to="#"
label={_(msg`Change email address`)}
style={[a.text_md, a.leading_snug]}
onPress={e => {
e.preventDefault()
control.close(() => {
openModal({name: 'change-email'})
})
return false
}}>
<Trans>Change your email address</Trans>
</InlineLinkText>
.
</Text>
</View>
) : (
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
You'll receive an email at{' '}
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
{currentAccount?.email}
</Text>{' '}
to verify it's you.
</Trans>{' '}
<InlineLinkText
to="#"
label={_(msg`Change email address`)}
style={[a.text_md, a.leading_snug]}
onPress={e => {
e.preventDefault()
control.close(() => {
openModal({name: 'change-email'})
})
return false
}}>
<Trans>Need to change it?</Trans>
</InlineLinkText>
</Text>
)}
</View>
) : (
<Text style={[a.text_md, a.leading_snug]}>
{uiStrings[currentStep].message}
</Text>
)}
<Text style={[a.text_md, a.leading_snug]}>
{currentStep === 'StepOne' ? (
<>
<Trans>
You'll receive an email at{' '}
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
{currentAccount?.email}
</Text>{' '}
to verify it's you.
</Trans>{' '}
<InlineLinkText
to="#"
label={_(msg`Change email address`)}
style={[a.text_md, a.leading_snug]}
onPress={e => {
e.preventDefault()
control.close(() => {
openModal({name: 'change-email'})
})
return false
}}>
<Trans>Need to change it?</Trans>
</InlineLinkText>
</>
) : (
uiStrings[currentStep].message
)}
</Text>
</View>
{currentStep === 'StepTwo' ? (
<View>
@@ -0,0 +1,117 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppearanceToggleButtonGroup} from '#/screens/Settings/AppearanceSettings'
import {atoms as a, useAlf, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
import {Divider} from '#/components/Divider'
import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
import {Text} from '#/components/Typography'
export function NeueTypography() {
const t = useTheme()
const {_} = useLingui()
const nuxDialogs = useNuxDialogContext()
const control = Dialog.useDialogControl()
const {fonts} = useAlf()
Dialog.useAutoOpen(control, 3e3)
const onClose = React.useCallback(() => {
nuxDialogs.dismissActiveNux()
}, [nuxDialogs])
const onChangeFontFamily = React.useCallback(
(values: string[]) => {
const next = values[0] === 'system' ? 'system' : 'theme'
fonts.setFontFamily(next)
},
[fonts],
)
const onChangeFontScale = React.useCallback(
(values: string[]) => {
const next = values[0] || ('0' as any)
fonts.setFontScale(next)
},
[fonts],
)
return (
<Dialog.Outer control={control} onClose={onClose}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
<View style={[a.gap_xl]}>
<View style={[a.gap_md]}>
<Text style={[a.text_3xl, a.font_heavy]}>
<Trans>New font settings </Trans>
</Text>
<Text style={[a.text_lg, a.leading_snug, {maxWidth: 400}]}>
<Trans>
We're introducing a new theme font, along with adjustable font
sizing.
</Trans>
</Text>
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
You can adjust these in your Appearance Settings later.
</Trans>
</Text>
</View>
<Divider />
<View style={[a.gap_lg]}>
<AppearanceToggleButtonGroup
title={_(msg`Font`)}
description={_(
msg`For the best experience, we recommend using the theme font.`,
)}
icon={Aa}
items={[
{
label: _(msg`System`),
name: 'system',
},
{
label: _(msg`Theme`),
name: 'theme',
},
]}
values={[fonts.family]}
onChange={onChangeFontFamily}
/>
<AppearanceToggleButtonGroup
title={_(msg`Font size`)}
icon={TextSize}
items={[
{
label: _(msg`Smaller`),
name: '-1',
},
{
label: _(msg`Default`),
name: '0',
},
{
label: _(msg`Larger`),
name: '1',
},
]}
values={[fonts.scale]}
onChange={onChangeFontScale}
/>
</View>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+16 -11
View File
@@ -19,6 +19,7 @@ import {useOnboardingState} from '#/state/shell'
/*
* NUXs
*/
import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
import {IS_DEV} from '#/env'
@@ -35,7 +36,19 @@ const queuedNuxs: {
currentProfile: AppBskyActorDefs.ProfileViewDetailed
preferences: UsePreferencesQueryResponse
}) => boolean
}[] = []
}[] = [
{
id: Nux.NeueTypography,
enabled(props) {
if (props.currentProfile.createdAt) {
if (new Date(props.currentProfile.createdAt) < new Date('2024-10-09')) {
return true
}
}
return false
},
},
]
const Context = React.createContext<Context>({
activeNux: undefined,
@@ -53,14 +66,7 @@ export function NuxDialogs() {
const onboardingActive = useOnboardingState().isActive
const isLoading =
onboardingActive ||
!currentAccount ||
!preferences ||
!profile ||
// Profile isn't legit ready until createdAt is a real date.
!profile.createdAt ||
profile.createdAt === '0001-01-01T00:00:00.000Z' // TODO: Fix this in AppView.
!currentAccount || !preferences || !profile || onboardingActive
return !isLoading ? (
<Inner
currentAccount={currentAccount}
@@ -100,7 +106,6 @@ function Inner({
if (IS_DEV && typeof window !== 'undefined') {
// @ts-ignore
// eslint-disable-next-line react-compiler/react-compiler
window.clearNuxDialog = (id: Nux) => {
if (!IS_DEV || !id) return
removeNuxs([id])
@@ -169,7 +174,7 @@ function Inner({
return (
<Context.Provider value={ctx}>
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
{activeNux === Nux.NeueTypography && <NeueTypography />}
</Context.Provider>
)
}
+8 -11
View File
@@ -34,7 +34,7 @@ export function ActionsWrapper({
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.get()}],
transform: [{scale: scale.value}],
}))
const open = React.useCallback(() => {
@@ -46,29 +46,26 @@ export function ActionsWrapper({
const shrink = React.useCallback(() => {
'worklet'
cancelAnimation(scale)
scale.set(() => withTiming(1, {duration: 200}))
scale.value = withTiming(1, {duration: 200})
}, [scale])
const doubleTapGesture = Gesture.Tap()
.numberOfTaps(2)
.hitSlop(HITSLOP_10)
.onEnd(open)
.runOnJS(true)
const pressAndHoldGesture = Gesture.LongPress()
.onStart(() => {
'worklet'
scale.set(() =>
withTiming(1.05, {duration: 200}, finished => {
if (!finished) return
runOnJS(open)()
shrink()
}),
)
scale.value = withTiming(1.05, {duration: 200}, finished => {
if (!finished) return
runOnJS(open)()
shrink()
})
})
.onTouchesUp(shrink)
.onTouchesMove(shrink)
.cancelsTouchesInView(false)
.runOnJS(true)
const composedGestures = Gesture.Exclusive(
doubleTapGesture,
+3 -3
View File
@@ -42,12 +42,12 @@ export function ChatEmptyPill() {
const onPressIn = React.useCallback(() => {
if (isWeb) return
scale.set(() => withTiming(1.075, {duration: 100}))
scale.value = withTiming(1.075, {duration: 100})
}, [scale])
const onPressOut = React.useCallback(() => {
if (isWeb) return
scale.set(() => withTiming(1, {duration: 100}))
scale.value = withTiming(1, {duration: 100})
}, [scale])
const onPress = React.useCallback(() => {
@@ -61,7 +61,7 @@ export function ChatEmptyPill() {
}, [playHaptic, prompts.length])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.get()}],
transform: [{scale: scale.value}],
}))
return (
+1 -1
View File
@@ -62,7 +62,7 @@ export let MessageMenu = ({
message.text,
langPrefs.primaryLanguage,
)
openLink(translatorUrl, true)
openLink(translatorUrl)
}, [langPrefs.primaryLanguage, message.text, openLink])
const onDelete = React.useCallback(() => {
+15 -41
View File
@@ -3,18 +3,14 @@ import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useEmail} from '#/lib/hooks/useEmail'
import {NavigationProp} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {ButtonIcon} from '#/components/Button'
import {canBeMessaged} from '#/components/dms/util'
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
import {useDialogControl} from '../Dialog'
import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
import {Link} from '#/components/Link'
export function MessageProfileButton({
profile,
@@ -23,29 +19,15 @@ export function MessageProfileButton({
}) {
const {_} = useLingui()
const t = useTheme()
const navigation = useNavigation<NavigationProp>()
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const {data: convo, isPending} = useMaybeConvoForUser(profile.did)
const onPress = React.useCallback(() => {
if (!convo?.id) {
return
}
if (needsEmailVerification) {
verifyEmailControl.open()
return
}
if (convo && !convo.lastMessage) {
logEvent('chat:create', {logContext: 'ProfileHeader'})
}
logEvent('chat:open', {logContext: 'ProfileHeader'})
navigation.navigate('MessagesConversation', {conversation: convo.id})
}, [needsEmailVerification, verifyEmailControl, convo, navigation])
}, [convo])
if (isPending) {
// show pending state based on declaration
@@ -71,26 +53,18 @@ export function MessageProfileButton({
if (convo) {
return (
<>
<Button
accessibilityRole="button"
testID="dmBtn"
size="small"
color="secondary"
variant="solid"
shape="round"
label={_(msg`Message ${profile.handle}`)}
style={[a.justify_center]}
onPress={onPress}>
<ButtonIcon icon={Message} size="md" />
</Button>
<VerifyEmailDialog
reasonText={_(
msg`Before you may message another user, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</>
<Link
testID="dmBtn"
size="small"
color="secondary"
variant="solid"
shape="round"
label={_(msg`Message ${profile.handle}`)}
to={`/messages/${convo.id}`}
style={[a.justify_center]}
onPress={onPress}>
<ButtonIcon icon={Message} size="md" />
</Link>
)
} else {
return null
+1 -1
View File
@@ -147,7 +147,7 @@ function HeaderReady({
const isDeletedAccount = profile?.handle === 'missing.invalid'
const displayName = isDeletedAccount
? _(msg`Deleted Account`)
? 'Deleted Account'
: sanitizeDisplayName(
profile.displayName || profile.handle,
moderation.ui('displayName'),
+3 -3
View File
@@ -35,12 +35,12 @@ export function NewMessagesPill({
const onPressIn = React.useCallback(() => {
if (isWeb) return
scale.set(() => withTiming(1.075, {duration: 100}))
scale.value = withTiming(1.075, {duration: 100})
}, [scale])
const onPressOut = React.useCallback(() => {
if (isWeb) return
scale.set(() => withTiming(1, {duration: 100}))
scale.value = withTiming(1, {duration: 100})
}, [scale])
const onPress = React.useCallback(() => {
@@ -49,7 +49,7 @@ export function NewMessagesPill({
}, [onPressInner, playHaptic])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.get()}],
transform: [{scale: scale.value}],
}))
return (
+1 -19
View File
@@ -2,7 +2,6 @@ import React, {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useEmail} from '#/lib/hooks/useEmail'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
@@ -10,8 +9,6 @@ import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast'
import {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useDialogControl} from '#/components/Dialog'
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {SearchablePeopleList} from './SearchablePeopleList'
@@ -24,8 +21,6 @@ export function NewChat({
}) {
const t = useTheme()
const {_} = useLingui()
const {needsEmailVerification} = useEmail()
const verifyEmailControl = useDialogControl()
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
@@ -53,13 +48,7 @@ export function NewChat({
<>
<FAB
testID="newChatFAB"
onPress={() => {
if (needsEmailVerification) {
verifyEmailControl.open()
} else {
control.open()
}
}}
onPress={control.open}
icon={<Plus size="lg" fill={t.palette.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New chat`)}
@@ -73,13 +62,6 @@ export function NewChat({
onSelectChat={onCreateChat}
/>
</Dialog.Outer>
<VerifyEmailDialog
reasonText={_(
msg`Before you may message another user, you must first verify your email.`,
)}
control={verifyEmailControl}
/>
</>
)
}
@@ -278,7 +278,7 @@ export function SearchablePeopleList({
) : null}
</View>
<View style={web([a.pt_xs])}>
<View style={[, web([a.pt_xs])]}>
<SearchInput
inputRef={inputRef}
value={searchText}
@@ -313,7 +313,6 @@ export function SearchablePeopleList({
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
native({height: '100%'}),
]}
webInnerContentContainerStyle={a.py_0}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
keyboardDismissMode="on-drag"
/>
+1 -1
View File
@@ -57,7 +57,7 @@ export const SearchInput = React.forwardRef<TextInput, SearchInputProps>(
<View
style={[
a.absolute,
a.z_20,
a.z_10,
a.my_auto,
a.inset_0,
a.justify_center,
-1
View File
@@ -326,7 +326,6 @@ export function SuffixText({
<Text
accessibilityLabel={label}
accessibilityHint={accessibilityHint}
numberOfLines={1}
style={[
a.z_20,
a.pr_sm,
+2 -2
View File
@@ -15,8 +15,8 @@ export function useFollowMethods({
logContext,
}: {
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
logContext: LogEvents['profile:follow:sampled']['logContext'] &
LogEvents['profile:unfollow:sampled']['logContext']
}) {
const {_} = useLingui()
const requireAuth = useRequireAuth()
-5
View File
@@ -1,5 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Beaker_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M13.5 4a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM10 5a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM8 6a1 1 0 0 0 0 2v2.64q-.319.374-.711.8l-.129.142c-.312.342-.649.711-.974 1.092-.731.857-1.488 1.866-1.89 2.99A4.9 4.9 0 0 0 4 17.298 4.7 4.7 0 0 0 8.702 22h6.596A4.7 4.7 0 0 0 20 17.298c0-.575-.114-1.122-.297-1.634-.401-1.124-1.157-2.133-1.89-2.99-.324-.38-.66-.75-.973-1.092h0l-.129-.141c-.26-.286-.5-.55-.711-.8V8a1 1 0 1 0 0-2H8Zm2 5.35V8h4v3.35l.22.275c.306.383.661.777 1.013 1.163l.13.143h0c.315.345.628.688.93 1.042.372.435.704.861.974 1.28l-.159.025c-.845.13-1.838.242-2.581.222-.842-.022-1.475-.217-2.227-.454l-.027-.008c-.746-.235-1.61-.507-2.746-.538-.743-.02-1.617.064-2.38.165q.26-.342.56-.692c.302-.354.615-.697.93-1.042l.13-.143c.352-.386.707-.78 1.014-1.163L10 11.35Zm7.41 5.905q.316-.048.586-.095.004.07.004.138A2.7 2.7 0 0 1 15.298 20H8.702A2.7 2.7 0 0 1 6 17.298q0-.213.039-.434c.236-.043.53-.093.853-.142.845-.13 1.837-.242 2.581-.222.842.022 1.475.217 2.227.454l.027.008c.746.235 1.61.507 2.746.538.931.024 2.07-.113 2.937-.245Z',
})
-5
View File
@@ -1,5 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const CalendarClock_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M15.439 3.148a1 1 0 0 1 .41.645l.568 3.22a7 7 0 1 1-6.174 10.97L4.32 19.027a1 1 0 0 1-1.159-.811L1.078 6.398a1 1 0 0 1 .81-1.158l12.803-2.258a1 1 0 0 1 .748.166ZM9.325 16.114A7 7 0 0 1 9 14c0-1.56.51-3 1.372-4.164l-6.456 1.139 1.041 5.909 4.368-.77ZM3.568 9.005l10.833-1.91-.347-1.97L3.22 7.036l.347 1.97ZM16 9a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm0 2a1 1 0 0 1 1 1v1.586l1.374 1.374a1 1 0 0 1-1.414 1.414l-1.667-1.667A1 1 0 0 1 15 14v-2a1 1 0 0 1 1-1Z',
})
-4
View File
@@ -24,10 +24,6 @@ export const PersonPlus_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H15a3 3 0 1 1 0-6c0-.824.332-1.571.87-2.113C14.739 12.32 13.435 12 12 12Zm6 2a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z',
})
export const PersonPlus_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 14c-2.95 0-5.163 1.733-6.08 4.21a.47.47 0 0 0 .09.493.9.9 0 0 0 .687.297H11a1 1 0 1 1 0 2H6.697a2.9 2.9 0 0 1-2.219-1.011 2.46 2.46 0 0 1-.433-2.473C5.235 14.296 8.168 12 12 12c.787 0 1.54.097 2.252.282a1 1 0 1 1-.504 1.936A7 7 0 0 0 12 14Zm6 0a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z',
})
export const PersonGroup_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M8 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM4 7a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm13-1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Zm7.301 9.7c-.836-2.6-2.88-3.503-4.575-3.111a1 1 0 0 1-.451-1.949c2.815-.651 5.81.966 6.93 4.448a2.49 2.49 0 0 1-.506 2.43A2.92 2.92 0 0 1 20 20h-2a1 1 0 1 1 0-2h2a.92.92 0 0 0 .69-.295.49.49 0 0 0 .112-.505ZM8 14c-1.865 0-3.878 1.274-4.681 4.151a.57.57 0 0 0 .132.55c.15.171.4.299.695.299h7.708a.93.93 0 0 0 .695-.299.57.57 0 0 0 .132-.55C11.878 15.274 9.865 14 8 14Zm0-2c2.87 0 5.594 1.98 6.607 5.613.53 1.9-1.09 3.387-2.753 3.387H4.146c-1.663 0-3.283-1.487-2.753-3.387C2.406 13.981 5.129 12 8 12Z',
})
@@ -68,8 +68,6 @@ export function ContentHider({
if (hasAdultContentLabel) {
return false
}
// https://github.com/facebook/react/issues/31569
// eslint-disable-next-line react-compiler/react-compiler
hasAdultContentLabel = true
}
return true
@@ -22,7 +22,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
<View
style={[
a.flex_row,
a.gap_sm,
a.gap_md,
a.px_lg,
a.py_lg,
a.justify_between,
@@ -74,9 +74,10 @@ export function Buttons({
hideLabel?: string
}) {
const {_} = useLingui()
const {gtPhone} = useBreakpoints()
return (
<View style={[{minHeight: 35}, a.w_full]}>
<View style={[{minHeight: 35}, gtPhone ? undefined : a.w_full]}>
<ToggleButton.Group
label={_(
msg`Configure content filtering setting for category: ${name}`,
@@ -258,7 +259,7 @@ export function LabelerLabelPreference({
</Content>
{showConfig && (
<>
<View style={[gtPhone ? undefined : a.w_full]}>
{cantConfigure ? (
<View
style={[
@@ -289,7 +290,7 @@ export function LabelerLabelPreference({
hideLabel={hideLabel}
/>
)}
</>
</View>
)}
</Outer>
)
+2 -6
View File
@@ -128,9 +128,7 @@ async function loggedOutFetch({
headers: {'Accept-Language': contentLangs, ...labelersHeader},
},
)
let data = res.ok
? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema)
: null
let data = res.ok ? jsonStringToLex(await res.text()) : null
if (data?.feed?.length) {
return {
success: true,
@@ -145,9 +143,7 @@ async function loggedOutFetch({
}&limit=${limit}`,
{method: 'GET', headers: {'Accept-Language': '', ...labelersHeader}},
)
data = res.ok
? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema)
: null
data = res.ok ? jsonStringToLex(await res.text()) : null
if (data?.feed?.length) {
return {
success: true,
+1 -8
View File
@@ -1,18 +1,11 @@
import {AtUri} from '@atproto/api'
import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants'
import {isWeb} from '#/platform/detection'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
let debugTopics = ''
if (isWeb && typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search)
debugTopics = params.get('debug_topics') ?? ''
}
export function createBskyTopicsHeader(userInterests?: string) {
return {
'X-Bsky-Topics': debugTopics || userInterests || '',
'X-Bsky-Topics': userInterests || '',
}
}
+64 -161
View File
@@ -5,6 +5,7 @@ import {
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
AppBskyFeedPost,
AppBskyFeedPostgate,
AtUri,
BlobRef,
BskyAgent,
@@ -14,12 +15,8 @@ import {
RichText,
} from '@atproto/api'
import {TID} from '@atproto/common-web'
import * as dcbor from '@ipld/dag-cbor'
import {t} from '@lingui/macro'
import {QueryClient} from '@tanstack/react-query'
import {sha256} from 'js-sha256'
import {CID} from 'multiformats/cid'
import * as Hasher from 'multiformats/hashes/hasher'
import {isNetworkError} from '#/lib/strings/errors'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
@@ -56,65 +53,52 @@ export async function post(
opts: PostOpts,
) {
const thread = opts.thread
opts.onStateChange?.(t`Processing...`)
const draft = thread.posts[0] // TODO: Support threads.
let replyPromise:
| Promise<AppBskyFeedPost.Record['reply']>
| AppBskyFeedPost.Record['reply']
| undefined
opts.onStateChange?.(t`Processing...`)
// NB -- Do not await anything here to avoid waterfalls!
// Instead, store Promises which will be unwrapped as they're needed.
const rtPromise = resolveRT(agent, draft.richtext)
const embedPromise = resolveEmbed(
agent,
queryClient,
draft,
opts.onStateChange,
)
let replyPromise
if (opts.replyTo) {
// Not awaited to avoid waterfalls.
replyPromise = resolveReply(agent, opts.replyTo)
}
// set labels
let labels: ComAtprotoLabelDefs.SelfLabels | undefined
if (draft.labels.length) {
labels = {
$type: 'com.atproto.label.defs#selfLabels',
values: draft.labels.map(val => ({val})),
}
}
// add top 3 languages from user preferences if langs is provided
let langs = opts.langs
if (opts.langs) {
langs = opts.langs.slice(0, 3)
}
const did = agent.assertDid
const rkey = TID.nextStr()
const uri = `at://${agent.assertDid}/app.bsky.feed.post/${rkey}`
const date = new Date().toISOString()
const writes: ComAtprotoRepoApplyWrites.Create[] = []
const uris: string[] = []
let now = new Date()
let tid: TID | undefined
for (let i = 0; i < thread.posts.length; i++) {
const draft = thread.posts[i]
// Not awaited to avoid waterfalls.
const rtPromise = resolveRT(agent, draft.richtext)
const embedPromise = resolveEmbed(
agent,
queryClient,
draft,
opts.onStateChange,
)
let labels: ComAtprotoLabelDefs.SelfLabels | undefined
if (draft.labels.length) {
labels = {
$type: 'com.atproto.label.defs#selfLabels',
values: draft.labels.map(val => ({val})),
}
}
// The sorting behavior for multiple posts sharing the same createdAt time is
// undefined, so what we'll do here is increment the time by 1 for every post
now.setMilliseconds(now.getMilliseconds() + 1)
tid = TID.next(tid)
const rkey = tid.toString()
const uri = `at://${did}/app.bsky.feed.post/${rkey}`
uris.push(uri)
// Create post record
{
const rt = await rtPromise
const embed = await embedPromise
const reply = await replyPromise
const record: AppBskyFeedPost.Record = {
// IMPORTANT: $type has to exist, CID is calculated with the `$type` field
// present and will produce the wrong CID if you omit it.
$type: 'app.bsky.feed.post',
createdAt: now.toISOString(),
createdAt: date,
text: rt.text,
facets: rt.facets,
reply,
@@ -122,52 +106,49 @@ export async function post(
langs,
labels,
}
writes.push({
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.feed.post',
rkey: rkey,
value: record,
})
}
if (i === 0 && thread.threadgate.some(tg => tg.type !== 'everybody')) {
writes.push({
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.feed.threadgate',
rkey: rkey,
value: createThreadgateRecord({
createdAt: now.toISOString(),
post: uri,
allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate),
}),
})
// Create threadgate record
if (thread.threadgate.some(tg => tg.type !== 'everybody')) {
const record = createThreadgateRecord({
createdAt: date,
post: uri,
allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate),
})
writes.push({
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.feed.threadgate',
rkey: rkey,
value: record,
})
}
// Create postgate record
if (
thread.postgate.embeddingRules?.length ||
thread.postgate.detachedEmbeddingUris?.length
) {
const record: AppBskyFeedPostgate.Record = {
...thread.postgate,
$type: 'app.bsky.feed.postgate',
createdAt: date,
post: uri,
}
if (
thread.postgate.embeddingRules?.length ||
thread.postgate.detachedEmbeddingUris?.length
) {
writes.push({
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.feed.postgate',
rkey: rkey,
value: {
...thread.postgate,
$type: 'app.bsky.feed.postgate',
createdAt: now.toISOString(),
post: uri,
},
})
}
// Prepare a ref to the current post for the next post in the thread.
const ref = {
cid: await computeCid(record),
uri,
}
replyPromise = {
root: reply?.root ?? ref,
parent: ref,
}
writes.push({
$type: 'com.atproto.repo.applyWrites#create',
collection: 'app.bsky.feed.postgate',
rkey: rkey,
value: record,
})
}
try {
@@ -189,7 +170,7 @@ export async function post(
}
}
return {uris}
return {uri}
}
async function resolveRT(agent: BskyAgent, richtext: RichText) {
@@ -401,81 +382,3 @@ async function resolveRecord(
}
return resolvedLink.record
}
// The built-in hashing functions from multiformats (`multiformats/hashes/sha2`)
// are meant for Node.js, this is the cross-platform equivalent.
const mf_sha256 = Hasher.from({
name: 'sha2-256',
code: 0x12,
encode: input => {
const digest = sha256.arrayBuffer(input)
return new Uint8Array(digest)
},
})
async function computeCid(record: AppBskyFeedPost.Record): Promise<string> {
// IMPORTANT: `prepareObject` prepares the record to be hashed by removing
// fields with undefined value, and converting BlobRef instances to the
// right IPLD representation.
const prepared = prepareForHashing(record)
// 1. Encode the record into DAG-CBOR format
const encoded = dcbor.encode(prepared)
// 2. Hash the record in SHA-256 (code 0x12)
const digest = await mf_sha256.digest(encoded)
// 3. Create a CIDv1, specifying DAG-CBOR as content (code 0x71)
const cid = CID.createV1(0x71, digest)
// 4. Get the Base32 representation of the CID (`b` prefix)
return cid.toString()
}
// Returns a transformed version of the object for use in DAG-CBOR.
function prepareForHashing(v: any): any {
// IMPORTANT: BlobRef#ipld() returns the correct object we need for hashing,
// the API client will convert this for you but we're hashing in the client,
// so we need it *now*.
if (v instanceof BlobRef) {
return v.ipld()
}
// Walk through arrays
if (Array.isArray(v)) {
let pure = true
const mapped = v.map(value => {
if (value !== (value = prepareForHashing(value))) {
pure = false
}
return value
})
return pure ? v : mapped
}
// Walk through plain objects
if (isPlainObject(v)) {
const obj: any = {}
let pure = true
for (const key in v) {
let value = v[key]
// `value` is undefined
if (value === undefined) {
pure = false
continue
}
// `prepareObject` returned a value that's different from what we had before
if (value !== (value = prepareForHashing(value))) {
pure = false
}
obj[key] = value
}
// Return as is if we haven't needed to tamper with anything
return pure ? v : obj
}
return v
}
function isPlainObject(v: any): boolean {
if (typeof v !== 'object' || v === null) {
return false
}
const proto = Object.getPrototypeOf(v)
return proto === Object.prototype || proto === null
}
+1 -10
View File
@@ -21,15 +21,7 @@ export const STARTER_PACK_MAX_SIZE = 150
// code and update this number with each release until we can get the
// server route done.
// -prf
export const JOINED_THIS_WEEK = 3500000 // estimate as of 11/16/24
export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at
'did:plc:fpruhuo22xkm5o7ttr2ktxdo': true, // danabra.mov
'did:plc:p2cp5gopk7mgjegy6wadk3ep': true, // samuel.bsky.team
'did:plc:ragtjsm2j2vknwkz3zp4oxrd': true, // pfrazee.com
'did:plc:vpkhqolt662uhesyj6nxm7ys': true, // why.bsky.team
}
export const JOINED_THIS_WEEK = 650000 // estimate as of 10/28/24
const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
export function FEEDBACK_FORM_URL({
@@ -103,7 +95,6 @@ export const HITSLOP_10 = createHitslop(10)
export const HITSLOP_20 = createHitslop(20)
export const HITSLOP_30 = createHitslop(30)
export const POST_CTRL_HITSLOP = {top: 5, bottom: 10, left: 10, right: 10}
export const LANG_DROPDOWN_HITSLOP = {top: 10, bottom: 10, left: 4, right: 4}
export const BACK_HITSLOP = HITSLOP_30
export const MAX_POST_LINES = 25
+3 -3
View File
@@ -91,15 +91,15 @@ export function CountWheel({
likeCount,
big,
isLiked,
hasBeenToggled,
isToggle,
}: {
likeCount: number
big?: boolean
isLiked: boolean
hasBeenToggled: boolean
isToggle: boolean
}) {
const t = useTheme()
const shouldAnimate = !useReducedMotion() && hasBeenToggled
const shouldAnimate = !useReducedMotion() && isToggle
const shouldRoll = decideShouldRoll(isLiked, likeCount)
// Incrementing the key will cause the `Animated.View` to re-render, with the newly selected entering/exiting
+3 -3
View File
@@ -39,15 +39,15 @@ export function CountWheel({
likeCount,
big,
isLiked,
hasBeenToggled,
isToggle,
}: {
likeCount: number
big?: boolean
isLiked: boolean
hasBeenToggled: boolean
isToggle: boolean
}) {
const t = useTheme()
const shouldAnimate = !useReducedMotion() && hasBeenToggled
const shouldAnimate = !useReducedMotion() && isToggle
const shouldRoll = decideShouldRoll(isLiked, likeCount)
const countView = React.useRef<HTMLDivElement>(null)
+41 -43
View File
@@ -61,7 +61,7 @@ export function GestureActionView({
const clampedTransX = useDerivedValue(() => {
const min = actions.leftFirst ? -MAX_WIDTH : 0
const max = actions.rightFirst ? MAX_WIDTH : 0
return clamp(transX.get(), min, max)
return clamp(transX.value, min, max)
})
const iconScale = useSharedValue(1)
@@ -75,23 +75,21 @@ export function GestureActionView({
return
}
iconScale.set(() =>
withSequence(
withTiming(1.2, {duration: 175}),
withTiming(1, {duration: 100}),
),
iconScale.value = withSequence(
withTiming(1.2, {duration: 175}),
withTiming(1, {duration: 100}),
)
}
useAnimatedReaction(
() => transX,
() => {
if (transX.get() === 0) {
if (transX.value === 0) {
runOnJS(setActiveAction)(null)
} else if (transX.get() < 0) {
} else if (transX.value < 0) {
if (
actions.leftSecond &&
transX.get() <= -actions.leftSecond.threshold
transX.value <= -actions.leftSecond.threshold
) {
if (activeAction !== 'leftSecond') {
runOnJS(setActiveAction)('leftSecond')
@@ -99,10 +97,10 @@ export function GestureActionView({
} else if (activeAction !== 'leftFirst') {
runOnJS(setActiveAction)('leftFirst')
}
} else if (transX.get() > 0) {
} else if (transX.value > 0) {
if (
actions.rightSecond &&
transX.get() > actions.rightSecond.threshold
transX.value > actions.rightSecond.threshold
) {
if (activeAction !== 'rightSecond') {
runOnJS(setActiveAction)('rightSecond')
@@ -121,44 +119,44 @@ export function GestureActionView({
.activeOffsetY([-200, 200])
.onStart(() => {
'worklet'
isActive.set(true)
isActive.value = true
})
.onChange(e => {
'worklet'
transX.set(e.translationX)
transX.value = e.translationX
if (e.translationX < 0) {
// Left side
if (actions.leftSecond) {
if (
e.translationX <= -actions.leftSecond.threshold &&
!hitSecond.get()
!hitSecond.value
) {
runPopAnimation()
runOnJS(haptic)()
hitSecond.set(true)
hitSecond.value = true
} else if (
hitSecond.get() &&
hitSecond.value &&
e.translationX > -actions.leftSecond.threshold
) {
runPopAnimation()
hitSecond.set(false)
hitSecond.value = false
}
}
if (!hitSecond.get() && actions.leftFirst) {
if (!hitSecond.value && actions.leftFirst) {
if (
e.translationX <= -actions.leftFirst.threshold &&
!hitFirst.get()
!hitFirst.value
) {
runPopAnimation()
runOnJS(haptic)()
hitFirst.set(true)
hitFirst.value = true
} else if (
hitFirst.get() &&
hitFirst.value &&
e.translationX > -actions.leftFirst.threshold
) {
hitFirst.set(false)
hitFirst.value = false
}
}
} else if (e.translationX > 0) {
@@ -166,33 +164,33 @@ export function GestureActionView({
if (actions.rightSecond) {
if (
e.translationX >= actions.rightSecond.threshold &&
!hitSecond.get()
!hitSecond.value
) {
runPopAnimation()
runOnJS(haptic)()
hitSecond.set(true)
hitSecond.value = true
} else if (
hitSecond.get() &&
hitSecond.value &&
e.translationX < actions.rightSecond.threshold
) {
runPopAnimation()
hitSecond.set(false)
hitSecond.value = false
}
}
if (!hitSecond.get() && actions.rightFirst) {
if (!hitSecond.value && actions.rightFirst) {
if (
e.translationX >= actions.rightFirst.threshold &&
!hitFirst.get()
!hitFirst.value
) {
runPopAnimation()
runOnJS(haptic)()
hitFirst.set(true)
hitFirst.value = true
} else if (
hitFirst.get() &&
hitFirst.value &&
e.translationX < actions.rightFirst.threshold
) {
hitFirst.set(false)
hitFirst.value = false
}
}
}
@@ -200,29 +198,29 @@ export function GestureActionView({
.onEnd(e => {
'worklet'
if (e.translationX < 0) {
if (hitSecond.get() && actions.leftSecond) {
if (hitSecond.value && actions.leftSecond) {
runOnJS(actions.leftSecond.action)()
} else if (hitFirst.get() && actions.leftFirst) {
} else if (hitFirst.value && actions.leftFirst) {
runOnJS(actions.leftFirst.action)()
}
} else if (e.translationX > 0) {
if (hitSecond.get() && actions.rightSecond) {
if (hitSecond.value && actions.rightSecond) {
runOnJS(actions.rightSecond.action)()
} else if (hitSecond.get() && actions.rightFirst) {
} else if (hitSecond.value && actions.rightFirst) {
runOnJS(actions.rightFirst.action)()
}
}
transX.set(() => withTiming(0, {duration: 200}))
hitFirst.set(false)
hitSecond.set(false)
isActive.set(false)
transX.value = withTiming(0, {duration: 200})
hitFirst.value = false
hitSecond.value = false
isActive.value = false
})
const composedGesture = Gesture.Simultaneous(panGesture)
const animatedSliderStyle = useAnimatedStyle(() => {
return {
transform: [{translateX: clampedTransX.get()}],
transform: [{translateX: clampedTransX.value}],
}
})
@@ -276,7 +274,7 @@ export function GestureActionView({
const animatedBackgroundStyle = useAnimatedStyle(() => {
return {
backgroundColor: interpolateColor(
clampedTransX.get(),
clampedTransX.value,
interpolation.inputRange,
// @ts-expect-error - Weird type expected by reanimated, but this is okay
interpolation.outputRange,
@@ -285,10 +283,10 @@ export function GestureActionView({
})
const animatedIconStyle = useAnimatedStyle(() => {
const absTransX = Math.abs(clampedTransX.get())
const absTransX = Math.abs(clampedTransX.value)
return {
opacity: interpolate(absTransX, [0, 75], [0.15, 1]),
transform: [{scale: iconScale.get()}],
transform: [{scale: iconScale.value}],
}
})
+10 -6
View File
@@ -71,15 +71,15 @@ const circle2Keyframe = new Keyframe({
export function AnimatedLikeIcon({
isLiked,
big,
hasBeenToggled,
isToggle,
}: {
isLiked: boolean
big?: boolean
hasBeenToggled: boolean
isToggle: boolean
}) {
const t = useTheme()
const size = big ? 22 : 18
const shouldAnimate = !useReducedMotion() && hasBeenToggled
const shouldAnimate = !useReducedMotion() && isToggle
return (
<View>
@@ -95,10 +95,12 @@ export function AnimatedLikeIcon({
width={size}
/>
)}
{isLiked && shouldAnimate ? (
{isLiked ? (
<>
<Animated.View
entering={circle1Keyframe.duration(300)}
entering={
shouldAnimate ? circle1Keyframe.duration(300) : undefined
}
style={{
position: 'absolute',
backgroundColor: s.likeColor.color,
@@ -112,7 +114,9 @@ export function AnimatedLikeIcon({
}}
/>
<Animated.View
entering={circle2Keyframe.duration(300)}
entering={
shouldAnimate ? circle2Keyframe.duration(300) : undefined
}
style={{
position: 'absolute',
backgroundColor: t.atoms.bg.backgroundColor,
+3 -3
View File
@@ -41,15 +41,15 @@ const circle2Keyframe = [
export function AnimatedLikeIcon({
isLiked,
big,
hasBeenToggled,
isToggle,
}: {
isLiked: boolean
big?: boolean
hasBeenToggled: boolean
isToggle: boolean
}) {
const t = useTheme()
const size = big ? 22 : 18
const shouldAnimate = !useReducedMotion() && hasBeenToggled
const shouldAnimate = !useReducedMotion() && isToggle
const prevIsLiked = React.useRef(isLiked)
const likeIconRef = React.useRef<HTMLDivElement>(null)
+8 -5
View File
@@ -2,6 +2,7 @@ import React from 'react'
import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
import Animated, {
cancelAnimation,
runOnJS,
useAnimatedStyle,
useReducedMotion,
useSharedValue,
@@ -31,25 +32,27 @@ export function PressableScale({
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.get()}],
transform: [{scale: scale.value}],
}))
return (
<AnimatedPressable
accessibilityRole="button"
onPressIn={e => {
'worklet'
if (onPressIn) {
onPressIn(e)
runOnJS(onPressIn)(e)
}
cancelAnimation(scale)
scale.set(() => withTiming(targetScale, {duration: 100}))
scale.value = withTiming(targetScale, {duration: 100})
}}
onPressOut={e => {
'worklet'
if (onPressOut) {
onPressOut(e)
runOnJS(onPressOut)(e)
}
cancelAnimation(scale)
scale.set(() => withTiming(1, {duration: 100}))
scale.value = withTiming(1, {duration: 100})
}}
style={[!reducedMotion && animatedStyle, style]}
{...rest}>
+14
View File
@@ -0,0 +1,14 @@
import React from 'react'
import {choose} from '#/lib/functions'
import {useTheme} from '#/lib/ThemeContext'
export function useCustomPalette<T>({light, dark}: {light: T; dark: T}) {
const theme = useTheme()
return React.useMemo(() => {
return choose<T, Record<string, T>>(theme.colorScheme, {
dark,
light,
})
}, [theme.colorScheme, dark, light])
}
-32
View File
@@ -1,32 +0,0 @@
import {useServiceConfigQuery} from '#/state/queries/email-verification-required'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {BSKY_SERVICE} from '../constants'
import {getHostnameFromUrl} from '../strings/url-helpers'
export function useEmail() {
const {currentAccount} = useSession()
const {data: serviceConfig} = useServiceConfigQuery()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const checkEmailConfirmed = !!serviceConfig?.checkEmailConfirmed
// Date set for 11 AM PST on the 18th of November
const isNewEnough =
!!profile?.createdAt &&
Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z')
const isSelfHost =
currentAccount &&
getHostnameFromUrl(currentAccount.service) !==
getHostnameFromUrl(BSKY_SERVICE)
const needsEmailVerification =
!isSelfHost &&
checkEmailConfirmed &&
!currentAccount?.emailConfirmed &&
isNewEnough
return {needsEmailVerification}
}
+9 -11
View File
@@ -10,16 +10,15 @@ export function useMinimalShellHeaderTransform() {
const {headerHeight} = useShellLayout()
const headerTransform = useAnimatedStyle(() => {
const headerModeValue = headerMode.get()
return {
pointerEvents: headerModeValue === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - headerModeValue, 2),
pointerEvents: headerMode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - headerMode.value, 2),
transform: [
{
translateY: interpolate(
headerModeValue,
headerMode.value,
[0, 1],
[0, -headerHeight.get()],
[0, -headerHeight.value],
),
},
],
@@ -34,16 +33,15 @@ export function useMinimalShellFooterTransform() {
const {footerHeight} = useShellLayout()
const footerTransform = useAnimatedStyle(() => {
const footerModeValue = footerMode.get()
return {
pointerEvents: footerModeValue === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - footerModeValue, 2),
pointerEvents: footerMode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - footerMode.value, 2),
transform: [
{
translateY: interpolate(
footerModeValue,
footerMode.value,
[0, 1],
[0, footerHeight.get()],
[0, footerHeight.value],
),
},
],
@@ -60,7 +58,7 @@ export function useMinimalShellFabTransform() {
return {
transform: [
{
translateY: interpolate(footerMode.get(), [0, 1], [-44, 0]),
translateY: interpolate(footerMode.value, [0, 1], [-44, 0]),
},
],
}
+32
View File
@@ -0,0 +1,32 @@
import * as React from 'react'
/**
* Helper hook to run persistent timers on views
*/
export function useTimer(time: number, handler: () => void) {
const timer = React.useRef<undefined | NodeJS.Timeout>(undefined)
// function to restart the timer
const reset = React.useCallback(() => {
if (timer.current) {
clearTimeout(timer.current)
}
timer.current = setTimeout(handler, time)
}, [time, timer, handler])
// function to cancel the timer
const cancel = React.useCallback(() => {
if (timer.current) {
clearTimeout(timer.current)
timer.current = undefined
}
}, [timer])
// start the timer immediately
React.useEffect(() => {
reset()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return [reset, cancel]
}
+9 -13
View File
@@ -9,24 +9,20 @@ if ('scrollRestoration' in history) {
function createInitialScrollState() {
return {
// Not used for rendering.
// Treat it as a ref so that we can mutate it without upsetting the compiler.
current: {
scrollYs: new Map(),
focusedKey: null as string | null,
},
scrollYs: new Map(),
focusedKey: null as string | null,
}
}
export function useWebScrollRestoration() {
const [ref] = useState(createInitialScrollState)
const [state] = useState(createInitialScrollState)
const navigation = useNavigation()
useEffect(() => {
function onDispatch() {
if (ref.current.focusedKey) {
if (state.focusedKey) {
// Remember where we were for later.
ref.current.scrollYs.set(ref.current.focusedKey, window.scrollY)
state.scrollYs.set(state.focusedKey, window.scrollY)
// TODO: Strictly speaking, this is a leak. We never clean up.
// This is because I'm not sure when it's appropriate to clean it up.
// It doesn't seem like popstate is enough because it can still Forward-Back again.
@@ -40,17 +36,17 @@ export function useWebScrollRestoration() {
return () => {
navigation.removeListener('__unsafe_action__' as any, onDispatch)
}
}, [ref, navigation])
}, [state, navigation])
const screenListeners = useMemo(
() => ({
focus(e: EventArg<'focus', boolean | undefined, unknown>) {
const scrollY = ref.current.scrollYs.get(e.target) ?? 0
const scrollY = state.scrollYs.get(e.target) ?? 0
window.scrollTo(0, scrollY)
ref.current.focusedKey = e.target ?? null
state.focusedKey = e.target ?? null
},
}),
[ref],
[state],
)
return screenListeners
}
+35
View File
@@ -0,0 +1,35 @@
import {Image} from 'react-native'
import type {Dimensions} from '#/lib/media/types'
const sizes: Map<string, Dimensions> = new Map()
const activeRequests: Map<string, Promise<Dimensions>> = new Map()
export function get(uri: string): Dimensions | undefined {
return sizes.get(uri)
}
export async function fetch(uri: string): Promise<Dimensions> {
const Dimensions = sizes.get(uri)
if (Dimensions) {
return Dimensions
}
const prom =
activeRequests.get(uri) ||
new Promise<Dimensions>(resolve => {
Image.getSize(
uri,
(width: number, height: number) => resolve({width, height}),
(err: any) => {
console.error('Failed to fetch image dimensions for', uri, err)
resolve({width: 0, height: 0})
},
)
})
activeRequests.set(uri, prom)
const res = await prom
activeRequests.delete(uri)
sizes.set(uri, res)
return res
}
+1 -1
View File
@@ -3,7 +3,7 @@ import {ImagePickerAsset} from 'expo-image-picker'
import {VideoTooLargeError} from '#/lib/media/video/errors'
import {CompressedVideo} from './types'
const MAX_VIDEO_SIZE = 1000 * 1000 * 50 // 50mb
const MAX_VIDEO_SIZE = 1024 * 1024 * 50 // 50mb
// doesn't actually compress, converts to ArrayBuffer
export async function compressVideo(
+1 -1
View File
@@ -48,7 +48,7 @@ export type CommonNavigatorParams = {
Hashtag: {tag: string; author?: string}
MessagesConversation: {conversation: string; embed?: string}
MessagesSettings: undefined
NotificationSettings: undefined
NotificationsSettings: undefined
Feeds: undefined
Start: {name: string; rkey: string}
StarterPack: {name: string; rkey: string; new?: boolean}
+15 -20
View File
@@ -21,11 +21,11 @@ export type LogEvents = {
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home'
status: 'granted' | 'denied' | 'undetermined'
}
'state:background': {
'state:background:sampled': {
secondsActive: number
}
'state:foreground': {}
'router:navigate:notifications': {}
'state:foreground:sampled': {}
'router:navigate:notifications:sampled': {}
'deepLink:referrerReceived': {
to: string
referrer: string
@@ -76,7 +76,7 @@ export type LogEvents = {
'onboarding:finished:avatarResult': {
avatarResult: 'default' | 'created' | 'uploaded'
}
'home:feedDisplayed': {
'home:feedDisplayed:sampled': {
feedUrl: string
feedType: string
index: number
@@ -87,12 +87,12 @@ export type LogEvents = {
| 'desktop-sidebar-click'
| 'starter-pack-initial-feed'
}
'feed:endReached': {
'feed:endReached:sampled': {
feedUrl: string
feedType: string
itemCount: number
}
'feed:refresh': {
'feed:refresh:sampled': {
feedUrl: string
feedType: string
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
@@ -103,13 +103,13 @@ export type LogEvents = {
'discover:showLess': {
feedContext: string
}
'discover:clickthrough': {
'discover:clickthrough:sampled': {
count: number
}
'discover:engaged': {
'discover:engaged:sampled': {
count: number
}
'discover:seen': {
'discover:seen:sampled': {
count: number
}
@@ -122,37 +122,32 @@ export type LogEvents = {
'post:create': {
imageCount: number
isReply: boolean
isPartOfThread: boolean
hasLink: boolean
hasQuote: boolean
langs: string
logContext: 'Composer'
}
'thread:create': {
postCount: number
isReply: boolean
}
'post:like': {
'post:like:sampled': {
doesLikerFollowPoster: boolean | undefined
doesPosterFollowLiker: boolean | undefined
likerClout: number | undefined
postClout: number | undefined
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:repost': {
'post:repost:sampled': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:unlike': {
'post:unlike:sampled': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:unrepost': {
'post:unrepost:sampled': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:mute': {}
'post:unmute': {}
'post:pin': {}
'post:unpin': {}
'profile:follow': {
'profile:follow:sampled': {
didBecomeMutual: boolean | undefined
followeeClout: number | undefined
followerClout: number | undefined
@@ -169,7 +164,7 @@ export type LogEvents = {
| 'FeedInterstitial'
| 'ProfileHeaderSuggestedFollows'
}
'profile:unfollow': {
'profile:unfollow:sampled': {
logContext:
| 'RecommendedFollowsItem'
| 'PostThreadItem'
+39 -3
View File
@@ -59,7 +59,6 @@ function createStatsigOptions(prefetchUsers: StatsigUser[]) {
initTimeoutMs: 1,
// Get fresh flags for other accounts as well, if any.
prefetchUsers,
api: 'https://events.bsky.app/v2',
}
}
@@ -90,14 +89,51 @@ export function toClout(n: number | null | undefined): number | undefined {
}
}
const DOWNSAMPLE_RATE = 0.99 // 99% likely
const DOWNSAMPLED_EVENTS: Set<keyof LogEvents> = new Set([
'router:navigate:notifications:sampled',
'state:background:sampled',
'state:foreground:sampled',
'home:feedDisplayed:sampled',
'feed:endReached:sampled',
'feed:refresh:sampled',
'discover:clickthrough:sampled',
'discover:engaged:sampled',
'discover:seen:sampled',
'post:like:sampled',
'post:unlike:sampled',
'post:repost:sampled',
'post:unrepost:sampled',
'profile:follow:sampled',
'profile:unfollow:sampled',
])
const isDownsampledSession = Math.random() < DOWNSAMPLE_RATE
export function logEvent<E extends keyof LogEvents>(
eventName: E & string,
rawMetadata: LogEvents[E] & FlatJSONRecord,
) {
try {
if (
process.env.NODE_ENV === 'development' &&
eventName.endsWith(':sampled') &&
!DOWNSAMPLED_EVENTS.has(eventName)
) {
logger.error(
'Did you forget to add ' + eventName + ' to DOWNSAMPLED_EVENTS?',
)
}
const isDownsampledEvent = DOWNSAMPLED_EVENTS.has(eventName)
if (isDownsampledSession && isDownsampledEvent) {
return
}
const fullMetadata = {
...rawMetadata,
} as Record<string, string> // Statsig typings are unnecessarily strict here.
if (isDownsampledEvent) {
fullMetadata.downsampleRate = DOWNSAMPLE_RATE.toString()
}
fullMetadata.routeName = getCurrentRouteName() ?? '(Uninitialized)'
if (Statsig.initializeCalled()) {
Statsig.logEvent(eventName, null, fullMetadata)
@@ -196,13 +232,13 @@ AppState.addEventListener('change', (state: AppStateStatus) => {
lastState = state
if (state === 'active') {
lastActive = performance.now()
logEvent('state:foreground', {})
logEvent('state:foreground:sampled', {})
} else {
let secondsActive = 0
if (lastActive != null) {
secondsActive = Math.round((performance.now() - lastActive) / 1e3)
lastActive = null
logEvent('state:background', {
logEvent('state:background:sampled', {
secondsActive,
})
}
+14 -5
View File
@@ -25,6 +25,7 @@ export const embedPlayerSources = [
'giphy',
'tenor',
'flickr',
'github',
] as const
export type EmbedPlayerSource = (typeof embedPlayerSources)[number]
@@ -45,6 +46,7 @@ export type EmbedPlayerType =
| 'giphy_gif'
| 'tenor_gif'
| 'flickr_album'
| 'github_gist'
export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
youtube: 'YouTube',
@@ -57,6 +59,7 @@ export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
appleMusic: 'Apple Music',
soundcloud: 'SoundCloud',
flickr: 'Flickr',
github: 'GitHub',
}
export interface EmbedPlayerParams {
@@ -88,9 +91,7 @@ export function parseEmbedPlayerFromUrl(
// youtube
if (urlp.hostname === 'youtu.be') {
const videoId = urlp.pathname.split('/')[1]
const t = urlp.searchParams.get('t') ?? '0'
const seek = encodeURIComponent(t.replace(/s$/, ''))
const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0)
if (videoId) {
return {
type: 'youtube_video',
@@ -113,8 +114,7 @@ export function parseEmbedPlayerFromUrl(
isShorts || isLive
? shortOrLiveVideoId
: (urlp.searchParams.get('v') as string)
const t = urlp.searchParams.get('t') ?? '0'
const seek = encodeURIComponent(t.replace(/s$/, ''))
const seek = encodeURIComponent(urlp.searchParams.get('t') ?? 0)
if (videoId) {
return {
@@ -451,6 +451,15 @@ export function parseEmbedPlayerFromUrl(
return undefined
}
}
if (urlp.hostname === 'gist.github.com') {
const id = urlp.pathname.replace(/^\//, '').split('/')[1]
return {
type: 'github_gist',
source: 'github',
playerUri: id,
}
}
}
export function getPlayerAspect({
+4 -10
View File
@@ -1,5 +1,3 @@
import {t} from '@lingui/macro'
export function cleanError(str: any): string {
if (!str) {
return ''
@@ -8,17 +6,13 @@ export function cleanError(str: any): string {
str = str.toString()
}
if (isNetworkError(str)) {
return t`Unable to connect. Please check your internet connection and try again.`
return 'Unable to connect. Please check your internet connection and try again.'
}
if (
str.includes('Upstream Failure') ||
str.includes('NotEnoughResources') ||
str.includes('pipethrough network error')
) {
return t`The server appears to be experiencing issues. Please try again in a few moments.`
if (str.includes('Upstream Failure')) {
return 'The server appears to be experiencing issues. Please try again in a few moments.'
}
if (str.includes('Bad token scope')) {
return t`This feature is not available while using an App Password. Please sign in with your main password.`
return 'This feature is not available while using an App Password. Please sign in with your main password.'
}
if (str.startsWith('Error: ')) {
return str.slice('Error: '.length)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More