diff --git a/__tests__/lib/__mocks__/exampleComHtml.ts b/__tests__/lib/__mocks__/exampleComHtml.ts
deleted file mode 100644
index 6633e40ca5..0000000000
--- a/__tests__/lib/__mocks__/exampleComHtml.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-export const exampleComHtml = `
-
-
- Example Domain
-
-
-
-
-
-
-
-
-
-
-
Example Domain
-
This domain is for use in illustrative examples in documents. You may use this
- domain in literature without prior coordination or asking for permission.
-
More information...
-
-
-`
diff --git a/__tests__/lib/__mocks__/tiktokHtml.ts b/__tests__/lib/__mocks__/tiktokHtml.ts
deleted file mode 100644
index fa3d112836..0000000000
--- a/__tests__/lib/__mocks__/tiktokHtml.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const tiktokHtml = `
-Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #bal... | TikTok
-`
diff --git a/__tests__/lib/__mocks__/youtubeChannelHtml.ts b/__tests__/lib/__mocks__/youtubeChannelHtml.ts
deleted file mode 100644
index cc71995c45..0000000000
--- a/__tests__/lib/__mocks__/youtubeChannelHtml.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-export const youtubeChannelHtml = `
-
-
About Press Copyright Contact us Creators Advertise Developers Terms Privacy Policy & Safety How YouTube works Test new features penguinz0 - YouTube
-
-`
diff --git a/__tests__/lib/__mocks__/youtubeHtml.ts b/__tests__/lib/__mocks__/youtubeHtml.ts
deleted file mode 100644
index 7fd9f819da..0000000000
--- a/__tests__/lib/__mocks__/youtubeHtml.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export const youtubeHTML = `
-
-YouTube
-
-`
diff --git a/__tests__/lib/errors.test.ts b/__tests__/lib/errors.test.ts
index 39e8d189e0..e721396845 100644
--- a/__tests__/lib/errors.test.ts
+++ b/__tests__/lib/errors.test.ts
@@ -9,11 +9,11 @@ describe('isNetworkError', () => {
]
const outputs = [true, false, false, true]
- it('correctly distinguishes network errors', () => {
- for (let i = 0; i < inputs.length; i++) {
- const input = inputs[i]
- const result = isNetworkError(input)
- expect(result).toEqual(outputs[i])
- }
- })
+ for (let i = 0; i < inputs.length; i++) {
+ const input = inputs[i]
+ const output = outputs[i]
+ it(`correctly distinguishes network errors for ${input}`, () => {
+ expect(isNetworkError(input)).toEqual(output)
+ })
+ }
})
diff --git a/__tests__/lib/extractHtmlMeta.test.ts b/__tests__/lib/extractHtmlMeta.test.ts
deleted file mode 100644
index cdd2a33848..0000000000
--- a/__tests__/lib/extractHtmlMeta.test.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import {extractHtmlMeta} from '../../src/lib/link-meta/html'
-import {exampleComHtml} from './__mocks__/exampleComHtml'
-import {youtubeHTML} from './__mocks__/youtubeHtml'
-import {tiktokHtml} from './__mocks__/tiktokHtml'
-import {youtubeChannelHtml} from './__mocks__/youtubeChannelHtml'
-
-describe('extractHtmlMeta', () => {
- const cases = [
- ['', {}],
- ['nothing', {}],
- ['title ', {title: 'title'}],
- [' aSd!@#AC ', {title: 'aSd!@#AC'}],
- ['\n title\n ', {title: 'title'}],
- [' ', {title: 'meta title'}],
- [
- ' ',
- {description: 'meta description'},
- ],
- [' ', {title: 'og title'}],
- [
- ' ',
- {description: 'og description'},
- ],
- [
- ' ',
- {image: 'https://ogimage.com/foo.png'},
- ],
- [
- ' ',
- {title: 'twitter title'},
- ],
- [
- ' ',
- {description: 'twitter description'},
- ],
- [
- ' ',
- {image: 'https://twitterimage.com/foo.png'},
- ],
- [' ', {title: 'meta title'}],
- ]
-
- it.each(cases)(
- 'given the html tag %p, returns %p',
- // @ts-ignore not worth fixing -prf
- (input, expectedResult) => {
- const output = extractHtmlMeta({html: input as string, hostname: ''})
- expect(output).toEqual(expectedResult)
- },
- )
-
- it('extracts title and description from a generic HTML page', () => {
- const input = exampleComHtml
- const expectedOutput = {
- title: 'Example Domain',
- description: 'An example website',
- }
- const output = extractHtmlMeta({html: input, hostname: 'example.com'})
- expect(output).toEqual(expectedOutput)
- })
-
- it('extracts title and description from a Tiktok HTML page', () => {
- const input = tiktokHtml
- const expectedOutput = {
- title:
- 'Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #bal... | TikTok',
- description:
- '5.5M Likes, 20.8K Comments. TikTok video from Power Vision Tests (@_powervision_): "Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #balloon #sciencemoment #scienceexperiment #experiment #test #amazing #pvexp". оригинальный звук - Power Vision Tests.',
- }
- const output = extractHtmlMeta({html: input, hostname: 'tiktok.com'})
- expect(output).toEqual(expectedOutput)
- })
-
- it('extracts title and description from a generic youtube page', () => {
- const input = youtubeHTML
- const expectedOutput = {
- title: 'HD Video (1080p) with Relaxing Music of Native American Shamans',
- description:
- 'Stunning HD Video ( 1080p ) of Patagonian Nature with Relaxing Native American Shamanic Music. HD footage used from ',
- image: 'https://i.ytimg.com/vi/x6UITRjhijI/sddefault.jpg',
- }
- const output = extractHtmlMeta({html: input, hostname: 'youtube.com'})
- expect(output).toEqual(expectedOutput)
- })
-
- it('extracts avatar from a youtube channel', () => {
- const input = youtubeChannelHtml
- const expectedOutput = {
- title: 'penguinz0',
- description:
- 'Clips channel: https://www.youtube.com/channel/UC4EQHfzIbkL_Skit_iKt1aA\n\nTwitter: https://twitter.com/MoistCr1TiKaL\n\nInstagram: https://www.instagram.com/bigmoistcr1tikal/?hl=en\n\nTwitch: https://www.twitch.tv/moistcr1tikal\n\nSnapchat: Hugecharles\n\nTik Tok: Hugecharles\n\nI don't have any other public accounts.',
- image:
- 'https://yt3.googleusercontent.com/ytc/AL5GRJWOhJOuUC6C2b7gP-5D2q6ypXbcOOckyAE1En4RUQ=s176-c-k-c0x00ffffff-no-rj',
- }
- const output = extractHtmlMeta({html: input, hostname: 'youtube.com'})
- expect(output).toEqual(expectedOutput)
- })
-
- it('extracts username from the url a twitter profile page', () => {
- const expectedOutput = {
- title: '@bluesky on Twitter',
- }
- const output = extractHtmlMeta({
- html: '',
- hostname: 'twitter.com',
- pathname: '/bluesky',
- })
- expect(output).toEqual(expectedOutput)
- })
-
- it('extracts username from the url a tweet', () => {
- const expectedOutput = {
- title: 'Tweet by @bluesky',
- }
- const output = extractHtmlMeta({
- html: '',
- hostname: 'twitter.com',
- pathname: '/bluesky/status/1582437529969917953',
- })
- expect(output).toEqual(expectedOutput)
- })
-
- it("does not extract username from the url when it's not a tweet or profile page", () => {
- const expectedOutput = {
- title: 'Twitter',
- }
- const output = extractHtmlMeta({
- html: '',
- hostname: 'twitter.com',
- pathname: '/i/articles/follows/-1675653703?time_window=24',
- })
- expect(output).toEqual(expectedOutput)
- })
-})
diff --git a/jest/jestSetup.js b/jest/jestSetup.js
index 50a33589ea..4653490f3c 100644
--- a/jest/jestSetup.js
+++ b/jest/jestSetup.js
@@ -54,21 +54,6 @@ jest.mock('expo-image-manipulator', () => ({
SaveFormat: jest.requireActual('expo-image-manipulator').SaveFormat,
}))
-jest.mock('@segment/analytics-react-native', () => ({
- createClient: () => ({
- add: jest.fn(),
- }),
- useAnalytics: () => ({
- track: jest.fn(),
- identify: jest.fn(),
- reset: jest.fn(),
- group: jest.fn(),
- screen: jest.fn(),
- alias: jest.fn(),
- flush: jest.fn(),
- }),
-}))
-
jest.mock('expo-camera', () => ({
Camera: {
useCameraPermissions: jest.fn(() => [true]),
diff --git a/modules/Share-with-Bluesky/Info.plist b/modules/Share-with-Bluesky/Info.plist
index 43f46a5e56..d73a9093fc 100644
--- a/modules/Share-with-Bluesky/Info.plist
+++ b/modules/Share-with-Bluesky/Info.plist
@@ -40,4 +40,4 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
-
+
\ No newline at end of file
diff --git a/modules/Share-with-Bluesky/ShareViewController.swift b/modules/Share-with-Bluesky/ShareViewController.swift
index 63143277a5..2acbb6187b 100644
--- a/modules/Share-with-Bluesky/ShareViewController.swift
+++ b/modules/Share-with-Bluesky/ShareViewController.swift
@@ -1,5 +1,14 @@
import UIKit
+let IMAGE_EXTENSIONS: [String] = ["png", "jpg", "jpeg", "gif", "heic"]
+let MOVIE_EXTENSIONS: [String] = ["mov", "mp4", "m4v"]
+
+enum URLType: String, CaseIterable {
+ case image
+ case movie
+ case other
+}
+
class ShareViewController: UIViewController {
// This allows other forks to use this extension while also changing their
// scheme.
@@ -23,7 +32,7 @@ class ShareViewController: UIViewController {
await self.handleUrl(item: firstAttachment)
} else if firstAttachment.hasItemConformingToTypeIdentifier("public.image") {
await self.handleImages(items: attachments)
- } else if firstAttachment.hasItemConformingToTypeIdentifier("public.video") {
+ } else if firstAttachment.hasItemConformingToTypeIdentifier("public.movie") {
await self.handleVideos(items: attachments)
} else {
self.completeRequest()
@@ -43,9 +52,18 @@ class ShareViewController: UIViewController {
private func handleUrl(item: NSItemProvider) async {
if let data = try? await item.loadItem(forTypeIdentifier: "public.url") as? URL {
- if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
- let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
- _ = self.openURL(url)
+ switch data.type {
+ case .image:
+ await handleImages(items: [item])
+ return
+ case .movie:
+ await handleVideos(items: [item])
+ return
+ case .other:
+ if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
+ let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
+ _ = self.openURL(url)
+ }
}
}
self.completeRequest()
@@ -101,13 +119,13 @@ class ShareViewController: UIViewController {
private func handleVideos(items: [NSItemProvider]) async {
let firstItem = items.first
- if let dataUri = try? await firstItem?.loadItem(forTypeIdentifier: "public.video") as? URL {
+ if let dataUri = try? await firstItem?.loadItem(forTypeIdentifier: "public.movie") as? URL {
let ext = String(dataUri.lastPathComponent.split(separator: ".").last ?? "mp4")
if let tempUrl = getTempUrl(ext: ext) {
let data = try? Data(contentsOf: dataUri)
try? data?.write(to: tempUrl)
- if let encoded = dataUri.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
+ if let encoded = tempUrl.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
let url = URL(string: "\(self.appScheme)://intent/compose?videoUri=\(encoded)") {
_ = self.openURL(url)
}
@@ -150,10 +168,29 @@ class ShareViewController: UIViewController {
var responder: UIResponder? = self
while responder != nil {
if let application = responder as? UIApplication {
- return application.perform(#selector(openURL(_:)), with: url) != nil
+ application.open(url)
+ return true
}
responder = responder?.next
}
return false
}
}
+
+extension URL {
+ var type: URLType {
+ get {
+ guard self.absoluteString.starts(with: "file://"),
+ let ext = self.pathComponents.last?.split(separator: ".").last?.lowercased() else {
+ return .other
+ }
+
+ if IMAGE_EXTENSIONS.contains(ext) {
+ return .image
+ } else if MOVIE_EXTENSIONS.contains(ext) {
+ return .movie
+ }
+ return .other
+ }
+ }
+}
diff --git a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt
index 7ecea16314..c88442057c 100644
--- a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt
+++ b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt
@@ -12,6 +12,11 @@ import java.io.File
import java.io.FileOutputStream
import java.net.URLEncoder
+enum class AttachmentType {
+ IMAGE,
+ VIDEO,
+}
+
class ExpoReceiveAndroidIntentsModule : Module() {
override fun definition() =
ModuleDefinition {
@@ -23,17 +28,26 @@ class ExpoReceiveAndroidIntentsModule : Module() {
}
private fun handleIntent(intent: Intent?) {
- if (appContext.currentActivity == null || intent == null) return
-
- if (intent.action == Intent.ACTION_SEND) {
- if (intent.type == "text/plain") {
- handleTextIntent(intent)
- } else if (intent.type.toString().startsWith("image/")) {
- handleImageIntent(intent)
+ if (appContext.currentActivity == null) return
+ intent?.let {
+ if (it.action == Intent.ACTION_SEND && it.type == "text/plain") {
+ handleTextIntent(it)
+ return
}
- } else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
- if (intent.type.toString().startsWith("image/")) {
- handleImagesIntent(intent)
+
+ val type =
+ if (it.type.toString().startsWith("image/")) {
+ AttachmentType.IMAGE
+ } else if (it.type.toString().startsWith("video/")) {
+ AttachmentType.VIDEO
+ } else {
+ return
+ }
+
+ if (it.action == Intent.ACTION_SEND) {
+ handleAttachmentIntent(it, type)
+ } else if (it.action == Intent.ACTION_SEND_MULTIPLE) {
+ handleAttachmentsIntent(it, type)
}
}
}
@@ -48,26 +62,46 @@ class ExpoReceiveAndroidIntentsModule : Module() {
}
}
- private fun handleImageIntent(intent: Intent) {
+ private fun handleAttachmentIntent(
+ intent: Intent,
+ type: AttachmentType,
+ ) {
val uri =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
- if (uri == null) return
- handleImageIntents(listOf(uri))
+ uri?.let {
+ when (type) {
+ AttachmentType.IMAGE -> handleImageIntents(listOf(it))
+ AttachmentType.VIDEO -> handleVideoIntents(listOf(it))
+ }
+ }
}
- private fun handleImagesIntent(intent: Intent) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)?.let {
- handleImageIntents(it.filterIsInstance().take(4))
+ private fun handleAttachmentsIntent(
+ intent: Intent,
+ type: AttachmentType,
+ ) {
+ val uris =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ intent
+ .getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)
+ ?.filterIsInstance()
+ ?.take(4)
+ } else {
+ intent
+ .getParcelableArrayListExtra(Intent.EXTRA_STREAM)
+ ?.filterIsInstance()
+ ?.take(4)
}
- } else {
- intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.let {
- handleImageIntents(it.filterIsInstance().take(4))
+
+ uris?.let {
+ when (type) {
+ AttachmentType.IMAGE -> handleImageIntents(it)
+ else -> return
}
}
}
@@ -93,11 +127,33 @@ class ExpoReceiveAndroidIntentsModule : Module() {
}
}
+ private fun handleVideoIntents(uris: List) {
+ val uri = uris[0]
+ // If there is no extension for the file, substringAfterLast returns the original string - not
+ // null, so we check for that below
+ // It doesn't actually matter what the extension is, so defaulting to mp4 is fine, even if the
+ // video isn't actually an mp4
+ var extension = uri.path?.substringAfterLast(".")
+ if (extension == null || extension == uri.path) {
+ extension = "mp4"
+ }
+ val file = createFile(extension)
+
+ val out = FileOutputStream(file)
+ appContext.currentActivity?.contentResolver?.openInputStream(uri)?.use {
+ it.copyTo(out)
+ }
+ "bluesky://intent/compose?videoUri=${URLEncoder.encode(file.path, "UTF-8")}".toUri().let {
+ val newIntent = Intent(Intent.ACTION_VIEW, it)
+ appContext.currentActivity?.startActivity(newIntent)
+ }
+ }
+
private fun getImageInfo(uri: Uri): Map {
val bitmap = MediaStore.Images.Media.getBitmap(appContext.currentActivity?.contentResolver, uri)
// We have to save this so that we can access it later when uploading the image.
// createTempFile will automatically place a unique string between "img" and "temp.jpeg"
- val file = File.createTempFile("img", "temp.jpeg", appContext.currentActivity?.cacheDir)
+ val file = createFile("jpeg")
val out = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
out.flush()
@@ -110,6 +166,8 @@ class ExpoReceiveAndroidIntentsModule : Module() {
)
}
+ private fun createFile(extension: String): File = File.createTempFile(extension, "temp.$extension", appContext.currentActivity?.cacheDir)
+
// We will pas the width and height to the app here, since getting measurements
// on the RN side is a bit more involved, and we already have them here anyway.
private fun buildUriData(info: Map): String {
diff --git a/package.json b/package.json
index 2cf8f5c240..84d364c70f 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
- "@atproto/api": "^0.13.7",
+ "@atproto/api": "^0.13.8",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1",
@@ -81,10 +81,6 @@
"@react-navigation/drawer": "^6.6.15",
"@react-navigation/native": "^6.1.17",
"@react-navigation/native-stack": "^6.9.26",
- "@segment/analytics-next": "^1.51.3",
- "@segment/analytics-react": "^1.0.0-rc1",
- "@segment/analytics-react-native": "^2.10.1",
- "@segment/sovran-react-native": "^0.4.5",
"@sentry/react-native": "5.32.0",
"@tamagui/focus-scope": "^1.84.1",
"@tanstack/query-async-storage-persister": "^5.25.0",
@@ -121,6 +117,7 @@
"expo": "^52.0.0-canary-20240912-1059f85",
"expo-modules-core": "2.0.0-canary-20240912-1059f85",
"expo-application": "^6.0.0-canary-20240912-1059f85",
+ "expo-blur": "^14.0.0-canary-20240927-ab8a962",
"expo-build-properties": "^1.0.0-canary-20240912-1059f85",
"expo-camera": "~16.0.0-canary-20240912-1059f85",
"expo-clipboard": "^7.0.0-canary-20240912-1059f85",
diff --git a/plugins/shareExtension/withIntentFilters.js b/plugins/shareExtension/withIntentFilters.js
index 605fcfd052..16494893bb 100644
--- a/plugins/shareExtension/withIntentFilters.js
+++ b/plugins/shareExtension/withIntentFilters.js
@@ -27,6 +27,29 @@ const withIntentFilters = config => {
},
],
},
+ {
+ action: [
+ {
+ $: {
+ 'android:name': 'android.intent.action.SEND',
+ },
+ },
+ ],
+ category: [
+ {
+ $: {
+ 'android:name': 'android.intent.category.DEFAULT',
+ },
+ },
+ ],
+ data: [
+ {
+ $: {
+ 'android:mimeType': 'video/*',
+ },
+ },
+ ],
+ },
{
action: [
{
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 7d98737a3b..1664812d08 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -1,5 +1,5 @@
-import 'lib/sentry' // must be near top
-import 'view/icons'
+import '#/lib/sentry' // must be near top
+import '#/view/icons'
import './style.css'
import React, {useEffect, useState} from 'react'
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 2beba4f9dc..53e8274d54 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -15,7 +15,6 @@ import {
StackActions,
} from '@react-navigation/native'
-import {init as initAnalytics} from '#/lib/analytics/analytics'
import {timeout} from '#/lib/async/timeout'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {usePalette} from '#/lib/hooks/usePalette'
@@ -647,8 +646,6 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
function onReady() {
prevLoggedRouteName.current = getCurrentRouteName()
- initAnalytics(currentAccount)
-
if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) {
openModal({name: 'verify-email', showReminder: true})
snoozeEmailConfirmationPrompt()
diff --git a/src/alf/fonts.ts b/src/alf/fonts.ts
index b11ce939f8..46c741bc79 100644
--- a/src/alf/fonts.ts
+++ b/src/alf/fonts.ts
@@ -45,15 +45,18 @@ export function applyFonts(
// '100': 'Inter-Thin',
// '200': 'Inter-ExtraLight',
// '300': 'Inter-Light',
+ // '500': 'Inter-Medium',
+ // '700': 'Inter-Bold',
+ // '900': 'Inter-Black',
'100': 'Inter-Regular',
'200': 'Inter-Regular',
'300': 'Inter-Regular',
'400': 'Inter-Regular',
- '500': 'Inter-Medium',
+ '500': 'Inter-SemiBold',
'600': 'Inter-SemiBold',
- '700': 'Inter-Bold',
+ '700': 'Inter-SemiBold',
'800': 'Inter-ExtraBold',
- '900': 'Inter-Black',
+ '900': 'Inter-ExtraBold',
}[style.fontWeight as string] || 'Inter-Regular'
if (style.fontStyle === 'italic') {
diff --git a/src/alf/themes.ts b/src/alf/themes.ts
index 9f7ec5c673..0cfe09aadc 100644
--- a/src/alf/themes.ts
+++ b/src/alf/themes.ts
@@ -305,6 +305,7 @@ export function createThemes({
} as const
const light: Theme = {
+ scheme: 'light',
name: 'light',
palette: lightPalette,
atoms: {
@@ -390,6 +391,7 @@ export function createThemes({
}
const dark: Theme = {
+ scheme: 'dark',
name: 'dark',
palette: darkPalette,
atoms: {
@@ -479,6 +481,7 @@ export function createThemes({
const dim: Theme = {
...dark,
+ scheme: 'dark',
name: 'dim',
palette: dimPalette,
atoms: {
diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts
index 3f30702e85..f3ac17e6af 100644
--- a/src/alf/tokens.ts
+++ b/src/alf/tokens.ts
@@ -60,6 +60,15 @@ export const fontWeight = {
} as const
export const gradients = {
+ primary: {
+ values: [
+ [0, '#054CFF'],
+ [0.4, '#1085FE'],
+ [0.6, '#1085FE'],
+ [1, '#59B9FF'],
+ ],
+ hover_value: '#1085FE',
+ },
sky: {
values: [
[0, '#0A7AFF'],
diff --git a/src/alf/types.ts b/src/alf/types.ts
index 41822b8dd5..08ec593927 100644
--- a/src/alf/types.ts
+++ b/src/alf/types.ts
@@ -156,6 +156,7 @@ export type ThemedAtoms = {
}
}
export type Theme = {
+ scheme: 'light' | 'dark' // for library support
name: ThemeName
palette: Palette
atoms: ThemedAtoms
diff --git a/src/components/AccountList.tsx b/src/components/AccountList.tsx
index 883c06c144..68bb482af2 100644
--- a/src/components/AccountList.tsx
+++ b/src/components/AccountList.tsx
@@ -126,10 +126,12 @@ function AccountItem({
-
+
{profile?.displayName || account.handle}{' '}
- {account.handle}
+
+ {account.handle}
+
{isCurrentAccount ? (
(
secondary: tokens.gradients.sky,
secondary_inverted: tokens.gradients.sky,
negative: tokens.gradients.sky,
+ gradient_primary: tokens.gradients.primary,
gradient_sky: tokens.gradients.sky,
gradient_midnight: tokens.gradients.midnight,
gradient_sunrise: tokens.gradients.sunrise,
@@ -444,7 +446,7 @@ export const Button = React.forwardRef(
[state, variant, color, size, disabled],
)
- const flattenedBaseStyles = flatten(baseStyles)
+ const flattenedBaseStyles = flatten([baseStyles, style])
return (
(
a.align_center,
a.justify_center,
flattenedBaseStyles,
- flatten(style),
...(state.hovered || state.pressed
? [hoverStyles, flatten(hoverStyleProp)]
: []),
@@ -626,9 +627,9 @@ export function useSharedButtonTextStyles() {
}
if (size === 'large') {
- baseStyles.push(a.text_md, a.leading_tight, web({paddingTop: 1}))
+ baseStyles.push(a.text_md, a.leading_tight, web({top: -0.4}))
} else if (size === 'small') {
- baseStyles.push(a.text_sm, a.leading_tight, web({paddingTop: 1}))
+ baseStyles.push(a.text_sm, a.leading_tight, web({top: -0.4}))
} else if (size === 'tiny') {
baseStyles.push(a.text_xs, a.leading_tight)
}
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 6c25faffb8..c80b9f3707 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -9,6 +9,7 @@ import {sanitizeUrl} from '@braintree/sanitize-url'
import {StackActions, useLinkProps} from '@react-navigation/native'
import {BSKY_DOWNLOAD_URL} from '#/lib/constants'
+import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
import {AllNavigatorParams} from '#/lib/routes/types'
import {shareUrl} from '#/lib/sharing'
import {
@@ -17,11 +18,10 @@ import {
isExternalUrl,
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
-import {isNative} from '#/platform/detection'
+import {isNative, isWeb} from '#/platform/detection'
import {shouldClickOpenNewTab} from '#/platform/urls'
import {useModalControls} from '#/state/modals'
import {useOpenLink} from '#/state/preferences/in-app-browser'
-import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import {useInteractionState} from '#/components/hooks/useInteractionState'
@@ -244,7 +244,10 @@ export function Link({
export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick
> &
- Pick
+ Pick & {
+ disableUnderline?: boolean
+ title?: TextProps['title']
+ }
export function InlineLinkText({
children,
@@ -257,6 +260,7 @@ export function InlineLinkText({
selectable,
label,
shareOnLongPress,
+ disableUnderline,
...rest
}: InlineLinkProps) {
const t = useTheme()
@@ -290,11 +294,12 @@ export function InlineLinkText({
{...rest}
style={[
{color: t.palette.primary_500},
- (hovered || focused || pressed) && {
- ...web({outline: 0}),
- textDecorationLine: 'underline',
- textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
- },
+ (hovered || focused || pressed) &&
+ !disableUnderline && {
+ ...web({outline: 0}),
+ textDecorationLine: 'underline',
+ textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
+ },
flattenedStyle,
]}
role="link"
@@ -365,3 +370,18 @@ export function BaseLink({
)
}
+
+export function WebOnlyInlineLinkText({
+ children,
+ to,
+ onPress,
+ ...props
+}: InlineLinkProps) {
+ return isWeb ? (
+
+ {children}
+
+ ) : (
+ {children}
+ )
+}
diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx
index 7fb0545a21..00afbdcfe9 100644
--- a/src/components/StarterPack/ProfileStarterPacks.tsx
+++ b/src/components/StarterPack/ProfileStarterPacks.tsx
@@ -12,15 +12,15 @@ import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query'
+import {useGenerateStarterPackMutation} from '#/lib/generate-starterpack'
+import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {NavigationProp} from '#/lib/routes/types'
+import {parseStarterPackUri} from '#/lib/strings/starter-pack'
import {logger} from '#/logger'
-import {useGenerateStarterPackMutation} from 'lib/generate-starterpack'
-import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {NavigationProp} from 'lib/routes/types'
-import {parseStarterPackUri} from 'lib/strings/starter-pack'
-import {List, ListRef} from 'view/com/util/List'
-import {Text} from 'view/com/util/text/Text'
-import {atoms as a, useTheme} from '#/alf'
+import {List, ListRef} from '#/view/com/util/List'
+import {Text} from '#/view/com/util/text/Text'
+import {atoms as a, ios, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
@@ -132,6 +132,7 @@ export const ProfileStarterPacks = React.forwardRef<
keyExtractor={keyExtractor}
refreshing={isPTRing}
headerOffset={headerOffset}
+ progressViewOffset={ios(0)}
contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}}
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
removeClippedSubviews={true}
diff --git a/src/components/StarterPack/QrCode.tsx b/src/components/StarterPack/QrCode.tsx
index 8ce5cbbb13..c6408109bb 100644
--- a/src/components/StarterPack/QrCode.tsx
+++ b/src/components/StarterPack/QrCode.tsx
@@ -1,18 +1,23 @@
import React from 'react'
import {View} from 'react-native'
import QRCode from 'react-native-qrcode-styled'
-import ViewShot from 'react-native-view-shot'
+import type ViewShot from 'react-native-view-shot'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {Trans} from '@lingui/macro'
-import {isWeb} from 'platform/detection'
-import {Logo} from 'view/icons/Logo'
-import {Logotype} from 'view/icons/Logotype'
+import {isWeb} from '#/platform/detection'
+import {Logo} from '#/view/icons/Logo'
+import {Logotype} from '#/view/icons/Logotype'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {Text} from '#/components/Typography'
+const LazyViewShot = React.lazy(
+ // @ts-expect-error dynamic import
+ () => import('react-native-view-shot/src/index'),
+)
+
interface Props {
starterPack: AppBskyGraphDefs.StarterPackView
link: string
@@ -29,7 +34,7 @@ export const QrCode = React.forwardRef(function QrCode(
}
return (
-
+
(function QrCode(
-
+
)
})
diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx
index a884390bf8..b2af8ff73a 100644
--- a/src/components/StarterPack/QrCodeDialog.tsx
+++ b/src/components/StarterPack/QrCodeDialog.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import {View} from 'react-native'
-import ViewShot from 'react-native-view-shot'
+import type ViewShot from 'react-native-view-shot'
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
import {createAssetAsync} from 'expo-media-library'
import * as Sharing from 'expo-sharing'
@@ -8,9 +8,9 @@ import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
-import {logEvent} from 'lib/statsig/statsig'
-import {isNative, isWeb} from 'platform/detection'
+import {isNative, isWeb} from '#/platform/detection'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -153,46 +153,54 @@ export function QrCodeDialog({
- {!link ? (
-
-
-
- ) : (
- <>
-
- {isProcessing ? (
-
-
-
- ) : (
-
-
-
- {isWeb ? Copy : Share }
-
-
-
-
- Save
-
-
-
- )}
- >
- )}
+ }>
+ {!link ? (
+
+ ) : (
+ <>
+
+ {isProcessing ? (
+
+
+
+ ) : (
+
+
+
+ {isWeb ? Copy : Share }
+
+
+
+
+ Save
+
+
+
+ )}
+ >
+ )}
+
)
}
+
+function Loading() {
+ return (
+
+
+
+ )
+}
diff --git a/src/components/dialogs/nuxs/TenMillion/Trigger.tsx b/src/components/dialogs/nuxs/TenMillion/Trigger.tsx
deleted file mode 100644
index 9616b3b1d3..0000000000
--- a/src/components/dialogs/nuxs/TenMillion/Trigger.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import Svg, {Circle, Path} from 'react-native-svg'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {Nux, useUpsertNuxMutation} from '#/state/queries/nuxs'
-import {atoms as a, ViewStyleProp} from '#/alf'
-import {Button, ButtonProps} from '#/components/Button'
-import * as Dialog from '#/components/Dialog'
-import {InlineLinkText} from '#/components/Link'
-import * as Prompt from '#/components/Prompt'
-import {TenMillion} from './'
-
-export function Trigger({children}: {children: ButtonProps['children']}) {
- const {_} = useLingui()
- const {mutate: upsertNux} = useUpsertNuxMutation()
- const [show, setShow] = React.useState(false)
- const [fallback, setFallback] = React.useState(false)
- const control = Prompt.usePromptControl()
-
- const handleOnPress = () => {
- if (!fallback) {
- setShow(true)
- upsertNux({
- id: Nux.TenMillionDialog,
- completed: true,
- data: undefined,
- })
- } else {
- control.open()
- }
- }
-
- const onHandleFallback = () => {
- setFallback(true)
- control.open()
- }
-
- return (
- <>
-
- {children}
-
-
- {show && !fallback && (
- setShow(false)}
- onFallback={onHandleFallback}
- />
- )}
-
-
-
-
- Bluesky is celebrating 10 million users!
-
-
-
-
- Together, we're rebuilding the social internet. We're glad you're
- here.
-
-
-
-
- To learn more,{' '}
- {
- control.close()
- }}
- style={[a.text_md, a.leading_snug]}>
- check out our post.
-
-
-
-
-
- >
- )
-}
-
-export function Icon({width, style}: {width: number} & ViewStyleProp) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx
deleted file mode 100644
index 9c8d47afd0..0000000000
--- a/src/components/dialogs/nuxs/TenMillion/icons/OnePercent.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import Svg, {Path} from 'react-native-svg'
-
-export function OnePercent({fill}: {fill?: string}) {
- return (
-
-
-
- )
-}
diff --git a/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx
deleted file mode 100644
index 1f9467e442..0000000000
--- a/src/components/dialogs/nuxs/TenMillion/icons/PointOnePercent.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import Svg, {Path} from 'react-native-svg'
-
-export function PointOnePercent({fill}: {fill?: string}) {
- return (
-
-
-
- )
-}
diff --git a/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx
deleted file mode 100644
index 4197be8357..0000000000
--- a/src/components/dialogs/nuxs/TenMillion/icons/TenPercent.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import Svg, {Path} from 'react-native-svg'
-
-export function TenPercent({fill}: {fill?: string}) {
- return (
-
-
-
- )
-}
diff --git a/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx b/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx
deleted file mode 100644
index 0d37971410..0000000000
--- a/src/components/dialogs/nuxs/TenMillion/icons/TwentyFivePercent.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import Svg, {Path} from 'react-native-svg'
-
-export function TwentyFivePercent({fill}: {fill?: string}) {
- return (
-
-
-
- )
-}
diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx
deleted file mode 100644
index 21e775a108..0000000000
--- a/src/components/dialogs/nuxs/TenMillion/index.tsx
+++ /dev/null
@@ -1,712 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import Animated, {FadeIn} from 'react-native-reanimated'
-import ViewShot from 'react-native-view-shot'
-import {Image} from 'expo-image'
-import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
-import * as MediaLibrary from 'expo-media-library'
-import {moderateProfile} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {networkRetry} from '#/lib/async/retry'
-import {getCanvas} from '#/lib/canvas'
-import {shareUrl} from '#/lib/sharing'
-import {logEvent} from '#/lib/statsig/statsig'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {isIOS, isNative} from '#/platform/detection'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useProfileQuery} from '#/state/queries/profile'
-import {useAgent, useSession} from '#/state/session'
-import {useComposerControls} from '#/state/shell'
-import {formatCount} from '#/view/com/util/numeric/format'
-import * as Toast from '#/view/com/util/Toast'
-import {Logomark} from '#/view/icons/Logomark'
-import {
- atoms as a,
- ThemeProvider,
- tokens,
- useBreakpoints,
- useTheme,
-} from '#/alf'
-import {Button, ButtonIcon, ButtonText} from '#/components/Button'
-import * as Dialog from '#/components/Dialog'
-import {useNuxDialogContext} from '#/components/dialogs/nuxs'
-import {OnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/OnePercent'
-import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent'
-import {TenPercent} from '#/components/dialogs/nuxs/TenMillion/icons/TenPercent'
-import {Divider} from '#/components/Divider'
-import {GradientFill} from '#/components/GradientFill'
-import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
-import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/Download'
-import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
-import {Loader} from '#/components/Loader'
-import {Text} from '#/components/Typography'
-
-const DEBUG = false
-const RATIO = 8 / 10
-const WIDTH = 2000
-const HEIGHT = WIDTH * RATIO
-
-function getFontSize(count: number) {
- const length = count.toString().length
- if (length < 7) {
- return 80
- } else if (length < 5) {
- return 100
- } else {
- return 70
- }
-}
-
-function getPercentBadge(percent: number) {
- if (percent <= 0.001) {
- return PointOnePercent
- } else if (percent <= 0.01) {
- return OnePercent
- } else if (percent <= 0.1) {
- return TenPercent
- }
- return null
-}
-
-function Frame({children}: {children: React.ReactNode}) {
- return (
-
- {children}
-
- )
-}
-
-export function TenMillion({
- showTimeout,
- onClose,
- onFallback,
-}: {
- showTimeout?: number
- onClose?: () => void
- onFallback?: () => void
-}) {
- const agent = useAgent()
- const nuxDialogs = useNuxDialogContext()
- const [userNumber, setUserNumber] = React.useState(0)
- const fetching = React.useRef(false)
-
- React.useEffect(() => {
- async function fetchUserNumber() {
- const isBlueskyHosted = agent.sessionManager.pdsUrl
- ?.toString()
- .includes('bsky.network')
-
- if (isBlueskyHosted && agent.session?.accessJwt) {
- const res = await fetch(
- `https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`,
- {
- headers: {
- Authorization: `Bearer ${agent.session.accessJwt}`,
- },
- },
- )
-
- if (!res.ok) {
- throw new Error('Network request failed')
- }
-
- const data = await res.json()
-
- if (data.number && data.number <= 10_000_000) {
- setUserNumber(data.number)
- } else {
- // should be rare
- nuxDialogs.dismissActiveNux()
- onFallback?.()
- }
- } else {
- nuxDialogs.dismissActiveNux()
- onFallback?.()
- }
- }
-
- if (!fetching.current) {
- fetching.current = true
- networkRetry(3, fetchUserNumber).catch(() => {
- nuxDialogs.dismissActiveNux()
- onFallback?.()
- })
- }
- }, [
- agent.sessionManager.pdsUrl,
- agent.session?.accessJwt,
- setUserNumber,
- nuxDialogs.dismissActiveNux,
- nuxDialogs,
- onFallback,
- ])
-
- return userNumber ? (
-
- ) : null
-}
-
-export function TenMillionInner({
- userNumber,
- showTimeout,
- onClose: onCloseOuter,
-}: {
- userNumber: number
- showTimeout: number
- onClose?: () => void
-}) {
- const t = useTheme()
- const lightTheme = useTheme('light')
- const {_, i18n} = useLingui()
- const control = Dialog.useDialogControl()
- const {gtMobile} = useBreakpoints()
- const {openComposer} = useComposerControls()
- const {currentAccount} = useSession()
- const {
- isLoading: isProfileLoading,
- data: profile,
- error: profileError,
- } = useProfileQuery({
- did: currentAccount!.did,
- })
- const moderationOpts = useModerationOpts()
- const nuxDialogs = useNuxDialogContext()
- const moderation = React.useMemo(() => {
- return profile && moderationOpts
- ? moderateProfile(profile, moderationOpts)
- : undefined
- }, [profile, moderationOpts])
- const [uri, setUri] = React.useState(null)
- const percent = userNumber / 10_000_000
- const Badge = getPercentBadge(percent)
- const isLoadingData = isProfileLoading || !moderation || !profile
- const isLoadingImage = !uri
-
- const displayName = React.useMemo(() => {
- if (!profile || !moderation) return ''
- return sanitizeDisplayName(
- profile.displayName || sanitizeHandle(profile.handle),
- moderation.ui('displayName'),
- )
- }, [profile, moderation])
- const handle = React.useMemo(() => {
- if (!profile) return ''
- return sanitizeHandle(profile.handle, '@')
- }, [profile])
- const joinedDate = React.useMemo(() => {
- if (!profile || !profile.createdAt) return ''
- const date = i18n.date(profile.createdAt, {
- month: 'short',
- day: 'numeric',
- year: 'numeric',
- })
- return date
- }, [i18n, profile])
-
- const error: string = React.useMemo(() => {
- if (profileError) {
- return _(
- msg`Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋`,
- )
- }
- return ''
- }, [_, profileError])
-
- /*
- * Opening and closing
- */
- React.useEffect(() => {
- const timeout = setTimeout(() => {
- control.open()
- }, showTimeout)
- return () => {
- clearTimeout(timeout)
- }
- }, [control, showTimeout])
- const onClose = React.useCallback(() => {
- nuxDialogs.dismissActiveNux()
- onCloseOuter?.()
- }, [nuxDialogs, onCloseOuter])
-
- /*
- * Actions
- */
- const sharePost = React.useCallback(() => {
- if (uri) {
- control.close(() => {
- setTimeout(() => {
- logEvent('tmd:post', {})
- openComposer({
- text: _(
- msg`Bluesky now has over 10 million users, and I was #${i18n.number(
- userNumber,
- )}!`,
- ),
- imageUris: [
- {
- uri,
- width: WIDTH,
- height: HEIGHT,
- altText: _(
- msg`A virtual certificate with text "Celebrating 10M users on Bluesky, #${i18n.number(
- userNumber,
- )}, ${displayName} ${handle}, joined on ${joinedDate}"`,
- ),
- },
- ],
- })
- }, 1e3)
- })
- }
- }, [
- _,
- i18n,
- control,
- openComposer,
- uri,
- userNumber,
- displayName,
- handle,
- joinedDate,
- ])
- const onNativeShare = React.useCallback(() => {
- if (uri) {
- control.close(() => {
- logEvent('tmd:share', {})
- shareUrl(uri)
- })
- }
- }, [uri, control])
- const onNativeDownload = React.useCallback(async () => {
- if (uri) {
- const res = await requestMediaLibraryPermissionsAsync()
-
- if (!res) {
- Toast.show(
- _(
- msg`You must grant access to your photo library to save the image.`,
- ),
- 'xmark',
- )
- return
- }
-
- try {
- await MediaLibrary.createAssetAsync(uri)
- logEvent('tmd:download', {})
- Toast.show(_(msg`Image saved to your camera roll!`))
- } catch (e: unknown) {
- console.log(e)
- Toast.show(_(msg`An error occurred while saving the image!`), 'xmark')
- return
- }
- }
- }, [_, uri])
- const onWebDownload = React.useCallback(async () => {
- if (uri) {
- const canvas = await getCanvas(uri)
- const imgHref = canvas
- .toDataURL('image/png')
- .replace('image/png', 'image/octet-stream')
- const link = document.createElement('a')
- link.setAttribute('download', `Bluesky 10M Users.png`)
- link.setAttribute('href', imgHref)
- link.click()
- logEvent('tmd:download', {})
- }
- }, [uri])
-
- /*
- * Canvas stuff
- */
- const imageRef = React.useRef(null)
- const captureInProgress = React.useRef(false)
- const onCanvasReady = React.useCallback(async () => {
- if (
- imageRef.current &&
- imageRef.current.capture &&
- !captureInProgress.current
- ) {
- captureInProgress.current = true
- const uri = await imageRef.current.capture()
- setUri(uri)
- }
- }, [setUri])
- const canvas = isLoadingData ? null : (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Centered content */}
-
-
-
- Celebrating {formatCount(i18n, 10000000)} users
- {' '}
- 🎉
-
-
-
- #
-
-
- {i18n.number(userNumber)}
-
-
-
- {Badge && (
-
-
-
- )}
-
- {/* End centered content */}
-
-
-
- {/*
-
- */}
-
-
- {displayName}
-
-
-
- {handle}
-
-
- {profile.createdAt && (
-
- Joined on {joinedDate}
-
- )}
-
-
-
-
-
-
-
-
-
-
-
- )
-
- return (
-
-
-
-
-
-
- {error ? (
-
-
- (╯°□°)╯︵ ┻━┻
-
-
- {error}
-
-
- ) : isLoadingData || isLoadingImage ? (
-
- ) : (
-
-
-
- )}
-
-
-
- {canvas}
-
-
-
- Thanks for being one of our first 10 million users.
-
-
-
-
- Together, we're rebuilding the social internet. We're glad
- you're here.
-
-
-
-
-
-
- {gtMobile && (
-
- Brag a little!
-
- )}
-
-
-
-
-
- {_(msg`Share`)}
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/src/components/dialogs/nuxs/index.tsx b/src/components/dialogs/nuxs/index.tsx
index b93831ad35..e7476972ad 100644
--- a/src/components/dialogs/nuxs/index.tsx
+++ b/src/components/dialogs/nuxs/index.tsx
@@ -16,10 +16,11 @@ import {
import {useProfileQuery} from '#/state/queries/profile'
import {SessionAccount, useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell'
+/*
+ * NUXs
+ */
import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
-// NUXs
-import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
import {IS_DEV} from '#/env'
type Context = {
@@ -36,14 +37,11 @@ const queuedNuxs: {
preferences: UsePreferencesQueryResponse
}) => boolean
}[] = [
- {
- id: Nux.TenMillionDialog,
- },
{
id: Nux.NeueTypography,
enabled(props) {
if (props.currentProfile.createdAt) {
- if (new Date(props.currentProfile.createdAt) < new Date('2024-09-25')) {
+ if (new Date(props.currentProfile.createdAt) < new Date('2024-10-01')) {
return true
}
}
@@ -176,7 +174,6 @@ function Inner({
return (
- {activeNux === Nux.TenMillionDialog && }
{activeNux === Nux.NeueTypography && }
)
diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx
index 94ee261e38..21928d3df3 100644
--- a/src/components/forms/TextField.tsx
+++ b/src/components/forms/TextField.tsx
@@ -135,6 +135,8 @@ export function createInput(Component: typeof TextInput) {
placeholder,
value,
onChangeText,
+ onFocus,
+ onBlur,
isInvalid,
inputRef,
style,
@@ -173,8 +175,14 @@ export function createInput(Component: typeof TextInput) {
ref={refs}
value={value}
onChangeText={onChangeText}
- onFocus={ctx.onFocus}
- onBlur={ctx.onBlur}
+ onFocus={e => {
+ ctx.onFocus()
+ onFocus?.(e)
+ }}
+ onBlur={e => {
+ ctx.onBlur()
+ onBlur?.(e)
+ }}
placeholder={placeholder || label}
placeholderTextColor={t.palette.contrast_500}
keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
@@ -188,8 +196,8 @@ export function createInput(Component: typeof TextInput) {
a.px_xs,
{
// paddingVertical doesn't work w/multiline - esb
- paddingTop: 14,
- paddingBottom: 14,
+ paddingTop: 12,
+ paddingBottom: 13,
lineHeight: a.text_md.fontSize * 1.1875,
textAlignVertical: rest.multiline ? 'top' : undefined,
minHeight: rest.multiline ? 80 : undefined,
@@ -197,13 +205,14 @@ export function createInput(Component: typeof TextInput) {
},
// fix for autofill styles covering border
web({
- paddingTop: 12,
- paddingBottom: 12,
+ paddingTop: 10,
+ paddingBottom: 11,
marginTop: 2,
marginBottom: 2,
}),
android({
- paddingBottom: 16,
+ paddingTop: 8,
+ paddingBottom: 8,
}),
style,
]}
diff --git a/src/components/hooks/useHeaderOffset.ts b/src/components/hooks/useHeaderOffset.ts
index e2290c04fb..5c80e18fe0 100644
--- a/src/components/hooks/useHeaderOffset.ts
+++ b/src/components/hooks/useHeaderOffset.ts
@@ -9,8 +9,8 @@ export function useHeaderOffset() {
return 0
}
const navBarHeight = 42
- const tabBarPad = 10 + 10 + 3 // padding + border
- const normalLineHeight = 1.2
- const tabBarText = 16 * normalLineHeight * fontScale
+ const tabBarPad = 10 + 10 + 6 // padding + arbitrary
+ const normalLineHeight = 20 // matches tab bar
+ const tabBarText = normalLineHeight * fontScale
return navBarHeight + tabBarPad + tabBarText
}
diff --git a/src/lib/analytics/analytics.tsx b/src/lib/analytics/analytics.tsx
deleted file mode 100644
index 5f93d982fd..0000000000
--- a/src/lib/analytics/analytics.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import React from 'react'
-import {AppState, AppStateStatus} from 'react-native'
-import AsyncStorage from '@react-native-async-storage/async-storage'
-import {createClient, SegmentClient} from '@segment/analytics-react-native'
-import * as Sentry from '@sentry/react-native'
-import {sha256} from 'js-sha256'
-
-import {logger} from '#/logger'
-import {SessionAccount, useSession} from '#/state/session'
-import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
-
-type AppInfo = {
- build?: string | undefined
- name?: string | undefined
- namespace?: string | undefined
- version?: string | undefined
-}
-
-// Delay creating until first actual use.
-let segmentClient: SegmentClient | null = null
-function getClient(): SegmentClient {
- if (!segmentClient) {
- segmentClient = createClient({
- writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
- trackAppLifecycleEvents: false,
- proxy: 'https://api.events.bsky.app/v1',
- })
- }
- return segmentClient
-}
-
-export const track = async (
- event: E,
- properties?: TrackPropertiesMap[E],
-) => {
- await getClient().track(event, properties)
-}
-
-export function useAnalytics() {
- const {hasSession} = useSession()
-
- return React.useMemo(() => {
- if (hasSession) {
- return {
- async screen(
- event: E,
- properties?: ScreenPropertiesMap[E],
- ) {
- await getClient().screen(event, properties)
- },
- async track(
- event: E,
- properties?: TrackPropertiesMap[E],
- ) {
- await getClient().track(event, properties)
- },
- }
- }
- // dont send analytics pings for anonymous users
- return {
- screen: async () => {},
- track: async () => {},
- }
- }, [hasSession])
-}
-
-export function init(account: SessionAccount | undefined) {
- setupListenersOnce()
-
- if (account) {
- const client = getClient()
- if (account.did) {
- const did_hashed = sha256(account.did)
- client.identify(did_hashed, {did_hashed})
- Sentry.setUser({id: did_hashed})
- logger.debug('Ping w/hash')
- } else {
- logger.debug('Ping w/o hash')
- client.identify()
- }
- }
-}
-
-let didSetupListeners = false
-function setupListenersOnce() {
- if (didSetupListeners) {
- return
- }
- didSetupListeners = true
- // NOTE
- // this is a copy of segment's own lifecycle event tracking
- // we handle it manually to ensure that it never fires while the app is backgrounded
- // -prf
- const client = getClient()
- client.isReady.onChange(async () => {
- if (AppState.currentState !== 'active') {
- logger.debug('Prevented a metrics ping while the app was backgrounded')
- return
- }
- const context = client.context.get()
- if (typeof context?.app === 'undefined') {
- logger.debug('Aborted metrics ping due to unavailable context')
- return
- }
-
- const oldAppInfo = await readAppInfo()
- const newAppInfo = context.app as AppInfo
- writeAppInfo(newAppInfo)
- logger.debug('Recording app info', {new: newAppInfo, old: oldAppInfo})
-
- if (typeof oldAppInfo === 'undefined') {
- client.track('Application Installed', {
- version: newAppInfo.version,
- build: newAppInfo.build,
- })
- } else if (newAppInfo.version !== oldAppInfo.version) {
- client.track('Application Updated', {
- version: newAppInfo.version,
- build: newAppInfo.build,
- previous_version: oldAppInfo.version,
- previous_build: oldAppInfo.build,
- })
- }
- client.track('Application Opened', {
- from_background: false,
- version: newAppInfo.version,
- build: newAppInfo.build,
- })
- })
-
- let lastState: AppStateStatus = AppState.currentState
- AppState.addEventListener('change', (state: AppStateStatus) => {
- if (state === 'active' && lastState !== 'active') {
- const context = client.context.get()
- client.track('Application Opened', {
- from_background: true,
- version: context?.app?.version,
- build: context?.app?.build,
- })
- } else if (state !== 'active' && lastState === 'active') {
- client.track('Application Backgrounded')
- }
- lastState = state
- })
-}
-
-async function writeAppInfo(value: AppInfo) {
- await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value))
-}
-
-async function readAppInfo(): Promise {
- const rawData = await AsyncStorage.getItem('BSKY_APP_INFO')
- const obj = rawData ? JSON.parse(rawData) : undefined
- if (!obj || typeof obj !== 'object') {
- return undefined
- }
- return obj
-}
diff --git a/src/lib/analytics/analytics.web.tsx b/src/lib/analytics/analytics.web.tsx
deleted file mode 100644
index c7f0ed3b14..0000000000
--- a/src/lib/analytics/analytics.web.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import React from 'react'
-import {createClient} from '@segment/analytics-react'
-import * as Sentry from '@sentry/react-native'
-import {sha256} from 'js-sha256'
-
-import {logger} from '#/logger'
-import {SessionAccount, useSession} from '#/state/session'
-import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
-
-type SegmentClient = ReturnType
-
-// Delay creating until first actual use.
-let segmentClient: SegmentClient | null = null
-function getClient(): SegmentClient {
- if (!segmentClient) {
- segmentClient = createClient(
- {
- writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
- },
- {
- integrations: {
- 'Segment.io': {
- apiHost: 'api.events.bsky.app/v1',
- },
- },
- },
- )
- }
- return segmentClient
-}
-
-export const track = async (
- event: E,
- properties?: TrackPropertiesMap[E],
-) => {
- await getClient().track(event, properties)
-}
-
-export function useAnalytics() {
- const {hasSession} = useSession()
-
- return React.useMemo(() => {
- if (hasSession) {
- return {
- async screen(
- event: E,
- properties?: ScreenPropertiesMap[E],
- ) {
- await getClient().screen(event, properties)
- },
- async track(
- event: E,
- properties?: TrackPropertiesMap[E],
- ) {
- await getClient().track(event, properties)
- },
- }
- }
- // dont send analytics pings for anonymous users
- return {
- screen: async () => {},
- track: async () => {},
- }
- }, [hasSession])
-}
-
-export function init(account: SessionAccount | undefined) {
- if (account) {
- const client = getClient()
- if (account.did) {
- const did_hashed = sha256(account.did)
- client.identify(did_hashed, {did_hashed})
- Sentry.setUser({id: did_hashed})
- logger.debug('Ping w/hash')
- } else {
- logger.debug('Ping w/o hash')
- client.identify()
- }
- }
-}
diff --git a/src/lib/analytics/types.ts b/src/lib/analytics/types.ts
deleted file mode 100644
index 720495ea1d..0000000000
--- a/src/lib/analytics/types.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-export type TrackPropertiesMap = {
- // LOGIN / SIGN UP events
- 'Sign In': {resumedSession: boolean} // CAN BE SERVER
- 'Create Account': {} // CAN BE SERVER
- 'Try Create Account': {}
- 'Signin:PressedForgotPassword': {}
- 'Signin:PressedSelectService': {}
- // COMPOSER / CREATE POST events
- 'Create Post': {imageCount: string | number} // CAN BE SERVER
- 'Composer:PastedPhotos': {}
- 'Composer:CameraOpened': {}
- 'Composer:GalleryOpened': {}
- 'Composer:ThreadgateOpened': {}
- 'HomeScreen:PressCompose': {}
- 'ProfileScreen:PressCompose': {}
- // EDIT PROFILE events
- 'EditHandle:ViewCustomForm': {}
- 'EditHandle:ViewProvidedForm': {}
- 'EditHandle:SetNewHandle': {}
- 'EditProfile:AvatarSelected': {}
- 'EditProfile:BannerSelected': {}
- 'EditProfile:Save': {} // CAN BE SERVER
- // FEED events
- 'Feed:onRefresh': {}
- 'Feed:onEndReached': {}
- // POST events
- 'Post:Like': {} // CAN BE SERVER
- 'Post:Unlike': {} // CAN BE SERVER
- 'Post:Repost': {} // CAN BE SERVER
- 'Post:Unrepost': {} // CAN BE SERVER
- 'Post:Delete': {} // CAN BE SERVER
- 'Post:ThreadMute': {} // CAN BE SERVER
- 'Post:ThreadUnmute': {} // CAN BE SERVER
- 'Post:Reply': {} // CAN BE SERVER
- 'Post:EditThreadgateOpened': {}
- 'Post:ThreadgateEdited': {}
- // PROFILE events
- 'Profile:Follow': {
- username: string
- }
- 'Profile:Unfollow': {
- username: string
- }
- // PROFILE HEADER events
- 'ProfileHeader:EditProfileButtonClicked': {}
- 'ProfileHeader:FollowersButtonClicked': {
- handle: string
- }
- 'ProfileHeader:FollowsButtonClicked': {
- handle: string
- }
- 'ProfileHeader:ShareButtonClicked': {}
- 'ProfileHeader:MuteAccountButtonClicked': {}
- 'ProfileHeader:UnmuteAccountButtonClicked': {}
- 'ProfileHeader:ReportAccountButtonClicked': {}
- 'ProfileHeader:AddToListsButtonClicked': {}
- 'ProfileHeader:BlockAccountButtonClicked': {}
- 'ProfileHeader:UnblockAccountButtonClicked': {}
- 'ProfileHeader:FollowButtonClicked': {}
- 'ProfileHeader:UnfollowButtonClicked': {}
- 'ProfileHeader:SuggestedFollowsOpened': {}
- 'ProfileHeader:SuggestedFollowFollowed': {}
- 'ViewHeader:MenuButtonClicked': {}
- // SETTINGS events
- 'Settings:SwitchAccountButtonClicked': {}
- 'Settings:AddAccountButtonClicked': {}
- 'Settings:ChangeHandleButtonClicked': {}
- 'Settings:InvitecodesButtonClicked': {}
- 'Settings:SignOutButtonClicked': {}
- 'Settings:ContentlanguagesButtonClicked': {}
- // MENU events
- 'Menu:ItemClicked': {url: string}
- 'Menu:FeedbackClicked': {}
- 'Menu:HelpClicked': {}
- // MOBILE SHELL events
- 'MobileShell:MyProfileButtonPressed': {}
- 'MobileShell:HomeButtonPressed': {}
- 'MobileShell:SearchButtonPressed': {}
- 'MobileShell:NotificationsButtonPressed': {}
- 'MobileShell:FeedsButtonPressed': {}
- 'MobileShell:MessagesButtonPressed': {}
- // NOTIFICATIONS events
- 'Notificatons:OpenApp': {}
- // LISTS events
- 'Lists:onRefresh': {}
- 'Lists:onEndReached': {}
- 'CreateList:AvatarSelected': {}
- 'CreateList:SaveCurateList': {} // CAN BE SERVER
- 'CreateList:SaveModList': {} // CAN BE SERVER
- 'Lists:Mute': {} // CAN BE SERVER
- 'Lists:Unmute': {} // CAN BE SERVER
- 'Lists:Block': {} // CAN BE SERVER
- 'Lists:Unblock': {} // CAN BE SERVER
- 'Lists:Delete': {} // CAN BE SERVER
- 'Lists:Share': {} // CAN BE SERVER
- // CUSTOM FEED events
- 'CustomFeed:Save': {}
- 'CustomFeed:Unsave': {}
- 'CustomFeed:Like': {}
- 'CustomFeed:Unlike': {}
- 'CustomFeed:Share': {}
- 'CustomFeed:Pin': {
- uri: string
- name?: string
- }
- 'CustomFeed:Unpin': {
- uri: string
- name?: string
- }
- 'CustomFeed:Reorder': {
- uri: string
- name?: string
- index: number
- }
- 'CustomFeed:LoadMore': {}
- 'MultiFeed:onEndReached': {}
- 'MultiFeed:onRefresh': {}
- // MODERATION events
- 'Moderation:ContentfilteringButtonClicked': {}
- // ONBOARDING events
- 'Onboarding:Begin': {}
- 'Onboarding:Complete': {}
- 'Onboarding:Skipped': {}
- 'Onboarding:Reset': {}
- 'Onboarding:SuggestedFollowFollowed': {}
- 'Onboarding:CustomFeedAdded': {}
- // Onboarding v2
- 'OnboardingV2:Begin': {}
- 'OnboardingV2:StepInterests:Start': {}
- 'OnboardingV2:StepInterests:End': {
- selectedInterests: string[]
- selectedInterestsLength: number
- }
- 'OnboardingV2:StepInterests:Error': {}
- 'OnboardingV2:StepSuggestedAccounts:Start': {}
- 'OnboardingV2:StepSuggestedAccounts:End': {
- selectedAccountsLength: number
- }
- 'OnboardingV2:StepFollowingFeed:Start': {}
- 'OnboardingV2:StepFollowingFeed:End': {}
- 'OnboardingV2:StepAlgoFeeds:Start': {}
- 'OnboardingV2:StepAlgoFeeds:End': {
- selectedPrimaryFeeds: string[]
- selectedPrimaryFeedsLength: number
- selectedSecondaryFeeds: string[]
- selectedSecondaryFeedsLength: number
- }
- 'OnboardingV2:StepTopicalFeeds:Start': {}
- 'OnboardingV2:StepTopicalFeeds:End': {
- selectedFeeds: string[]
- selectedFeedsLength: number
- }
- 'OnboardingV2:StepModeration:Start': {}
- 'OnboardingV2:StepModeration:End': {}
- 'OnboardingV2:StepProfile:Start': {}
- 'OnboardingV2:StepProfile:End': {}
- 'OnboardingV2:StepFinished:Start': {}
- 'OnboardingV2:StepFinished:End': {}
- 'OnboardingV2:Complete': {}
- 'OnboardingV2:Skip': {}
-}
-
-export type ScreenPropertiesMap = {
- Login: {}
- CreateAccount: {}
- 'Choose Account': {}
- 'Signin:ForgotPassword': {}
- 'Signin:SetNewPasswordForm': {}
- 'Signin:PasswordUpdatedForm': {}
- Feed: {}
- Notifications: {}
- Profile: {}
- 'Profile:Preview': {}
- Settings: {}
- AppPasswords: {}
- Moderation: {}
- PreferencesExternalEmbeds: {}
- BlockedAccounts: {}
- MutedAccounts: {}
- SavedFeeds: {}
-}
diff --git a/src/lib/api/feed/author.ts b/src/lib/api/feed/author.ts
index 56eff18816..50e6a447e7 100644
--- a/src/lib/api/feed/author.ts
+++ b/src/lib/api/feed/author.ts
@@ -8,7 +8,7 @@ import {FeedAPI, FeedAPIResponse} from './types'
export class AuthorFeedAPI implements FeedAPI {
agent: BskyAgent
- params: GetAuthorFeed.QueryParams
+ _params: GetAuthorFeed.QueryParams
constructor({
agent,
@@ -18,7 +18,13 @@ export class AuthorFeedAPI implements FeedAPI {
feedParams: GetAuthorFeed.QueryParams
}) {
this.agent = agent
- this.params = feedParams
+ this._params = feedParams
+ }
+
+ get params() {
+ const params = {...this._params}
+ params.includePins = params.filter !== 'posts_with_media'
+ return params
}
async peekLatest(): Promise {
@@ -57,8 +63,9 @@ export class AuthorFeedAPI implements FeedAPI {
return feed.filter(post => {
const isReply = post.reply
const isRepost = AppBskyFeedDefs.isReasonRepost(post.reason)
+ const isPin = AppBskyFeedDefs.isReasonPin(post.reason)
if (!isReply) return true
- if (isRepost) return true
+ if (isRepost || isPin) return true
return isReply && isAuthorReplyChain(this.params.actor, post, feed)
})
}
diff --git a/src/lib/custom-animations/PressableScale.tsx b/src/lib/custom-animations/PressableScale.tsx
new file mode 100644
index 0000000000..ca080dc8ae
--- /dev/null
+++ b/src/lib/custom-animations/PressableScale.tsx
@@ -0,0 +1,59 @@
+import React from 'react'
+import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
+import Animated, {
+ cancelAnimation,
+ runOnJS,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from 'react-native-reanimated'
+
+import {isTouchDevice} from '#/lib/browser'
+import {isNative} from '#/platform/detection'
+
+const DEFAULT_TARGET_SCALE = isNative || isTouchDevice ? 0.98 : 1
+
+const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
+
+export function PressableScale({
+ targetScale = DEFAULT_TARGET_SCALE,
+ children,
+ style,
+ onPressIn,
+ onPressOut,
+ ...rest
+}: {
+ targetScale?: number
+ style?: StyleProp
+} & Exclude) {
+ const scale = useSharedValue(1)
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{scale: scale.value}],
+ }))
+
+ return (
+ {
+ 'worklet'
+ if (onPressIn) {
+ runOnJS(onPressIn)(e)
+ }
+ cancelAnimation(scale)
+ scale.value = withTiming(targetScale, {duration: 100})
+ }}
+ onPressOut={e => {
+ 'worklet'
+ if (onPressOut) {
+ runOnJS(onPressOut)(e)
+ }
+ cancelAnimation(scale)
+ scale.value = withTiming(1, {duration: 100})
+ }}
+ style={[animatedStyle, style]}
+ {...rest}>
+ {children}
+
+ )
+}
diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts
index f588808fc3..234be777d3 100644
--- a/src/lib/haptics.ts
+++ b/src/lib/haptics.ts
@@ -1,8 +1,10 @@
import React from 'react'
+import * as Device from 'expo-device'
import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics'
import {isIOS, isWeb} from '#/platform/detection'
import {useHapticsDisabled} from '#/state/preferences/disable-haptics'
+import * as Toast from '#/view/com/util/Toast'
export function useHaptics() {
const isHapticsDisabled = useHapticsDisabled()
@@ -18,6 +20,11 @@ export function useHaptics() {
? ImpactFeedbackStyle[strength]
: ImpactFeedbackStyle.Light
impactAsync(style)
+
+ // DEV ONLY - show a toast when a haptic is meant to fire on simulator
+ if (__DEV__ && !Device.isDevice) {
+ Toast.show(`Buzzz!`)
+ }
},
[isHapticsDisabled],
)
diff --git a/src/lib/hooks/useAccountSwitcher.ts b/src/lib/hooks/useAccountSwitcher.ts
index 09ff30277f..22eb348f2e 100644
--- a/src/lib/hooks/useAccountSwitcher.ts
+++ b/src/lib/hooks/useAccountSwitcher.ts
@@ -2,7 +2,6 @@ import {useCallback, useState} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {SessionAccount, useSessionApi} from '#/state/session'
@@ -14,7 +13,6 @@ import {LogEvents} from '../statsig/statsig'
export function useAccountSwitcher() {
const [pendingDid, setPendingDid] = useState(null)
const {_} = useLingui()
- const {track} = useAnalytics()
const {resumeSession} = useSessionApi()
const {requestSwitchToAccount} = useLoggedOutViewControls()
@@ -23,7 +21,6 @@ export function useAccountSwitcher() {
account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'],
) => {
- track('Settings:SwitchAccountButtonClicked')
if (pendingDid) {
// The session API isn't resilient to race conditions so let's just ignore this.
return
@@ -62,7 +59,7 @@ export function useAccountSwitcher() {
setPendingDid(null)
}
},
- [_, track, resumeSession, requestSwitchToAccount, pendingDid],
+ [_, resumeSession, requestSwitchToAccount, pendingDid],
)
return {onPressSwitchAccount, pendingDid}
diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts
index fd16387034..ce1d474d31 100644
--- a/src/lib/hooks/useIntentHandler.ts
+++ b/src/lib/hooks/useIntentHandler.ts
@@ -1,11 +1,11 @@
import React from 'react'
import * as Linking from 'expo-linking'
-import {logEvent} from 'lib/statsig/statsig'
-import {isNative} from 'platform/detection'
-import {useSession} from 'state/session'
-import {useComposerControls} from 'state/shell'
-import {useCloseAllActiveElements} from 'state/util'
+import {logEvent} from '#/lib/statsig/statsig'
+import {isNative} from '#/platform/detection'
+import {useSession} from '#/state/session'
+import {useComposerControls} from '#/state/shell'
+import {useCloseAllActiveElements} from '#/state/util'
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
@@ -52,6 +52,7 @@ export function useIntentHandler() {
composeIntent({
text: params.get('text'),
imageUrisStr: params.get('imageUris'),
+ videoUri: params.get('videoUri'),
})
return
}
@@ -80,14 +81,25 @@ export function useComposeIntent() {
({
text,
imageUrisStr,
+ videoUri,
}: {
text: string | null
- imageUrisStr: string | null // unused for right now, will be used later with intents
+ imageUrisStr: string | null
+ videoUri: string | null
}) => {
if (!hasSession) return
closeAllActiveElements()
+ // Whenever a video URI is present, we don't support adding images right now.
+ if (videoUri) {
+ openComposer({
+ text: text ?? undefined,
+ videoUri,
+ })
+ return
+ }
+
const imageUris = imageUrisStr
?.split(',')
.filter(part => {
diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts
index e4e7e14744..625ec9e6a9 100644
--- a/src/lib/hooks/useNotificationHandler.ts
+++ b/src/lib/hooks/useNotificationHandler.ts
@@ -3,19 +3,18 @@ import * as Notifications from 'expo-notifications'
import {CommonActions, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
+import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
+import {NavigationProp} from '#/lib/routes/types'
+import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
-import {track} from 'lib/analytics/analytics'
-import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
-import {NavigationProp} from 'lib/routes/types'
-import {logEvent} from 'lib/statsig/statsig'
-import {isAndroid} from 'platform/detection'
-import {useCurrentConvoId} from 'state/messages/current-convo-id'
-import {RQKEY as RQKEY_NOTIFS} from 'state/queries/notifications/feed'
-import {invalidateCachedUnreadPage} from 'state/queries/notifications/unread'
-import {truncateAndInvalidate} from 'state/queries/util'
-import {useSession} from 'state/session'
-import {useLoggedOutViewControls} from 'state/shell/logged-out'
-import {useCloseAllActiveElements} from 'state/util'
+import {isAndroid} from '#/platform/detection'
+import {useCurrentConvoId} from '#/state/messages/current-convo-id'
+import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
+import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
+import {truncateAndInvalidate} from '#/state/queries/util'
+import {useSession} from '#/state/session'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {useCloseAllActiveElements} from '#/state/util'
import {resetToTab} from '#/Navigation'
type NotificationReason =
@@ -228,7 +227,6 @@ export function useNotificationsHandler() {
{},
logger.DebugContext.notifications,
)
- track('Notificatons:OpenApp')
logEvent('notifications:openApp', {})
invalidateCachedUnreadPage()
truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
diff --git a/src/lib/link-meta/html.ts b/src/lib/link-meta/html.ts
deleted file mode 100644
index 220f8431d5..0000000000
--- a/src/lib/link-meta/html.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import {extractTwitterMeta} from './twitter'
-import {extractYoutubeMeta} from './youtube'
-
-interface ExtractHtmlMetaInput {
- html: string
- hostname?: string
- pathname?: string
-}
-
-export const extractHtmlMeta = ({
- html,
- hostname,
- pathname,
-}: ExtractHtmlMetaInput): Record => {
- const htmlTitleRegex = /([^<]+)<\/title>/i
-
- let res: Record = {}
-
- const match = htmlTitleRegex.exec(html)
-
- if (match) {
- res.title = match[1].trim()
- }
-
- let metaMatch
- let propMatch
- const metaRe = / ]+)>/gis
- while ((metaMatch = metaRe.exec(html))) {
- let propName
- let propValue
- const propRe = /(name|property|content)="([^"]+)"/gis
- while ((propMatch = propRe.exec(metaMatch[1]))) {
- if (propMatch[1] === 'content') {
- propValue = propMatch[2]
- } else {
- propName = propMatch[2]
- }
- }
- if (!propName || !propValue) {
- continue
- }
- switch (propName?.trim()) {
- case 'title':
- case 'og:title':
- case 'twitter:title':
- res.title = propValue?.trim()
- break
- case 'description':
- case 'og:description':
- case 'twitter:description':
- res.description = propValue?.trim()
- break
- case 'og:image':
- case 'twitter:image':
- res.image = propValue?.trim()
- break
- }
- }
-
- const isYoutubeUrl =
- hostname?.includes('youtube.') || hostname?.includes('youtu.be')
- const isTwitterUrl = hostname?.includes('twitter.')
- // Workaround for some websites not having a title or description in the meta tags in the initial serve
- if (isYoutubeUrl) {
- res = {...res, ...extractYoutubeMeta(html)}
- } else if (isTwitterUrl && pathname) {
- res = {...extractTwitterMeta({pathname})}
- }
-
- return res
-}
diff --git a/src/lib/link-meta/twitter.ts b/src/lib/link-meta/twitter.ts
deleted file mode 100644
index d785903c00..0000000000
--- a/src/lib/link-meta/twitter.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-export const extractTwitterMeta = ({
- pathname,
-}: {
- pathname: string
-}): Record => {
- const res = {title: 'Twitter'}
- const parsedPathname = pathname.split('/')
- if (parsedPathname.length <= 1 || parsedPathname[1].length <= 1) {
- // Excluding one letter usernames as they're reserved by twitter for things like cases like twitter.com/i/articles/follows/-1675653703
- return res
- }
- const username = parsedPathname?.[1]
- const isUserProfile = parsedPathname?.length === 2
-
- res.title = isUserProfile
- ? `@${username} on Twitter`
- : `Tweet by @${username}`
-
- return res
-}
diff --git a/src/lib/link-meta/youtube.ts b/src/lib/link-meta/youtube.ts
deleted file mode 100644
index 42eed51e8f..0000000000
--- a/src/lib/link-meta/youtube.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export const extractYoutubeMeta = (html: string): Record => {
- const res: Record = {}
- const youtubeTitleRegex = /"videoDetails":.*"title":"([^"]*)"/i
- const youtubeDescriptionRegex =
- /"videoDetails":.*"shortDescription":"([^"]*)"/i
- const youtubeThumbnailRegex = /"videoDetails":.*"url":"(.*)(default\.jpg)/i
- const youtubeAvatarRegex =
- /"avatar":{"thumbnails":\[{.*?url.*?url.*?url":"([^"]*)"/i
- const youtubeTitleMatch = youtubeTitleRegex.exec(html)
- const youtubeDescriptionMatch = youtubeDescriptionRegex.exec(html)
- const youtubeThumbnailMatch = youtubeThumbnailRegex.exec(html)
- const youtubeAvatarMatch = youtubeAvatarRegex.exec(html)
-
- if (youtubeTitleMatch && youtubeTitleMatch.length >= 1) {
- res.title = decodeURI(youtubeTitleMatch[1])
- }
- if (youtubeDescriptionMatch && youtubeDescriptionMatch.length >= 1) {
- res.description = decodeURI(youtubeDescriptionMatch[1]).replace(
- /\\n/g,
- '\n',
- )
- }
- if (youtubeThumbnailMatch && youtubeThumbnailMatch.length >= 2) {
- res.image = youtubeThumbnailMatch[1] + 'default.jpg'
- }
- if (!res.image && youtubeAvatarMatch && youtubeAvatarMatch.length >= 1) {
- res.image = youtubeAvatarMatch[1]
- }
-
- return res
-}
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index 7966767d1b..866d87aef0 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -1,3 +1,5 @@
export type Gate =
// Keep this alphabetic please.
- 'debug_show_feedcontext' | 'suggested_feeds_interstitial'
+ | 'debug_show_feedcontext'
+ | 'post_feed_lang_window'
+ | 'suggested_feeds_interstitial'
diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts
index 3bae771c0a..d0d8277c86 100644
--- a/src/lib/strings/embed-player.ts
+++ b/src/lib/strings/embed-player.ts
@@ -1,7 +1,7 @@
import {Dimensions} from 'react-native'
-import {isSafari} from 'lib/browser'
-import {isWeb} from 'platform/detection'
+import {isSafari} from '#/lib/browser'
+import {isWeb} from '#/platform/detection'
const {height: SCREEN_HEIGHT} = Dimensions.get('window')
@@ -185,6 +185,20 @@ export function parseEmbedPlayerFromUrl(
playerUri: `https://open.spotify.com/embed/track/${id ?? idOrType}`,
}
}
+ if (typeOrLocale === 'episode' || idOrType === 'episode') {
+ return {
+ type: 'spotify_song',
+ source: 'spotify',
+ playerUri: `https://open.spotify.com/embed/episode/${id ?? idOrType}`,
+ }
+ }
+ if (typeOrLocale === 'show' || idOrType === 'show') {
+ return {
+ type: 'spotify_song',
+ source: 'spotify',
+ playerUri: `https://open.spotify.com/embed/show/${id ?? idOrType}`,
+ }
+ }
}
}
diff --git a/src/lib/strings/errors.ts b/src/lib/strings/errors.ts
index 899d8ebce4..7d00c5e7f5 100644
--- a/src/lib/strings/errors.ts
+++ b/src/lib/strings/errors.ts
@@ -20,11 +20,19 @@ export function cleanError(str: any): string {
return str
}
+const NETWORK_ERRORS = [
+ 'Abort',
+ 'Network request failed',
+ 'Failed to fetch',
+ 'Load failed',
+]
+
export function isNetworkError(e: unknown) {
const str = String(e)
- return (
- str.includes('Abort') ||
- str.includes('Network request failed') ||
- str.includes('Failed to fetch')
- )
+ for (const err of NETWORK_ERRORS) {
+ if (str.includes(err)) {
+ return true
+ }
+ }
+ return false
}
diff --git a/src/logger/index.ts b/src/logger/index.ts
index d6d8d9fc1d..7bd812af00 100644
--- a/src/logger/index.ts
+++ b/src/logger/index.ts
@@ -1,10 +1,11 @@
import format from 'date-fns/format'
import {nanoid} from 'nanoid/non-secure'
-import {Sentry} from '#/logger/sentry'
-import * as env from '#/env'
+import {isNetworkError} from '#/lib/strings/errors'
import {DebugContext} from '#/logger/debugContext'
import {add} from '#/logger/logDump'
+import {Sentry} from '#/logger/sentry'
+import * as env from '#/env'
export enum LogLevel {
Debug = 'debug',
@@ -160,6 +161,11 @@ export const sentryTransport: Transport = (
timestamp: timestamp / 1000, // Sentry expects seconds
})
+ // We don't want to send any network errors to sentry
+ if (isNetworkError(message)) {
+ return
+ }
+
/**
* Send all higher levels with `captureMessage`, with appropriate severity
* level
diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx
index 678ba51237..9765786ecf 100644
--- a/src/screens/Login/ChooseAccountForm.tsx
+++ b/src/screens/Login/ChooseAccountForm.tsx
@@ -3,7 +3,6 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
@@ -23,16 +22,11 @@ export const ChooseAccountForm = ({
onPressBack: () => void
}) => {
const [pendingDid, setPendingDid] = React.useState(null)
- const {track, screen} = useAnalytics()
const {_} = useLingui()
const {currentAccount} = useSession()
const {resumeSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
- React.useEffect(() => {
- screen('Choose Account')
- }, [screen])
-
const onSelect = React.useCallback(
async (account: SessionAccount) => {
if (pendingDid) {
@@ -56,7 +50,6 @@ export const ChooseAccountForm = ({
logContext: 'ChooseAccountForm',
withPassword: false,
})
- track('Sign In', {resumedSession: true})
Toast.show(_(msg`Signed in as @${account.handle}`))
} catch (e: any) {
logger.error('choose account: initSession failed', {
@@ -70,7 +63,6 @@ export const ChooseAccountForm = ({
},
[
currentAccount,
- track,
resumeSession,
pendingDid,
onSelectAccount,
diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx
index 7acaae5101..e8582f46f5 100644
--- a/src/screens/Login/ForgotPasswordForm.tsx
+++ b/src/screens/Login/ForgotPasswordForm.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useState} from 'react'
+import React, {useState} from 'react'
import {ActivityIndicator, Keyboard, View} from 'react-native'
import {ComAtprotoServerDescribeServer} from '@atproto/api'
import {BskyAgent} from '@atproto/api'
@@ -6,7 +6,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as EmailValidator from 'email-validator'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {isNetworkError} from '#/lib/strings/errors'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
@@ -41,13 +40,8 @@ export const ForgotPasswordForm = ({
const t = useTheme()
const [isProcessing, setIsProcessing] = useState(false)
const [email, setEmail] = useState('')
- const {screen} = useAnalytics()
const {_} = useLingui()
- useEffect(() => {
- screen('Signin:ForgotPassword')
- }, [screen])
-
const onPressSelectService = React.useCallback(() => {
Keyboard.dismiss()
}, [])
diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx
index c2038b2879..f3661ac92c 100644
--- a/src/screens/Login/LoginForm.tsx
+++ b/src/screens/Login/LoginForm.tsx
@@ -13,15 +13,14 @@ import {
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
+import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {isNetworkError} from '#/lib/strings/errors'
import {cleanError} from '#/lib/strings/errors'
import {createFullHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
+import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
import {useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
-import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
-import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {FormError} from '#/components/forms/FormError'
@@ -57,7 +56,6 @@ export const LoginForm = ({
onPressBack: () => void
onPressForgotPassword: () => void
}) => {
- const {track} = useAnalytics()
const t = useTheme()
const [isProcessing, setIsProcessing] = useState(false)
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
@@ -74,8 +72,7 @@ export const LoginForm = ({
const onPressSelectService = React.useCallback(() => {
Keyboard.dismiss()
- track('Signin:PressedSelectService')
- }, [track])
+ }, [])
const onPressNext = async () => {
if (isProcessing) return
diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx
index 03e7d86696..9c12a47e3f 100644
--- a/src/screens/Login/PasswordUpdatedForm.tsx
+++ b/src/screens/Login/PasswordUpdatedForm.tsx
@@ -1,9 +1,8 @@
-import React, {useEffect} from 'react'
+import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {atoms as a, useBreakpoints} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
@@ -14,14 +13,9 @@ export const PasswordUpdatedForm = ({
}: {
onPressNext: () => void
}) => {
- const {screen} = useAnalytics()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
- useEffect(() => {
- screen('Signin:PasswordUpdatedForm')
- }, [screen])
-
return (
void
onPasswordSet: () => void
}) => {
- const {screen} = useAnalytics()
const {_} = useLingui()
const t = useTheme()
- useEffect(() => {
- screen('Signin:SetNewPasswordForm')
- }, [screen])
-
const [isProcessing, setIsProcessing] = useState(false)
const [resetCode, setResetCode] = useState('')
const [password, setPassword] = useState('')
diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx
index 1fce63d298..b46f8d26bf 100644
--- a/src/screens/Login/index.tsx
+++ b/src/screens/Login/index.tsx
@@ -4,7 +4,6 @@ import {LayoutAnimationConfig} from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {DEFAULT_SERVICE} from '#/lib/constants'
import {logger} from '#/logger'
import {useServiceQuery} from '#/state/queries/service'
@@ -31,7 +30,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
const {_} = useLingui()
const {accounts} = useSession()
- const {track} = useAnalytics()
const {requestedAccountSwitchTo} = useLoggedOutView()
const requestedAccount = accounts.find(
acc => acc.did === requestedAccountSwitchTo,
@@ -87,7 +85,6 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
}, [serviceError, serviceUrl, _])
const onPressForgotPassword = () => {
- track('Signin:PressedForgotPassword')
setCurrentForm(Forms.ForgotPassword)
}
diff --git a/src/screens/Messages/Conversation/MessageInputEmbed.tsx b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
index bf28ed4fe9..2d1551019e 100644
--- a/src/screens/Messages/Conversation/MessageInputEmbed.tsx
+++ b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
@@ -174,7 +174,6 @@ export function MessageInputEmbed({
showAvatar
author={post.author}
moderation={moderation}
- authorHasWarning={!!post.author.labels?.length}
timestamp={post.indexedAt}
postHref={itemHref}
style={a.flex_0}
diff --git a/src/screens/Messages/List/ChatListItem.tsx b/src/screens/Messages/List/ChatListItem.tsx
index e9668b4e11..11c071082b 100644
--- a/src/screens/Messages/List/ChatListItem.tsx
+++ b/src/screens/Messages/List/ChatListItem.tsx
@@ -24,8 +24,9 @@ import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session'
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
+import * as tokens from '#/alf/tokens'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
import {Link} from '#/components/Link'
@@ -203,6 +204,19 @@ function ChatListItemReady({
onFocus={onFocus}
onBlur={onMouseLeave}
style={[a.relative]}>
+
+
+
+
-
+ {/* Avatar goes here */}
+
@@ -357,7 +368,7 @@ function ChatListItemReady({
a.self_end,
a.justify_center,
{
- right: a.px_lg.paddingRight,
+ right: tokens.space.lg,
opacity: !gtMobile || showActions || menuControl.isOpen ? 1 : 0,
},
]}
diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx
index 9bfe6c3fac..070b879508 100644
--- a/src/screens/Moderation/index.tsx
+++ b/src/screens/Moderation/index.tsx
@@ -7,7 +7,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {logger} from '#/logger'
@@ -163,7 +162,6 @@ export function ModerationScreenInner({
const {_} = useLingui()
const t = useTheme()
const setMinimalShellMode = useSetMinimalShellMode()
- const {screen} = useAnalytics()
const {gtMobile} = useBreakpoints()
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
const birthdateDialogControl = Dialog.useDialogControl()
@@ -175,9 +173,8 @@ export function ModerationScreenInner({
useFocusEffect(
React.useCallback(() => {
- screen('Moderation')
setMinimalShellMode(false)
- }, [screen, setMinimalShellMode]),
+ }, [setMinimalShellMode]),
)
const {mutateAsync: setAdultContentPref, variables: optimisticAdultContent} =
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx
index bc765781af..fdc0a3eb72 100644
--- a/src/screens/Onboarding/StepFinished.tsx
+++ b/src/screens/Onboarding/StepFinished.tsx
@@ -7,27 +7,26 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
-import {useAnalytics} from '#/lib/analytics/analytics'
+import {uploadBlob} from '#/lib/api'
import {
BSKY_APP_ACCOUNT_DID,
DISCOVER_SAVED_FEED,
TIMELINE_SAVED_FEED,
} from '#/lib/constants'
+import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
+import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
+import {getAllListMembers} from '#/state/queries/list-members'
import {preferencesQueryKey} from '#/state/queries/preferences'
import {RQKEY as profileRQKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {useProgressGuideControls} from '#/state/shell/progress-guide'
-import {uploadBlob} from 'lib/api'
-import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
-import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
-import {getAllListMembers} from 'state/queries/list-members'
import {
useActiveStarterPack,
useSetActiveStarterPack,
-} from 'state/shell/starter-pack'
+} from '#/state/shell/starter-pack'
import {
DescriptionText,
OnboardingControls,
@@ -48,7 +47,6 @@ import {Text} from '#/components/Typography'
export function StepFinished() {
const {_} = useLingui()
const t = useTheme()
- const {track} = useAnalytics()
const {state, dispatch} = React.useContext(Context)
const onboardDispatch = useOnboardingDispatch()
const [saving, setSaving] = React.useState(false)
@@ -190,8 +188,6 @@ export function StepFinished() {
startProgressGuide('like-10-and-follow-7')
dispatch({type: 'finish'})
onboardDispatch({type: 'finish'})
- track('OnboardingV2:StepFinished:End')
- track('OnboardingV2:Complete')
logEvent('onboarding:finished:nextPressed', {
usedStarterPack: Boolean(starterPack),
starterPackName: AppBskyGraphStarterpack.isRecord(starterPack?.record)
@@ -214,7 +210,6 @@ export function StepFinished() {
agent,
dispatch,
onboardDispatch,
- track,
activeStarterPack,
state,
requestNotificationsPermission,
@@ -223,10 +218,6 @@ export function StepFinished() {
startProgressGuide,
])
- React.useEffect(() => {
- track('OnboardingV2:StepFinished:Start')
- }, [track])
-
return (
diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx
index ded473ff59..2f41433aa5 100644
--- a/src/screens/Onboarding/StepInterests/index.tsx
+++ b/src/screens/Onboarding/StepInterests/index.tsx
@@ -4,7 +4,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQuery} from '@tanstack/react-query'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {capitalize} from '#/lib/strings/capitalize'
import {logger} from '#/logger'
@@ -36,7 +35,6 @@ export function StepInterests() {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
- const {track} = useAnalytics()
const interestsDisplayNames = useInterestsDisplayNames()
const {state, dispatch} = React.useContext(Context)
@@ -90,7 +88,6 @@ export function StepInterests() {
`onboarding: getTaggedSuggestions fetch or processing failed`,
)
logger.error(e)
- track('OnboardingV2:StepInterests:Error')
throw new Error(`a network error occurred`)
}
@@ -108,11 +105,6 @@ export function StepInterests() {
selectedInterests: interests,
})
dispatch({type: 'next'})
-
- track('OnboardingV2:StepInterests:End', {
- selectedInterests: interests,
- selectedInterestsLength: interests.length,
- })
logEvent('onboarding:interests:nextPressed', {
selectedInterests: interests,
selectedInterestsLength: interests.length,
@@ -121,18 +113,12 @@ export function StepInterests() {
logger.info(`onboading: error saving interests`)
logger.error(e)
}
- }, [interests, data, setSaving, dispatch, track])
+ }, [interests, data, setSaving, dispatch])
const skipOnboarding = React.useCallback(() => {
onboardDispatch({type: 'finish'})
dispatch({type: 'finish'})
- track('OnboardingV2:Skip')
- }, [onboardDispatch, dispatch, track])
-
- React.useEffect(() => {
- track('OnboardingV2:Begin')
- track('OnboardingV2:StepInterests:Start')
- }, [track])
+ }, [onboardDispatch, dispatch])
const title = isError ? (
Oh no! Something went wrong.
diff --git a/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx
index d1d1af6d9f..eaad2113f8 100644
--- a/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx
+++ b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx
@@ -1,14 +1,19 @@
import React from 'react'
import {View} from 'react-native'
-import ViewShot from 'react-native-view-shot'
+import type ViewShot from 'react-native-view-shot'
import {useAvatar} from '#/screens/Onboarding/StepProfile/index'
import {atoms as a} from '#/alf'
+const LazyViewShot = React.lazy(
+ // @ts-expect-error dynamic import
+ () => import('react-native-view-shot/src/index'),
+)
+
const SIZE_MULTIPLIER = 5
export interface PlaceholderCanvasRef {
- capture: () => Promise
+ capture: () => Promise
}
// This component is supposed to be invisible to the user. We only need this for ViewShot to have something to
@@ -16,7 +21,7 @@ export interface PlaceholderCanvasRef {
export const PlaceholderCanvas = React.forwardRef(
function PlaceholderCanvas({}, ref) {
const {avatar} = useAvatar()
- const viewshotRef = React.useRef()
+ const viewshotRef = React.useRef(null)
const Icon = avatar.placeholder.component
const styles = React.useMemo(
@@ -32,13 +37,16 @@ export const PlaceholderCanvas = React.forwardRef(
)
React.useImperativeHandle(ref, () => ({
- // @ts-ignore this library doesn't have types
- capture: viewshotRef.current.capture,
+ capture: async () => {
+ if (viewshotRef.current?.capture) {
+ return await viewshotRef.current.capture()
+ }
+ },
}))
return (
- (
style={{color: 'white'}}
/>
-
+
)
},
diff --git a/src/screens/Onboarding/StepProfile/index.tsx b/src/screens/Onboarding/StepProfile/index.tsx
index 5304aa5031..663418f220 100644
--- a/src/screens/Onboarding/StepProfile/index.tsx
+++ b/src/screens/Onboarding/StepProfile/index.tsx
@@ -9,14 +9,13 @@ import {
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
+import {usePhotoLibraryPermission} from '#/lib/hooks/usePermissions'
+import {compressIfNeeded} from '#/lib/media/manip'
+import {openCropper} from '#/lib/media/picker'
+import {getDataUriSize} from '#/lib/media/util'
+import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {logEvent, useGate} from '#/lib/statsig/statsig'
-import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
-import {compressIfNeeded} from 'lib/media/manip'
-import {openCropper} from 'lib/media/picker'
-import {getDataUriSize} from 'lib/media/util'
-import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
-import {isNative, isWeb} from 'platform/detection'
+import {isNative, isWeb} from '#/platform/detection'
import {
DescriptionText,
OnboardingControls,
@@ -68,7 +67,6 @@ export function StepProfile() {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
- const {track} = useAnalytics()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
const gate = useGate()
const requestNotificationsPermission = useRequestNotificationsPermission()
@@ -87,10 +85,6 @@ export function StepProfile() {
const canvasRef = React.useRef(null)
- React.useEffect(() => {
- track('OnboardingV2:StepProfile:Start')
- }, [track])
-
React.useEffect(() => {
requestNotificationsPermission('StartOnboarding')
}, [gate, requestNotificationsPermission])
@@ -132,6 +126,10 @@ export function StepProfile() {
const onContinue = React.useCallback(async () => {
let imageUri = avatar?.image?.path
+
+ // In the event that view-shot didn't load in time and the user pressed continue, this will just be undefined
+ // and the default avatar will be used. We don't want to block getting through create if this fails for some
+ // reason
if (!imageUri || avatar.useCreatedAvatar) {
imageUri = await canvasRef.current?.capture()
}
@@ -151,9 +149,8 @@ export function StepProfile() {
}
dispatch({type: 'next'})
- track('OnboardingV2:StepProfile:End')
logEvent('onboarding:profile:nextPressed', {})
- }, [avatar, dispatch, track])
+ }, [avatar, dispatch])
const onDoneCreating = React.useCallback(() => {
setAvatar(prev => ({
diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts
index c41db5c3b7..70fa696408 100644
--- a/src/screens/Onboarding/state.ts
+++ b/src/screens/Onboarding/state.ts
@@ -51,13 +51,15 @@ export type OnboardingAction =
| {
type: 'setProfileStepResults'
isCreatedAvatar: boolean
- image?: OnboardingState['profileStepResults']['image']
- imageUri: string
+ image: OnboardingState['profileStepResults']['image'] | undefined
+ imageUri: string | undefined
imageMime: string
- creatorState?: {
- emoji: Emoji
- backgroundColor: AvatarColor
- }
+ creatorState:
+ | {
+ emoji: Emoji
+ backgroundColor: AvatarColor
+ }
+ | undefined
}
export type ApiResponseMap = {
diff --git a/src/screens/Profile/Header/DisplayName.tsx b/src/screens/Profile/Header/DisplayName.tsx
index e30162c3af..bcc56d7f66 100644
--- a/src/screens/Profile/Header/DisplayName.tsx
+++ b/src/screens/Profile/Header/DisplayName.tsx
@@ -5,7 +5,7 @@ import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {Shadow} from '#/state/cache/types'
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
export function ProfileHeaderDisplayName({
@@ -16,12 +16,19 @@ export function ProfileHeaderDisplayName({
moderation: ModerationDecision
}) {
const t = useTheme()
+ const {gtMobile} = useBreakpoints()
+
return (
+ style={[
+ t.atoms.text,
+ gtMobile ? a.text_4xl : a.text_3xl,
+ a.self_start,
+ {fontWeight: '600'},
+ ]}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
diff --git a/src/screens/Profile/Header/GrowableAvatar.tsx b/src/screens/Profile/Header/GrowableAvatar.tsx
new file mode 100644
index 0000000000..20ac14892d
--- /dev/null
+++ b/src/screens/Profile/Header/GrowableAvatar.tsx
@@ -0,0 +1,61 @@
+import React from 'react'
+import {StyleProp, View, ViewStyle} from 'react-native'
+import Animated, {
+ Extrapolation,
+ interpolate,
+ SharedValue,
+ useAnimatedStyle,
+} from 'react-native-reanimated'
+
+import {isIOS} from '#/platform/detection'
+import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
+
+export function GrowableAvatar({
+ children,
+ style,
+}: {
+ children: React.ReactNode
+ style?: StyleProp
+}) {
+ const pagerContext = usePagerHeaderContext()
+
+ // pagerContext should only be present on iOS, but better safe than sorry
+ if (!pagerContext || !isIOS) {
+ return {children}
+ }
+
+ const {scrollY} = pagerContext
+
+ return (
+
+ {children}
+
+ )
+}
+
+function GrowableAvatarInner({
+ scrollY,
+ children,
+ style,
+}: {
+ scrollY: SharedValue
+ children: React.ReactNode
+ style?: StyleProp
+}) {
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [
+ {
+ scale: interpolate(scrollY.value, [-150, 0], [1.2, 1], {
+ extrapolateRight: Extrapolation.CLAMP,
+ }),
+ },
+ ],
+ }))
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/screens/Profile/Header/GrowableBanner.tsx b/src/screens/Profile/Header/GrowableBanner.tsx
new file mode 100644
index 0000000000..e1bb8e00ef
--- /dev/null
+++ b/src/screens/Profile/Header/GrowableBanner.tsx
@@ -0,0 +1,212 @@
+import React, {useEffect, useState} from 'react'
+import {View} from 'react-native'
+import {ActivityIndicator} from 'react-native'
+import Animated, {
+ Extrapolation,
+ interpolate,
+ runOnJS,
+ SharedValue,
+ useAnimatedProps,
+ useAnimatedReaction,
+ useAnimatedStyle,
+} from 'react-native-reanimated'
+import {BlurView} from 'expo-blur'
+import {useIsFetching} from '@tanstack/react-query'
+
+import {isIOS} from '#/platform/detection'
+import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs'
+import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed'
+import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens'
+import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists'
+import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
+import {atoms as a} from '#/alf'
+
+const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
+
+export function GrowableBanner({
+ backButton,
+ children,
+}: {
+ backButton?: React.ReactNode
+ children: React.ReactNode
+}) {
+ const pagerContext = usePagerHeaderContext()
+
+ // pagerContext should only be present on iOS, but better safe than sorry
+ if (!pagerContext || !isIOS) {
+ return (
+
+ {backButton}
+ {children}
+
+ )
+ }
+
+ const {scrollY} = pagerContext
+
+ return (
+
+ {children}
+
+ )
+}
+
+function GrowableBannerInner({
+ scrollY,
+ backButton,
+ children,
+}: {
+ scrollY: SharedValue
+ backButton?: React.ReactNode
+ children: React.ReactNode
+}) {
+ const isFetching = useIsProfileFetching()
+ const animateSpinner = useShouldAnimateSpinner({isFetching, scrollY})
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [
+ {
+ scale: interpolate(scrollY.value, [-150, 0], [2, 1], {
+ extrapolateRight: Extrapolation.CLAMP,
+ }),
+ },
+ ],
+ }))
+
+ const animatedBlurViewProps = useAnimatedProps(() => {
+ return {
+ intensity: interpolate(
+ scrollY.value,
+ [-300, -65, -15],
+ [50, 40, 0],
+ Extrapolation.CLAMP,
+ ),
+ }
+ })
+
+ const animatedSpinnerStyle = useAnimatedStyle(() => {
+ return {
+ display: scrollY.value < 0 ? 'flex' : 'none',
+ opacity: interpolate(
+ scrollY.value,
+ [-60, -15],
+ [1, 0],
+ Extrapolation.CLAMP,
+ ),
+ transform: [
+ {translateY: interpolate(scrollY.value, [-150, 0], [-75, 0])},
+ {rotate: '90deg'},
+ ],
+ }
+ })
+
+ const animatedBackButtonStyle = useAnimatedStyle(() => ({
+ transform: [
+ {
+ translateY: interpolate(scrollY.value, [-150, 60], [-150, 60], {
+ extrapolateRight: Extrapolation.CLAMP,
+ }),
+ },
+ ],
+ }))
+
+ return (
+ <>
+
+ {children}
+
+
+
+
+
+
+
+
+ {backButton}
+
+ >
+ )
+}
+
+function useIsProfileFetching() {
+ // are any of the profile-related queries fetching?
+ return [
+ useIsFetching({queryKey: [FEED_RQKEY_ROOT]}),
+ useIsFetching({queryKey: [FEEDGEN_RQKEY_ROOT]}),
+ useIsFetching({queryKey: [LIST_RQKEY_ROOT]}),
+ useIsFetching({queryKey: [STARTERPACK_RQKEY_ROOT]}),
+ ].some(isFetching => isFetching)
+}
+
+function useShouldAnimateSpinner({
+ isFetching,
+ scrollY,
+}: {
+ isFetching: boolean
+ scrollY: SharedValue
+}) {
+ const [isOverscrolled, setIsOverscrolled] = useState(false)
+ // HACK: it reports a scroll pos of 0 for a tick when fetching finishes
+ // so paper over that by keeping it true for a bit -sfn
+ const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10)
+
+ useAnimatedReaction(
+ () => scrollY.value < -5,
+ (value, prevValue) => {
+ if (value !== prevValue) {
+ runOnJS(setIsOverscrolled)(value)
+ }
+ },
+ [scrollY],
+ )
+
+ const [isAnimating, setIsAnimating] = useState(isFetching)
+
+ if (isFetching && !isAnimating) {
+ setIsAnimating(true)
+ }
+
+ if (!isFetching && isAnimating && !stickyIsOverscrolled) {
+ setIsAnimating(false)
+ }
+
+ return isAnimating
+}
+
+// stayed true for at least `delay` ms before returning to false
+function useStickyToggle(value: boolean, delay: number) {
+ const [prevValue, setPrevValue] = useState(value)
+ const [isSticking, setIsSticking] = useState(false)
+
+ useEffect(() => {
+ if (isSticking) {
+ const timeout = setTimeout(() => setIsSticking(false), delay)
+ return () => clearTimeout(timeout)
+ }
+ }, [isSticking, delay])
+
+ if (value !== prevValue) {
+ setIsSticking(prevValue) // Going true -> false should stick.
+ setPrevValue(value)
+ return prevValue ? true : value
+ }
+
+ return isSticking ? true : value
+}
diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
index a807c70dd9..7b44e58693 100644
--- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
@@ -12,18 +12,17 @@ import {useLingui} from '@lingui/react'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import {MAX_LABELERS} from '#/lib/constants'
+import {useHaptics} from '#/lib/haptics'
import {isAppLabeler} from '#/lib/moderation'
import {logger} from '#/logger'
+import {isIOS} from '#/platform/detection'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
import {Shadow} from '#/state/cache/types'
import {useModalControls} from '#/state/modals'
import {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useRequireAuth, useSession} from '#/state/session'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {useHaptics} from 'lib/haptics'
-import {isIOS} from 'platform/detection'
-import {useProfileShadow} from 'state/cache/profile-shadow'
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf'
@@ -66,7 +65,6 @@ let ProfileHeaderLabeler = ({
const {_} = useLingui()
const {currentAccount, hasSession} = useSession()
const {openModal} = useModalControls()
- const {track} = useAnalytics()
const requireAuth = useRequireAuth()
const playHaptic = useHaptics()
const cantSubscribePrompt = Prompt.usePromptControl()
@@ -102,12 +100,10 @@ let ProfileHeaderLabeler = ({
if (likeUri) {
await unlikeMod({uri: likeUri})
- track('CustomFeed:Unlike')
setLikeCount(c => c - 1)
setLikeUri('')
} else {
const res = await likeMod({uri: labeler.uri, cid: labeler.cid})
- track('CustomFeed:Like')
setLikeCount(c => c + 1)
setLikeUri(res.uri)
}
@@ -120,15 +116,14 @@ let ProfileHeaderLabeler = ({
)
logger.error(`Failed to toggle labeler like`, {message: e.message})
}
- }, [labeler, playHaptic, likeUri, unlikeMod, track, likeMod, _])
+ }, [labeler, playHaptic, likeUri, unlikeMod, likeMod, _])
const onPressEditProfile = React.useCallback(() => {
- track('ProfileHeader:EditProfileButtonClicked')
openModal({
name: 'edit-profile',
profile,
})
- }, [track, openModal, profile])
+ }, [openModal, profile])
const onPressSubscribe = React.useCallback(
() =>
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
index 846fa4424b..3bfc4bf2f2 100644
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -9,8 +9,10 @@ import {
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
import {Shadow} from '#/state/cache/types'
import {useModalControls} from '#/state/modals'
import {
@@ -18,9 +20,6 @@ import {
useProfileFollowMutationQueue,
} from '#/state/queries/profile'
import {useRequireAuth, useSession} from '#/state/session'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {useProfileShadow} from 'state/cache/profile-shadow'
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a} from '#/alf'
@@ -59,7 +58,6 @@ let ProfileHeaderStandard = ({
const {currentAccount, hasSession} = useSession()
const {_} = useLingui()
const {openModal} = useModalControls()
- const {track} = useAnalytics()
const moderation = useMemo(
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
@@ -77,17 +75,15 @@ let ProfileHeaderStandard = ({
profile.viewer?.blockingByList
const onPressEditProfile = React.useCallback(() => {
- track('ProfileHeader:EditProfileButtonClicked')
openModal({
name: 'edit-profile',
profile,
})
- }, [track, openModal, profile])
+ }, [openModal, profile])
const onPressFollow = () => {
requireAuth(async () => {
try {
- track('ProfileHeader:FollowButtonClicked')
await queueFollow()
Toast.show(
_(
@@ -109,7 +105,6 @@ let ProfileHeaderStandard = ({
const onPressUnfollow = () => {
requireAuth(async () => {
try {
- track('ProfileHeader:UnfollowButtonClicked')
await queueUnfollow()
Toast.show(
_(
@@ -129,7 +124,6 @@ let ProfileHeaderStandard = ({
}
const unblockAccount = React.useCallback(async () => {
- track('ProfileHeader:UnblockAccountButtonClicked')
try {
await queueUnblock()
Toast.show(_(msg`Account unblocked`))
@@ -139,7 +133,7 @@ let ProfileHeaderStandard = ({
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
- }, [_, queueUnblock, track])
+ }, [_, queueUnblock])
const isMe = React.useMemo(
() => currentAccount?.did === profile.did,
diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx
index 90c2830907..f7011fd359 100644
--- a/src/screens/Profile/Header/Shell.tsx
+++ b/src/screens/Profile/Header/Shell.tsx
@@ -6,19 +6,21 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
+import {BACK_HITSLOP} from '#/lib/constants'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {NavigationProp} from '#/lib/routes/types'
+import {isIOS} from '#/platform/detection'
import {Shadow} from '#/state/cache/types'
import {ProfileImageLightbox, useLightboxControls} from '#/state/lightbox'
import {useSession} from '#/state/session'
-import {BACK_HITSLOP} from 'lib/constants'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {NavigationProp} from 'lib/routes/types'
-import {isIOS} from 'platform/detection'
-import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
-import {UserAvatar} from 'view/com/util/UserAvatar'
-import {UserBanner} from 'view/com/util/UserBanner'
+import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {UserBanner} from '#/view/com/util/UserBanner'
import {atoms as a, useTheme} from '#/alf'
import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
+import {GrowableAvatar} from './GrowableAvatar'
+import {GrowableBanner} from './GrowableBanner'
interface Props {
profile: Shadow
@@ -63,20 +65,45 @@ let ProfileHeaderShell = ({
return (
-
- {isPlaceholderProfile ? (
-
- ) : (
-
- )}
+
+
+ {!isDesktop && !hideBackButton && (
+
+
+
+
+
+ )}
+ >
+ }>
+ {isPlaceholderProfile ? (
+
+ ) : (
+
+ )}
+
{children}
@@ -93,40 +120,29 @@ let ProfileHeaderShell = ({
)}
- {!isDesktop && !hideBackButton && (
+
-
-
+
+
- )}
-
-
-
-
-
+
)
}
@@ -144,6 +160,9 @@ const styles = StyleSheet.create({
borderRadius: 15,
// @ts-ignore web only
cursor: 'pointer',
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ alignItems: 'center',
+ justifyContent: 'center',
},
backBtn: {
width: 30,
@@ -152,10 +171,12 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
- avi: {
+ aviPosition: {
position: 'absolute',
top: 110,
left: 10,
+ },
+ avi: {
width: 94,
height: 94,
borderRadius: 47,
diff --git a/src/screens/Profile/Header/index.tsx b/src/screens/Profile/Header/index.tsx
index c7ef34b701..cdb0667d06 100644
--- a/src/screens/Profile/Header/index.tsx
+++ b/src/screens/Profile/Header/index.tsx
@@ -7,18 +7,22 @@ import {
RichText as RichTextAPI,
} from '@atproto/api'
-import {usePalette} from 'lib/hooks/usePalette'
-import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
+import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+import {useTheme} from '#/alf'
import {ProfileHeaderLabeler} from './ProfileHeaderLabeler'
import {ProfileHeaderStandard} from './ProfileHeaderStandard'
let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
- const pal = usePalette('default')
+ const t = useTheme()
return (
-
+
+ style={[
+ t.atoms.bg,
+ {borderColor: t.atoms.bg.backgroundColor},
+ styles.avi,
+ ]}>
diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx
index fc4eff02c8..22ac5df9a7 100644
--- a/src/screens/Profile/Sections/Feed.tsx
+++ b/src/screens/Profile/Sections/Feed.tsx
@@ -4,17 +4,18 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
+import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
+import {usePalette} from '#/lib/hooks/usePalette'
import {isNative} from '#/platform/detection'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
-import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
-import {usePalette} from 'lib/hooks/usePalette'
+import {Feed} from '#/view/com/posts/Feed'
+import {EmptyState} from '#/view/com/util/EmptyState'
+import {ListRef} from '#/view/com/util/List'
+import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {Text} from '#/view/com/util/text/Text'
-import {Feed} from 'view/com/posts/Feed'
-import {EmptyState} from 'view/com/util/EmptyState'
-import {ListRef} from 'view/com/util/List'
-import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
+import {ios} from '#/alf'
import {SectionRef} from './types'
interface FeedSectionProps {
@@ -82,6 +83,7 @@ export const ProfileFeedSection = React.forwardRef<
onScrolledDownChange={setIsScrolledDown}
renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight}
+ progressViewOffset={ios(0)}
renderEndOfFeed={ProfileEndOfFeed}
ignoreFilterFor={ignoreFilterFor}
initialNumToRender={
diff --git a/src/screens/Search/__tests__/utils.test.ts b/src/screens/Search/__tests__/utils.test.ts
new file mode 100644
index 0000000000..81610cc59a
--- /dev/null
+++ b/src/screens/Search/__tests__/utils.test.ts
@@ -0,0 +1,43 @@
+import {describe, expect, it} from '@jest/globals'
+
+import {parseSearchQuery} from '#/screens/Search/utils'
+
+describe(`parseSearchQuery`, () => {
+ const tests = [
+ {
+ input: `bluesky`,
+ output: {query: `bluesky`, params: {}},
+ },
+ {
+ input: `bluesky from:esb.lol`,
+ output: {query: `bluesky`, params: {from: `esb.lol`}},
+ },
+ {
+ input: `bluesky "from:esb.lol"`,
+ output: {query: `bluesky "from:esb.lol"`, params: {}},
+ },
+ {
+ input: `bluesky mentions:@esb.lol`,
+ output: {query: `bluesky`, params: {mentions: `@esb.lol`}},
+ },
+ {
+ input: `bluesky since:2021-01-01:00:00:00`,
+ output: {query: `bluesky`, params: {since: `2021-01-01:00:00:00`}},
+ },
+ {
+ input: `bluesky lang:"en"`,
+ output: {query: `bluesky`, params: {lang: `en`}},
+ },
+ {
+ input: `bluesky "literal" lang:en "from:invalid"`,
+ output: {query: `bluesky "literal" "from:invalid"`, params: {lang: `en`}},
+ },
+ ]
+
+ it.each(tests)(
+ `$input -> $output.query $output.params`,
+ ({input, output}) => {
+ expect(parseSearchQuery(input)).toEqual(output)
+ },
+ )
+})
diff --git a/src/screens/Search/utils.ts b/src/screens/Search/utils.ts
new file mode 100644
index 0000000000..dcf92c0926
--- /dev/null
+++ b/src/screens/Search/utils.ts
@@ -0,0 +1,43 @@
+export type Params = Record
+
+export function parseSearchQuery(rawQuery: string) {
+ let base = rawQuery
+ const rawLiterals = rawQuery.match(/[^:\w\d]".+?"/gi) || []
+
+ // remove literals from base
+ for (const literal of rawLiterals) {
+ base = base.replace(literal.trim(), '')
+ }
+
+ // find remaining params in base
+ const rawParams = base.match(/[a-z]+:[a-z-\.@\d:"]+/gi) || []
+
+ for (const param of rawParams) {
+ base = base.replace(param, '')
+ }
+
+ base = base.trim()
+
+ const params = rawParams.reduce((params, param) => {
+ const [name, ...value] = param.split(/:/)
+ params[name] = value.join(':').replace(/"/g, '') // dates can contain additional colons
+ return params
+ }, {} as Params)
+ const literals = rawLiterals.map(l => String(l).trim())
+
+ return {
+ query: [base, literals.join(' ')].filter(Boolean).join(' '),
+ params,
+ }
+}
+
+export function makeSearchQuery(query: string, params: Params) {
+ return [
+ query,
+ Object.entries(params)
+ .map(([name, value]) => `${name}:${value}`)
+ .join(' '),
+ ]
+ .filter(Boolean)
+ .join(' ')
+}
diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx
index 2cdb4b7224..d9b680602a 100644
--- a/src/screens/Signup/StepInfo/index.tsx
+++ b/src/screens/Signup/StepInfo/index.tsx
@@ -6,8 +6,8 @@ import * as EmailValidator from 'email-validator'
import type tldts from 'tldts'
import {logEvent} from '#/lib/statsig/statsig'
+import {isEmailMaybeInvalid} from '#/lib/strings/email'
import {logger} from '#/logger'
-import {isEmailMaybeInvalid} from 'lib/strings/email'
import {ScreenTransition} from '#/screens/Login/ScreenTransition'
import {is13, is18, useSignupContext} from '#/screens/Signup/state'
import {Policies} from '#/screens/Signup/StepInfo/Policies'
@@ -59,6 +59,9 @@ export function StepInfo({
import('tldts/dist/index.cjs.min.js').then(tldts => {
tldtsRef.current = tldts
})
+ // This will get used in the avatar creator a few steps later, so lets preload it now
+ // @ts-expect-error - valid path
+ import('react-native-view-shot/src/index')
}, [])
const onNextPress = () => {
diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx
index 3209800328..e3da053c09 100644
--- a/src/screens/Signup/index.tsx
+++ b/src/screens/Signup/index.tsx
@@ -5,7 +5,6 @@ import {AppBskyGraphStarterpack} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {FEEDBACK_FORM_URL} from '#/lib/constants'
import {useServiceQuery} from '#/state/queries/service'
import {useStarterPackQuery} from '#/state/queries/starter-packs'
@@ -31,7 +30,6 @@ import {Text} from '#/components/Typography'
export function Signup({onPressBack}: {onPressBack: () => void}) {
const {_} = useLingui()
const t = useTheme()
- const {screen} = useAnalytics()
const [state, dispatch] = React.useReducer(reducer, initialState)
const {gtMobile} = useBreakpoints()
const submit = useSubmitSignup()
@@ -56,10 +54,6 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
refetch,
} = useServiceQuery(state.serviceUrl)
- React.useEffect(() => {
- screen('CreateAccount')
- }, [screen])
-
React.useEffect(() => {
if (isFetching) {
dispatch({type: 'setIsLoading', value: true})
diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts
index 65300a8ef1..b456a76d97 100644
--- a/src/state/cache/post-shadow.ts
+++ b/src/state/cache/post-shadow.ts
@@ -21,6 +21,7 @@ export interface PostShadow {
repostUri: string | undefined
isDeleted: boolean
embed: AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined
+ pinned: boolean
}
export const POST_TOMBSTONE = Symbol('PostTombstone')
@@ -113,6 +114,7 @@ function mergeShadow(
...(post.viewer || {}),
like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like,
repost: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost,
+ pinned: 'pinned' in shadow ? shadow.pinned : post.viewer?.pinned,
},
})
}
diff --git a/src/state/queries/actor-starter-packs.ts b/src/state/queries/actor-starter-packs.ts
index 9de80b07de..487bcdfd99 100644
--- a/src/state/queries/actor-starter-packs.ts
+++ b/src/state/queries/actor-starter-packs.ts
@@ -6,9 +6,9 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
-import {useAgent} from 'state/session'
+import {useAgent} from '#/state/session'
-const RQKEY_ROOT = 'actor-starter-packs'
+export const RQKEY_ROOT = 'actor-starter-packs'
export const RQKEY = (did?: string) => [RQKEY_ROOT, did]
export function useActorStarterPacksQuery({did}: {did?: string}) {
diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts
index 63a8079623..8166602c8a 100644
--- a/src/state/queries/nuxs/definitions.ts
+++ b/src/state/queries/nuxs/definitions.ts
@@ -3,23 +3,16 @@ import zod from 'zod'
import {BaseNux} from '#/state/queries/nuxs/types'
export enum Nux {
- TenMillionDialog = 'TenMillionDialog',
NeueTypography = 'NeueTypography',
}
export const nuxNames = new Set(Object.values(Nux))
-export type AppNux =
- | BaseNux<{
- id: Nux.TenMillionDialog
- data: undefined
- }>
- | BaseNux<{
- id: Nux.NeueTypography
- data: undefined
- }>
+export type AppNux = BaseNux<{
+ id: Nux.NeueTypography
+ data: undefined
+}>
export const NuxSchemas: Record | undefined> = {
- [Nux.TenMillionDialog]: undefined,
[Nux.NeueTypography]: undefined,
}
diff --git a/src/state/queries/pinned-post.ts b/src/state/queries/pinned-post.ts
new file mode 100644
index 0000000000..7e2c8ee798
--- /dev/null
+++ b/src/state/queries/pinned-post.ts
@@ -0,0 +1,87 @@
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
+
+import {logger} from '#/logger'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import * as Toast from '#/view/com/util/Toast'
+import {updatePostShadow} from '../cache/post-shadow'
+import {useAgent, useSession} from '../session'
+import {useProfileUpdateMutation} from './profile'
+
+export function usePinnedPostMutation() {
+ const {_} = useLingui()
+ const {currentAccount} = useSession()
+ const agent = useAgent()
+ const queryClient = useQueryClient()
+ const {mutateAsync: profileUpdateMutate} = useProfileUpdateMutation()
+
+ return useMutation({
+ mutationFn: async ({
+ postUri,
+ postCid,
+ action,
+ }: {
+ postUri: string
+ postCid: string
+ action: 'pin' | 'unpin'
+ }) => {
+ const pinCurrentPost = action === 'pin'
+ let prevPinnedPost: string | undefined
+ try {
+ updatePostShadow(queryClient, postUri, {pinned: pinCurrentPost})
+
+ // get the currently pinned post so we can optimistically remove the pin from it
+ if (!currentAccount) throw new Error('Not logged in')
+ const {data: profile} = await agent.getProfile({
+ actor: currentAccount.did,
+ })
+ prevPinnedPost = profile.pinnedPost?.uri
+ if (prevPinnedPost && prevPinnedPost !== postUri) {
+ updatePostShadow(queryClient, prevPinnedPost, {pinned: false})
+ }
+
+ await profileUpdateMutate({
+ profile,
+ updates: existing => {
+ existing.pinnedPost = pinCurrentPost
+ ? {uri: postUri, cid: postCid}
+ : undefined
+ return existing
+ },
+ checkCommitted: res =>
+ pinCurrentPost
+ ? res.data.pinnedPost?.uri === postUri
+ : !res.data.pinnedPost,
+ })
+
+ if (pinCurrentPost) {
+ Toast.show(_(msg`Post pinned`))
+ } else {
+ Toast.show(_(msg`Post unpinned`))
+ }
+
+ queryClient.invalidateQueries({
+ queryKey: FEED_RQKEY(
+ `author|${currentAccount.did}|posts_and_author_threads`,
+ ),
+ })
+ queryClient.invalidateQueries({
+ queryKey: FEED_RQKEY(
+ `author|${currentAccount.did}|posts_with_replies`,
+ ),
+ })
+ } catch (e: any) {
+ Toast.show(_(msg`Failed to pin post`))
+ logger.error('Failed to pin post', {message: String(e)})
+ // revert optimistic update
+ updatePostShadow(queryClient, postUri, {
+ pinned: !pinCurrentPost,
+ })
+ if (prevPinnedPost && prevPinnedPost !== postUri) {
+ updatePostShadow(queryClient, prevPinnedPost, {pinned: true})
+ }
+ }
+ },
+ })
+}
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 7daf441adb..1785eb445f 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -15,24 +15,25 @@ import {
useInfiniteQuery,
} from '@tanstack/react-query'
+import {AuthorFeedAPI} from '#/lib/api/feed/author'
+import {CustomFeedAPI} from '#/lib/api/feed/custom'
+import {FollowingFeedAPI} from '#/lib/api/feed/following'
import {HomeFeedAPI} from '#/lib/api/feed/home'
+import {LikesFeedAPI} from '#/lib/api/feed/likes'
+import {ListFeedAPI} from '#/lib/api/feed/list'
+import {MergeFeedAPI} from '#/lib/api/feed/merge'
+import {FeedAPI, ReasonFeedSource} from '#/lib/api/feed/types'
import {aggregateUserInterests} from '#/lib/api/feed/utils'
+import {FeedTuner, FeedTunerFn} from '#/lib/api/feed-manip'
import {DISCOVER_FEED_URI} from '#/lib/constants'
+import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
+import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {useAgent} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
-import {AuthorFeedAPI} from 'lib/api/feed/author'
-import {CustomFeedAPI} from 'lib/api/feed/custom'
-import {FollowingFeedAPI} from 'lib/api/feed/following'
-import {LikesFeedAPI} from 'lib/api/feed/likes'
-import {ListFeedAPI} from 'lib/api/feed/list'
-import {MergeFeedAPI} from 'lib/api/feed/merge'
-import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
-import {FeedTuner, FeedTunerFn} from 'lib/api/feed-manip'
-import {BSKY_FEED_OWNER_DIDS} from 'lib/constants'
import {KnownError} from '#/view/com/posts/FeedErrorMessage'
import {useFeedTuners} from '../preferences/feed-tuners'
import {useModerationOpts} from '../preferences/moderation-opts'
@@ -65,7 +66,7 @@ export interface FeedParams {
type RQPageParam = {cursor: string | undefined; api: FeedAPI} | undefined
-const RQKEY_ROOT = 'post-feed'
+export const RQKEY_ROOT = 'post-feed'
export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
return [RQKEY_ROOT, feedDesc, params || {}]
}
@@ -90,6 +91,7 @@ export interface FeedPostSlice {
feedContext: string | undefined
reason?:
| AppBskyFeedDefs.ReasonRepost
+ | AppBskyFeedDefs.ReasonPin
| ReasonFeedSource
| {[k: string]: unknown; $type: string}
}
@@ -109,13 +111,19 @@ export interface FeedPage {
fetchedAt: number
}
-const PAGE_SIZE = 30
+/**
+ * The minimum number of posts we want in a single "page" of results. Since we
+ * filter out unwanted content, we may fetch more than this number to ensure
+ * that we get _at least_ this number.
+ */
+const MIN_POSTS = 30
export function usePostFeedQuery(
feedDesc: FeedDescriptor,
params?: FeedParams,
opts?: {enabled?: boolean; ignoreFilterFor?: string},
) {
+ const gate = useGate()
const feedTuners = useFeedTuners(feedDesc)
const moderationOpts = useModerationOpts()
const {data: preferences} = usePreferencesQuery()
@@ -135,6 +143,13 @@ export function usePostFeedQuery(
} | null>(null)
const isDiscover = feedDesc.includes(DISCOVER_FEED_URI)
+ /**
+ * The number of posts to fetch in a single request. Because we filter
+ * unwanted content, we may over-fetch here to try and fill pages by
+ * `MIN_POSTS`.
+ */
+ const fetchLimit = gate('post_feed_lang_window') ? 100 : MIN_POSTS
+
// Make sure this doesn't invalidate unless really needed.
const selectArgs = React.useMemo(
() => ({
@@ -175,7 +190,7 @@ export function usePostFeedQuery(
}
try {
- const res = await api.fetch({cursor, limit: PAGE_SIZE})
+ const res = await api.fetch({cursor, limit: fetchLimit})
/*
* If this is a public view, we need to check if posts fail moderation.
@@ -373,13 +388,13 @@ export function usePostFeedQuery(
// Now track how many items we really want, and fetch more if needed.
if (isLoading || isRefetching) {
// During the initial fetch, we want to get an entire page's worth of items.
- wantedItemCount.current = PAGE_SIZE
+ wantedItemCount.current = MIN_POSTS
} else if (isFetchingNextPage) {
if (itemCount > wantedItemCount.current) {
// We have more items than wantedItemCount, so wantedItemCount must be out of date.
// Some other code must have called fetchNextPage(), for example, from onEndReached.
// Adjust the wantedItemCount to reflect that we want one more full page of items.
- wantedItemCount.current = itemCount + PAGE_SIZE
+ wantedItemCount.current = itemCount + MIN_POSTS
}
} else if (hasNextPage) {
// At this point we're not fetching anymore, so it's time to make a decision.
diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts
index 982d224aee..7023580bbe 100644
--- a/src/state/queries/post.ts
+++ b/src/state/queries/post.ts
@@ -2,7 +2,6 @@ import {useCallback} from 'react'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
-import {track} from '#/lib/analytics/analytics'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
import {updatePostShadow} from '#/state/cache/post-shadow'
@@ -193,9 +192,6 @@ function usePostLikeMutation(
})
return agent.like(uri, cid)
},
- onSuccess() {
- track('Post:Like')
- },
})
}
@@ -208,9 +204,6 @@ function usePostUnlikeMutation(
logEvent('post:unlike:sampled', {logContext})
return agent.deleteLike(likeUri)
},
- onSuccess() {
- track('Post:Unlike')
- },
})
}
@@ -285,9 +278,6 @@ function usePostRepostMutation(
logEvent('post:repost:sampled', {logContext})
return agent.repost(post.uri, post.cid)
},
- onSuccess() {
- track('Post:Repost')
- },
})
}
@@ -300,9 +290,6 @@ function usePostUnrepostMutation(
logEvent('post:unrepost:sampled', {logContext})
return agent.deleteRepost(repostUri)
},
- onSuccess() {
- track('Post:Unrepost')
- },
})
}
@@ -313,9 +300,8 @@ export function usePostDeleteMutation() {
mutationFn: async ({uri}) => {
await agent.deletePost(uri)
},
- onSuccess(data, variables) {
+ onSuccess(_, variables) {
updatePostShadow(queryClient, variables.uri, {isDeleted: true})
- track('Post:Delete')
},
})
}
diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts
index ab866d5e2a..3cb121a470 100644
--- a/src/state/queries/preferences/index.ts
+++ b/src/state/queries/preferences/index.ts
@@ -5,7 +5,6 @@ import {
} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
-import {track} from '#/lib/analytics/analytics'
import {PROD_DEFAULT_FEED} from '#/lib/constants'
import {replaceEqualDeep} from '#/lib/functions'
import {getAge} from '#/lib/strings/time'
@@ -218,7 +217,6 @@ export function useAddSavedFeedsMutation() {
>({
mutationFn: async savedFeeds => {
await agent.addSavedFeeds(savedFeeds)
- track('CustomFeed:Save')
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
@@ -234,7 +232,6 @@ export function useRemoveFeedMutation() {
return useMutation>({
mutationFn: async savedFeed => {
await agent.removeSavedFeeds([savedFeed.id])
- track('CustomFeed:Unsave')
// triggers a refetch
await queryClient.invalidateQueries({
queryKey: preferencesQueryKey,
diff --git a/src/state/queries/profile-feedgens.ts b/src/state/queries/profile-feedgens.ts
index b50a2a2890..79d9735c98 100644
--- a/src/state/queries/profile-feedgens.ts
+++ b/src/state/queries/profile-feedgens.ts
@@ -8,7 +8,7 @@ const PAGE_SIZE = 50
type RQPageParam = string | undefined
// TODO refactor invalidate on mutate?
-const RQKEY_ROOT = 'profile-feedgens'
+export const RQKEY_ROOT = 'profile-feedgens'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileFeedgensQuery(
diff --git a/src/state/queries/profile-lists.ts b/src/state/queries/profile-lists.ts
index 03c983ff80..5c9f9f0d6f 100644
--- a/src/state/queries/profile-lists.ts
+++ b/src/state/queries/profile-lists.ts
@@ -7,7 +7,7 @@ import {useModerationOpts} from '../preferences/moderation-opts'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
-const RQKEY_ROOT = 'profile-lists'
+export const RQKEY_ROOT = 'profile-lists'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) {
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 532b005cf4..3059d9efea 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -16,7 +16,6 @@ import {
useQueryClient,
} from '@tanstack/react-query'
-import {track} from '#/lib/analytics/analytics'
import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
@@ -160,6 +159,9 @@ export function useProfileUpdateMutation() {
} else {
existing.displayName = updates.displayName
existing.description = updates.description
+ if ('pinnedPost' in updates) {
+ existing.pinnedPost = updates.pinnedPost
+ }
}
if (newUserAvatarPromise) {
const res = await newUserAvatarPromise
@@ -316,9 +318,6 @@ function useProfileFollowMutation(
})
return await agent.follow(did)
},
- onSuccess(data, variables) {
- track('Profile:Follow', {username: variables.did})
- },
})
}
@@ -329,7 +328,6 @@ function useProfileUnfollowMutation(
return useMutation({
mutationFn: async ({followUri}) => {
logEvent('profile:unfollow:sampled', {logContext})
- track('Profile:Unfollow', {username: followUri})
return await agent.deleteFollow(followUri)
},
})
diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts
index 5ae8317047..07e16946e7 100644
--- a/src/state/queries/suggested-follows.ts
+++ b/src/state/queries/suggested-follows.ts
@@ -105,17 +105,16 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) {
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
const agent = useAgent()
- return useQuery({
+ return useQuery({
queryKey: suggestedFollowsByActorQueryKey(did),
queryFn: async () => {
const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({
actor: did,
})
- const data = res.data.isFallback ? {suggestions: []} : res.data
- data.suggestions = data.suggestions.filter(profile => {
- return !profile.viewer?.following
- })
- return data
+ const suggestions = res.data.isFallback
+ ? []
+ : res.data.suggestions.filter(profile => !profile.viewer?.following)
+ return {suggestions}
},
})
}
diff --git a/src/state/queries/video/video.ts b/src/state/queries/video/video.ts
index 32d02a63cb..0d77935da9 100644
--- a/src/state/queries/video/video.ts
+++ b/src/state/queries/video/video.ts
@@ -7,17 +7,17 @@ import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
import {AbortError} from '#/lib/async/cancelable'
import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants'
-import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
import {
ServerError,
UploadLimitError,
VideoTooLargeError,
-} from 'lib/media/video/errors'
-import {CompressedVideo} from 'lib/media/video/types'
-import {useCompressVideoMutation} from 'state/queries/video/compress-video'
-import {useVideoAgent} from 'state/queries/video/util'
-import {useUploadVideoMutation} from 'state/queries/video/video-upload'
+} from '#/lib/media/video/errors'
+import {CompressedVideo} from '#/lib/media/video/types'
+import {logger} from '#/logger'
+import {isWeb} from '#/platform/detection'
+import {useCompressVideoMutation} from '#/state/queries/video/compress-video'
+import {useVideoAgent} from '#/state/queries/video/util'
+import {useUploadVideoMutation} from '#/state/queries/video/video-upload'
type Status = 'idle' | 'compressing' | 'processing' | 'uploading' | 'done'
@@ -101,9 +101,11 @@ function reducer(queryClient: QueryClient) {
export function useUploadVideo({
setStatus,
+ initialVideoUri,
}: {
setStatus: (status: string) => void
onSuccess: () => void
+ initialVideoUri?: string
}) {
const {_} = useLingui()
const queryClient = useQueryClient()
@@ -237,21 +239,24 @@ export function useUploadVideo({
signal: state.abortController.signal,
})
- const selectVideo = (asset: ImagePickerAsset) => {
- // compression step on native converts to mp4, so no need to check there
- if (isWeb) {
- const mimeType = getMimeType(asset)
- if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
- throw new Error(_(msg`Unsupported video type: ${mimeType}`))
+ const selectVideo = React.useCallback(
+ (asset: ImagePickerAsset) => {
+ // compression step on native converts to mp4, so no need to check there
+ if (isWeb) {
+ const mimeType = getMimeType(asset)
+ if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
+ throw new Error(_(msg`Unsupported video type: ${mimeType}`))
+ }
}
- }
- dispatch({
- type: 'SetAsset',
- asset,
- })
- onSelectVideo(asset)
- }
+ dispatch({
+ type: 'SetAsset',
+ asset,
+ })
+ onSelectVideo(asset)
+ },
+ [_, onSelectVideo],
+ )
const clearVideo = () => {
dispatch({type: 'Reset'})
@@ -265,6 +270,13 @@ export function useUploadVideo({
})
}, [])
+ // Whenever we receive an initial video uri, we should immediately run compression if necessary
+ useEffect(() => {
+ if (initialVideoUri) {
+ selectVideo({uri: initialVideoUri} as ImagePickerAsset)
+ }
+ }, [initialVideoUri, selectVideo])
+
return {
state,
dispatch,
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index 21fe7f75b9..ab3352bf3a 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -1,7 +1,6 @@
import React from 'react'
import {AtpSessionEvent, BskyAgent} from '@atproto/api'
-import {track} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted'
@@ -70,7 +69,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
async params => {
addSessionDebugLog({type: 'method:start', method: 'createAccount'})
const signal = cancelPendingTask()
- track('Try Create Account')
logEvent('account:create:begin', {})
const {agent, account} = await createAgentAndCreateAccount(
params,
@@ -85,7 +83,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
newAgent: agent,
newAccount: account,
})
- track('Create Account')
logEvent('account:create:success', {})
addSessionDebugLog({type: 'method:end', method: 'createAccount', account})
},
@@ -109,7 +106,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
newAgent: agent,
newAccount: account,
})
- track('Sign In', {resumedSession: false})
logEvent('account:loggedIn', {logContext, withPassword: true})
addSessionDebugLog({type: 'method:end', method: 'login', account})
},
diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx
index 8e12386bd3..1c3aa6a81d 100644
--- a/src/state/shell/composer/index.tsx
+++ b/src/state/shell/composer/index.tsx
@@ -38,6 +38,7 @@ export interface ComposerOpts {
openEmojiPicker?: (pos: DOMRect | undefined) => void
text?: string
imageUris?: {uri: string; width: number; height: number; altText?: string}[]
+ videoUri?: string
}
type StateContext = ComposerOpts | undefined
diff --git a/src/state/shell/onboarding.tsx b/src/state/shell/onboarding.tsx
index d3a8fec466..9aad9953d4 100644
--- a/src/state/shell/onboarding.tsx
+++ b/src/state/shell/onboarding.tsx
@@ -1,6 +1,5 @@
import React from 'react'
-import {track} from '#/lib/analytics/analytics'
import * as persisted from '#/state/persisted'
export const OnboardingScreenSteps = {
@@ -55,17 +54,14 @@ function reducer(state: StateContext, action: Action): StateContext {
return compute({...state, step: nextStep})
}
case 'start': {
- track('Onboarding:Begin')
persisted.write('onboarding', {step: 'Welcome'})
return compute({...state, step: 'Welcome'})
}
case 'finish': {
- track('Onboarding:Complete')
persisted.write('onboarding', {step: 'Home'})
return compute({...state, step: 'Home'})
}
case 'skip': {
- track('Onboarding:Skipped')
persisted.write('onboarding', {step: 'Home'})
return compute({...state, step: 'Home'})
}
diff --git a/src/style.css b/src/style.css
index 980d92ef77..b0b01ba3eb 100644
--- a/src/style.css
+++ b/src/style.css
@@ -9,7 +9,7 @@
@font-face {
font-family: 'Inter-Regular';
src: local('Inter-Regular'),
- url(/assets/fonts/inter/Inter-Regular.otf) format('font/otf');
+ url(/assets/fonts/inter/Inter-Regular.otf) format('opentype');
font-weight: 400;
font-style: normal;
font-display: swap;
@@ -17,7 +17,7 @@
@font-face {
font-family: 'Inter-Italic';
src: local('Inter-Italic'),
- url(/assets/fonts/inter/Inter-Italic.otf) format('font/otf');
+ url(/assets/fonts/inter/Inter-Italic.otf) format('opentype');
font-weight: 400;
font-style: italic;
font-display: swap;
@@ -25,14 +25,14 @@
/*
@font-face {
font-family: "Inter-Medium";
- src: local("Inter-Medium"), url(/assets/fonts/inter/Inter-Medium.otf) format("font/otf");
+ src: local("Inter-Medium"), url(/assets/fonts/inter/Inter-Medium.otf) format("opentype");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter-MediumItalic";
- src: local("Inter-MediumItalic"), url(/assets/fonts/inter/Inter-MediumItalic.otf) format("font/otf");
+ src: local("Inter-MediumItalic"), url(/assets/fonts/inter/Inter-MediumItalic.otf) format("opentype");
font-weight: 500;
font-style: italic;
font-display: swap;
@@ -41,7 +41,7 @@
@font-face {
font-family: 'Inter-SemiBold';
src: local('Inter-SemiBold'),
- url(/assets/fonts/inter/Inter-SemiBold.otf) format('font/otf');
+ url(/assets/fonts/inter/Inter-SemiBold.otf) format('opentype');
font-weight: 600;
font-style: normal;
font-display: swap;
@@ -49,7 +49,7 @@
@font-face {
font-family: 'Inter-SemiBoldItalic';
src: local('Inter-SemiBoldItalic'),
- url(/assets/fonts/inter/Inter-SemiBoldItalic.otf) format('font/otf');
+ url(/assets/fonts/inter/Inter-SemiBoldItalic.otf) format('opentype');
font-weight: 600;
font-style: italic;
font-display: swap;
@@ -57,14 +57,14 @@
/*
@font-face {
font-family: "Inter-Bold";
- src: local("Inter-Bold"), url(/assets/fonts/inter/Inter-Bold.otf) format("font/otf");
+ src: local("Inter-Bold"), url(/assets/fonts/inter/Inter-Bold.otf) format("opentype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter-BoldItalic";
- src: local("Inter-BoldItalic"), url(/assets/fonts/inter/Inter-BoldItalic.otf) format("font/otf");
+ src: local("Inter-BoldItalic"), url(/assets/fonts/inter/Inter-BoldItalic.otf) format("opentype");
font-weight: 700;
font-style: italic;
font-display: swap;
@@ -73,7 +73,7 @@
@font-face {
font-family: 'Inter-ExtraBold';
src: local('Inter-ExtraBold'),
- url(/assets/fonts/inter/Inter-ExtraBold.otf) format('font/otf');
+ url(/assets/fonts/inter/Inter-ExtraBold.otf) format('opentype');
font-weight: 800;
font-style: normal;
font-display: swap;
@@ -81,7 +81,7 @@
@font-face {
font-family: 'Inter-ExtraBoldItalic';
src: local('Inter-ExtraBoldItalic'),
- url(/assets/fonts/inter/Inter-ExtraBoldItalic.otf) format('font/otf');
+ url(/assets/fonts/inter/Inter-ExtraBoldItalic.otf) format('opentype');
font-weight: 800;
font-style: italic;
font-display: swap;
@@ -89,14 +89,14 @@
/*
@font-face {
font-family: "Inter-Black";
- src: local("Inter-Black"), url(/assets/fonts/inter/Inter-Black.otf) format("font/otf");
+ src: local("Inter-Black"), url(/assets/fonts/inter/Inter-Black.otf) format("opentype");
font-weight: 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Inter-BlackItalic";
- src: local("Inter-BlackItalic"), url(/assets/fonts/inter/Inter-BlackItalic.otf) format("font/otf");
+ src: local("Inter-BlackItalic"), url(/assets/fonts/inter/Inter-BlackItalic.otf) format("opentype");
font-weight: 900;
font-style: italic;
font-display: swap;
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index cc57238053..5b9e3932f9 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -4,7 +4,6 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {usePalette} from '#/lib/hooks/usePalette'
import {logEvent} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles'
@@ -32,7 +31,6 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const {_} = useLingui()
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
- const {screen} = useAnalytics()
const {requestedAccountSwitchTo} = useLoggedOutView()
const [screenState, setScreenState] = React.useState(() => {
if (requestedAccountSwitchTo === 'new') {
@@ -48,9 +46,8 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const {clearRequestedAccount} = useLoggedOutViewControls()
React.useEffect(() => {
- screen('Login')
setMinimalShellMode(true)
- }, [screen, setMinimalShellMode])
+ }, [setMinimalShellMode])
const onPressDismiss = React.useCallback(() => {
if (onDismiss) {
diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
index a18f17612e..ae18f13905 100644
--- a/src/view/com/auth/SplashScreen.tsx
+++ b/src/view/com/auth/SplashScreen.tsx
@@ -39,21 +39,21 @@ export const SplashScreen = ({
What's up?
-
+
- Create a new account
+ Create account
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
index 1fd62e1d3f..dee0d6b70a 100644
--- a/src/view/com/auth/SplashScreen.web.tsx
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -85,21 +85,19 @@ export const SplashScreen = ({
+ style={[a.w_full, a.px_xl, a.gap_md, a.pb_2xl, {maxWidth: 320}]}>
- Create a new account
+ Create account
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 3b7cf13851..ade37af1b6 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -45,7 +45,6 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import * as apilib from '#/lib/api/index'
import {until} from '#/lib/async/until'
import {MAX_GRAPHEME_LENGTH} from '#/lib/constants'
@@ -137,6 +136,7 @@ export const ComposePost = ({
openEmojiPicker,
text: initText,
imageUris: initImageUris,
+ videoUri: initVideoUri,
cancelRef,
}: Props & {
cancelRef?: React.RefObject
@@ -146,7 +146,6 @@ export const ComposePost = ({
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
const {isModalActive} = useModals()
const {closeComposer} = useComposerControls()
- const {track} = useAnalytics()
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
const {_} = useLingui()
@@ -199,6 +198,7 @@ export const ComposePost = ({
onPressPublish(true)
}
},
+ initialVideoUri: initVideoUri,
})
const hasVideo = Boolean(videoUploadState.asset || videoUploadState.video)
@@ -308,7 +308,6 @@ export const ComposePost = ({
const onPhotoPasted = useCallback(
async (uri: string) => {
- track('Composer:PastedPhotos')
if (uri.startsWith('data:video/')) {
selectVideo({uri, type: 'video', height: 0, width: 0})
} else {
@@ -316,7 +315,7 @@ export const ComposePost = ({
onImageAdd([res])
}
},
- [track, selectVideo, onImageAdd],
+ [selectVideo, onImageAdd],
)
const isAltTextRequiredAndMissing = useMemo(() => {
@@ -444,10 +443,6 @@ export const ComposePost = ({
logContext: 'Composer',
})
}
- track('Create Post', {
- imageCount: images.length,
- })
- if (replyTo && replyTo.uri) track('Post:Reply')
}
if (postUri && !replyTo) {
emitPostCreated()
@@ -497,7 +492,6 @@ export const ComposePost = ({
setExtLink,
setLangPrefs,
threadgateAllowUISettings,
- track,
videoAltText,
videoUploadState.asset,
videoUploadState.pendingPublish,
diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx
index 74ca615070..7f4bb85f23 100644
--- a/src/view/com/composer/ComposerReplyTo.tsx
+++ b/src/view/com/composer/ComposerReplyTo.tsx
@@ -10,12 +10,12 @@ import {
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {sanitizeHandle} from 'lib/strings/handles'
-import {ComposerOptsPostRef} from 'state/shell/composer'
-import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed'
-import {Text} from 'view/com/util/text/Text'
-import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
+import {sanitizeDisplayName} from '#/lib/strings/display-names'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {ComposerOptsPostRef} from '#/state/shell/composer'
+import {QuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed'
+import {Text} from '#/view/com/util/text/Text'
+import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
@@ -91,7 +91,7 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
type={replyTo.author.associated?.labeler ? 'labeler' : 'user'}
/>
-
+
{sanitizeDisplayName(
replyTo.author.displayName || sanitizeHandle(replyTo.author.handle),
)}
@@ -101,7 +101,8 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
+ numberOfLines={!showFull ? 6 : undefined}
+ emoji>
{replyTo.text}
diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx
index 2183ca7902..79d59a92d1 100644
--- a/src/view/com/composer/photos/OpenCameraBtn.tsx
+++ b/src/view/com/composer/photos/OpenCameraBtn.tsx
@@ -3,7 +3,6 @@ import * as MediaLibrary from 'expo-media-library'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {POST_IMG_MAX} from '#/lib/constants'
import {useCameraPermission} from '#/lib/hooks/usePermissions'
import {openCamera} from '#/lib/media/picker'
@@ -20,7 +19,6 @@ type Props = {
}
export function OpenCameraBtn({disabled, onAdd}: Props) {
- const {track} = useAnalytics()
const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission()
const [mediaPermissionRes, requestMediaPermission] =
@@ -28,7 +26,6 @@ export function OpenCameraBtn({disabled, onAdd}: Props) {
const t = useTheme()
const onPressTakePicture = useCallback(async () => {
- track('Composer:CameraOpened')
try {
if (!(await requestCameraAccessIfNeeded())) {
return
@@ -58,7 +55,6 @@ export function OpenCameraBtn({disabled, onAdd}: Props) {
}
}, [
onAdd,
- track,
requestCameraAccessIfNeeded,
mediaPermissionRes,
requestMediaPermission,
diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx
index 95d2df022c..34ead3d9a9 100644
--- a/src/view/com/composer/photos/SelectPhotoBtn.tsx
+++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx
@@ -3,7 +3,6 @@ import React, {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {usePhotoLibraryPermission} from '#/lib/hooks/usePermissions'
import {openPicker} from '#/lib/media/picker'
import {isNative} from '#/platform/detection'
@@ -19,14 +18,11 @@ type Props = {
}
export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
- const {track} = useAnalytics()
const {_} = useLingui()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
const t = useTheme()
const onPressSelectPhotos = useCallback(async () => {
- track('Composer:GalleryOpened')
-
if (isNative && !(await requestPhotoAccessIfNeeded())) {
return
}
@@ -41,7 +37,7 @@ export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
)
onAdd(results)
- }, [track, requestPhotoAccessIfNeeded, size, onAdd])
+ }, [requestPhotoAccessIfNeeded, size, onAdd])
return (
{textDecorated}
diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
index 9c8f8f9162..3d2bcfa61e 100644
--- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
@@ -1,13 +1,17 @@
-import React, {useEffect, useRef} from 'react'
-import {Animated, TouchableOpacity, StyleSheet, View} from 'react-native'
-import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Text} from 'view/com/util/text/Text'
-import {UserAvatar} from 'view/com/util/UserAvatar'
-import {useGrapheme} from '../hooks/useGrapheme'
-import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
-import {Trans} from '@lingui/macro'
+import React, {useRef} from 'react'
+import {View} from 'react-native'
+import Animated, {FadeInDown, FadeOut} from 'react-native-reanimated'
import {AppBskyActorDefs} from '@atproto/api'
+import {Trans} from '@lingui/macro'
+
+import {PressableScale} from '#/lib/custom-animations/PressableScale'
+import {sanitizeDisplayName} from '#/lib/strings/display-names'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {atoms as a, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+import {useGrapheme} from '../hooks/useGrapheme'
export function Autocomplete({
prefix,
@@ -16,8 +20,8 @@ export function Autocomplete({
prefix: string
onSelect: (item: string) => void
}) {
- const pal = usePalette('default')
- const positionInterp = useAnimatedValue(0)
+ const t = useTheme()
+
const {getGraphemeString} = useGrapheme()
const isActive = !!prefix
const {data: suggestions, isFetching} = useActorAutocompleteQuery(prefix)
@@ -28,108 +32,85 @@ export function Autocomplete({
suggestionsRef.current = suggestions
}
- useEffect(() => {
- Animated.timing(positionInterp, {
- toValue: isActive ? 1 : 0,
- duration: 200,
- useNativeDriver: true,
- }).start()
- }, [positionInterp, isActive])
-
- const topAnimStyle = {
- transform: [
- {
- translateY: positionInterp.interpolate({
- inputRange: [0, 1],
- outputRange: [200, 0],
- }),
- },
- ],
- }
+ if (!isActive) return null
return (
-
- {isActive ? (
-
- {suggestionsRef.current?.length ? (
- suggestionsRef.current.slice(0, 5).map(item => {
- // Eventually use an average length
- const MAX_CHARS = 40
- const MAX_HANDLE_CHARS = 20
+
+ {suggestionsRef.current?.length ? (
+ suggestionsRef.current.slice(0, 5).map((item, index, arr) => {
+ // Eventually use an average length
+ const MAX_CHARS = 40
+ const MAX_HANDLE_CHARS = 20
- // Using this approach because styling is not respecting
- // bounding box wrapping (before converting to ellipsis)
- const {name: displayHandle, remainingCharacters} =
- getGraphemeString(item.handle, MAX_HANDLE_CHARS)
+ // Using this approach because styling is not respecting
+ // bounding box wrapping (before converting to ellipsis)
+ const {name: displayHandle, remainingCharacters} = getGraphemeString(
+ item.handle,
+ MAX_HANDLE_CHARS,
+ )
- const {name: displayName} = getGraphemeString(
- item.displayName ?? item.handle,
- MAX_CHARS -
- MAX_HANDLE_CHARS +
- (remainingCharacters > 0 ? remainingCharacters : 0),
- )
+ const {name: displayName} = getGraphemeString(
+ item.displayName || item.handle,
+ MAX_CHARS -
+ MAX_HANDLE_CHARS +
+ (remainingCharacters > 0 ? remainingCharacters : 0),
+ )
- return (
- onSelect(item.handle)}
- accessibilityLabel={`Select ${item.handle}`}
- accessibilityHint="">
-
-
-
- {displayName}
-
-
-
- @{displayHandle}
+ return (
+
+ onSelect(item.handle)}
+ accessibilityLabel={`Select ${item.handle}`}
+ accessibilityHint="">
+
+
+
+ {sanitizeDisplayName(displayName)}
-
- )
- })
- ) : (
-
- {isFetching ? (
- Loading...
- ) : (
- No result
- )}
-
- )}
-
- ) : null}
+
+
+ {sanitizeHandle(displayHandle, '@')}
+
+
+
+ )
+ })
+ ) : (
+
+ {isFetching ? Loading... : No result }
+
+ )}
)
}
-
-const styles = StyleSheet.create({
- container: {
- marginLeft: -50, // Composer avatar width
- top: 10,
- borderTopWidth: 1,
- },
- item: {
- borderBottomWidth: 1,
- paddingVertical: 12,
- display: 'flex',
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- gap: 6,
- },
- avatarAndHandle: {
- display: 'flex',
- flexDirection: 'row',
- gap: 6,
- alignItems: 'center',
- },
- noResults: {
- paddingVertical: 12,
- },
-})
diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
index c4ce9a232c..b0806180c4 100644
--- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx
+++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
@@ -7,7 +7,6 @@ import {useLingui} from '@lingui/react'
import {isNative} from '#/platform/detection'
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
-import {useAnalytics} from 'lib/analytics/analytics'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -30,13 +29,11 @@ export function ThreadgateBtn({
style?: StyleProp>
}) {
- const {track} = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const control = Dialog.useDialogControl()
const onPress = () => {
- track('Composer:ThreadgateOpened')
if (isNative && Keyboard.isVisible()) {
Keyboard.dismiss()
}
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index f1d03a7ca3..d61a814983 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -6,8 +6,11 @@ import {useLingui} from '@lingui/react'
import {NavigationProp, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
+import {ComposeIcon2} from '#/lib/icons'
import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
+import {AllNavigatorParams} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
+import {s} from '#/lib/styles'
import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
@@ -17,10 +20,6 @@ import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {ComposeIcon2} from 'lib/icons'
-import {AllNavigatorParams} from 'lib/routes/types'
-import {s} from 'lib/styles'
import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
import {Feed} from '../posts/Feed'
import {FAB} from '../util/fab/FAB'
@@ -54,7 +53,6 @@ export function FeedPage({
const {openComposer} = useComposerControls()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const setMinimalShellMode = useSetMinimalShellMode()
- const {screen, track} = useAnalytics()
const headerOffset = useHeaderOffset()
const feedFeedback = useFeedFeedback(feed, hasSession)
const scrollElRef = React.useRef(null)
@@ -89,14 +87,12 @@ export function FeedPage({
if (!isPageFocused) {
return
}
- screen('Feed')
return listenSoftReset(onSoftReset)
- }, [onSoftReset, screen, isPageFocused])
+ }, [onSoftReset, isPageFocused])
const onPressCompose = React.useCallback(() => {
- track('HomeScreen:PressCompose')
openComposer({})
- }, [openComposer, track])
+ }, [openComposer])
const onPressLoadLatest = React.useCallback(() => {
scrollToTop()
diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx
index 6f98cc49a4..693a8e361a 100644
--- a/src/view/com/feeds/ProfileFeedgens.tsx
+++ b/src/view/com/feeds/ProfileFeedgens.tsx
@@ -15,9 +15,9 @@ import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
+import {EmptyState} from '#/view/com/util/EmptyState'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
-import {EmptyState} from 'view/com/util/EmptyState'
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, ios, useTheme} from '#/alf'
import * as FeedCard from '#/components/FeedCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {List, ListRef} from '../util/List'
@@ -191,6 +191,7 @@ export const ProfileFeedgens = React.forwardRef<
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
+ progressViewOffset={ios(0)}
contentContainerStyle={isNative && {paddingBottom: headerOffset + 100}}
indicatorStyle={t.name === 'light' ? 'black' : 'white'}
removeClippedSubviews={true}
diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx
index 9bfa82cd22..7049306eba 100644
--- a/src/view/com/home/HomeHeaderLayout.web.tsx
+++ b/src/view/com/home/HomeHeaderLayout.web.tsx
@@ -1,25 +1,16 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
-import Animated, {
- useAnimatedStyle,
- useReducedMotion,
- useSharedValue,
- withDelay,
- withRepeat,
- withSequence,
- withSpring,
- withTiming,
-} from 'react-native-reanimated'
+import Animated from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {useKawaiiMode} from '#/state/preferences/kawaii'
import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout'
-import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-// import {Logo} from '#/view/icons/Logo'
+import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
-import {Icon, Trigger} from '#/components/dialogs/nuxs/TenMillion/Trigger'
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import {Link} from '#/components/Link'
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
@@ -48,43 +39,7 @@ function HomeHeaderLayoutDesktopAndTablet({
const {headerHeight} = useShellLayout()
const {hasSession} = useSession()
const {_} = useLingui()
-
- // TEMPORARY - REMOVE AFTER MILLY
- // This will just cause the icon to shake a bit when the user first opens the app, drawing attention to the celebration
- // 🎉
- const rotate = useSharedValue(0)
- const reducedMotion = useReducedMotion()
-
- // Run this a single time on app mount.
- React.useEffect(() => {
- if (reducedMotion) return
-
- // Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once.
- rotate.value = withDelay(
- 1000,
- withRepeat(
- withSequence(
- withTiming(10, {duration: 100}),
- withSpring(0, {
- mass: 1,
- damping: 1,
- stiffness: 200,
- overshootClamping: false,
- }),
- ),
- 2,
- false,
- ),
- )
- }, [rotate, reducedMotion])
-
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [
- {
- rotateZ: `${rotate.value}deg`,
- },
- ],
- }))
+ const kawaii = useKawaiiMode()
return (
<>
@@ -101,30 +56,21 @@ function HomeHeaderLayoutDesktopAndTablet({
t.atoms.bg,
t.atoms.border_contrast_low,
styles.bar,
+ kawaii && {paddingTop: 22, paddingBottom: 16},
]}>
-
-
- {ctx => (
-
- )}
-
- {/* */}
-
+
+
{
- if (reducedMotion) return
-
- // Waits 1500ms, then rotates 10 degrees with a spring animation. Repeats once.
- rotate.value = withDelay(
- 1000,
- withRepeat(
- withSequence(
- withTiming(10, {duration: 100}),
- withSpring(0, {
- mass: 1,
- damping: 1,
- stiffness: 200,
- overshootClamping: false,
- }),
- ),
- 2,
- false,
- ),
- )
- }, [rotate, reducedMotion])
-
- const animatedStyle = useAnimatedStyle(() => ({
- transform: [
- {
- rotateZ: `${rotate.value}deg`,
- },
- ],
- }))
-
return (
-
-
- {ctx => (
-
- )}
-
- {/* */}
-
+
+
+
{
- track('Lists:onRefresh')
setIsRefreshing(true)
try {
await refetch()
@@ -106,17 +104,16 @@ export function ListMembers({
logger.error('Failed to refresh lists', {message: err})
}
setIsRefreshing(false)
- }, [refetch, track, setIsRefreshing])
+ }, [refetch, setIsRefreshing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
- track('Lists:onEndReached')
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more lists', {message: err})
}
- }, [isFetching, hasNextPage, isError, fetchNextPage, track])
+ }, [isFetching, hasNextPage, isError, fetchNextPage])
const onPressRetryLoadMore = React.useCallback(() => {
fetchNextPage()
diff --git a/src/view/com/lists/MyLists.tsx b/src/view/com/lists/MyLists.tsx
index b56fa6c75f..363dd100dd 100644
--- a/src/view/com/lists/MyLists.tsx
+++ b/src/view/com/lists/MyLists.tsx
@@ -11,15 +11,14 @@ import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {usePalette} from '#/lib/hooks/usePalette'
import {cleanError} from '#/lib/strings/errors'
+import {s} from '#/lib/styles'
import {logger} from '#/logger'
+import {isWeb} from '#/platform/detection'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {s} from 'lib/styles'
-import {isWeb} from 'platform/detection'
-import {useModerationOpts} from 'state/preferences/moderation-opts'
-import {EmptyState} from 'view/com/util/EmptyState'
+import {EmptyState} from '#/view/com/util/EmptyState'
import {atoms as a, useTheme} from '#/alf'
import * as ListCard from '#/components/ListCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
@@ -44,7 +43,6 @@ export function MyLists({
}) {
const pal = usePalette('default')
const t = useTheme()
- const {track} = useAnalytics()
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const [isPTRing, setIsPTRing] = React.useState(false)
@@ -71,7 +69,6 @@ export function MyLists({
// =
const onRefresh = React.useCallback(async () => {
- track('Lists:onRefresh')
setIsPTRing(true)
try {
await refetch()
@@ -79,7 +76,7 @@ export function MyLists({
logger.error('Failed to refresh lists', {message: err})
}
setIsPTRing(false)
- }, [refetch, track, setIsPTRing])
+ }, [refetch, setIsPTRing])
// rendering
// =
diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx
index f633774c7a..27b7f94df8 100644
--- a/src/view/com/lists/ProfileLists.tsx
+++ b/src/view/com/lists/ProfileLists.tsx
@@ -14,10 +14,9 @@ import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
-import {useAnalytics} from 'lib/analytics/analytics'
+import {EmptyState} from '#/view/com/util/EmptyState'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
-import {EmptyState} from 'view/com/util/EmptyState'
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, ios, useTheme} from '#/alf'
import * as ListCard from '#/components/ListCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {List, ListRef} from '../util/List'
@@ -48,7 +47,6 @@ export const ProfileLists = React.forwardRef(
ref,
) {
const t = useTheme()
- const {track} = useAnalytics()
const {_} = useLingui()
const [isPTRing, setIsPTRing] = React.useState(false)
const opts = React.useMemo(() => ({enabled}), [enabled])
@@ -102,7 +100,6 @@ export const ProfileLists = React.forwardRef(
}))
const onRefresh = React.useCallback(async () => {
- track('Lists:onRefresh')
setIsPTRing(true)
try {
await refetch()
@@ -110,18 +107,16 @@ export const ProfileLists = React.forwardRef(
logger.error('Failed to refresh lists', {message: err})
}
setIsPTRing(false)
- }, [refetch, track, setIsPTRing])
+ }, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
-
- track('Lists:onEndReached')
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more lists', {message: err})
}
- }, [isFetching, hasNextPage, isError, fetchNextPage, track])
+ }, [isFetching, hasNextPage, isError, fetchNextPage])
const onPressRetryLoadMore = React.useCallback(() => {
fetchNextPage()
@@ -192,6 +187,7 @@ export const ProfileLists = React.forwardRef(
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
+ progressViewOffset={ios(0)}
contentContainerStyle={
isNative && {paddingBottom: headerOffset + 100}
}
diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx
index 54750acf2e..2181a94aaf 100644
--- a/src/view/com/modals/ChangeHandle.tsx
+++ b/src/view/com/modals/ChangeHandle.tsx
@@ -11,17 +11,16 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {cleanError} from '#/lib/strings/errors'
+import {createFullHandle, makeValidHandle} from '#/lib/strings/handles'
+import {s} from '#/lib/styles'
+import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
import {useServiceQuery} from '#/state/queries/service'
import {SessionAccount, useAgent, useSession} from '#/state/session'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {cleanError} from 'lib/strings/errors'
-import {createFullHandle, makeValidHandle} from 'lib/strings/handles'
-import {s} from 'lib/styles'
-import {useTheme} from 'lib/ThemeContext'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {SelectableBtn} from '../util/forms/SelectableBtn'
@@ -67,7 +66,6 @@ export function Inner({
}) {
const {_} = useLingui()
const pal = usePalette('default')
- const {track} = useAnalytics()
const {closeModal} = useModalControls()
const {mutateAsync: updateHandle, isPending: isUpdateHandlePending} =
useUpdateHandleMutation()
@@ -91,10 +89,7 @@ export function Inner({
setHandle('')
setCanSave(false)
setCustom(!isCustom)
- track(
- isCustom ? 'EditHandle:ViewCustomForm' : 'EditHandle:ViewProvidedForm',
- )
- }, [setCustom, isCustom, track])
+ }, [setCustom, isCustom])
const onPressSave = React.useCallback(async () => {
if (!userDomain) {
logger.error(`ChangeHandle: userDomain is undefined`, {
@@ -105,7 +100,6 @@ export function Inner({
}
try {
- track('EditHandle:SetNewHandle')
const newHandle = isCustom ? handle : createFullHandle(handle, userDomain)
logger.debug(`Updating handle to ${newHandle}`)
await updateHandle({
@@ -125,7 +119,6 @@ export function Inner({
userDomain,
isCustom,
onChanged,
- track,
closeModal,
updateHandle,
serviceInfo,
diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx
index 7717f597dd..8f5487733a 100644
--- a/src/view/com/modals/CreateOrEditList.tsx
+++ b/src/view/com/modals/CreateOrEditList.tsx
@@ -14,7 +14,6 @@ import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {compressIfNeeded} from '#/lib/media/manip'
@@ -54,7 +53,6 @@ export function Component({
const [error, setError] = useState('')
const pal = usePalette('default')
const theme = useTheme()
- const {track} = useAnalytics()
const {_} = useLingui()
const listCreateMutation = useListCreateMutation()
const listMetadataMutation = useListMetadataMutation()
@@ -120,7 +118,6 @@ export function Component({
setAvatar(undefined)
return
}
- track('CreateList:AvatarSelected')
try {
const finalImg = await compressIfNeeded(img, 1000000)
setNewAvatar(finalImg)
@@ -129,15 +126,10 @@ export function Component({
setError(cleanError(e))
}
},
- [track, setNewAvatar, setAvatar, setError],
+ [setNewAvatar, setAvatar, setError],
)
const onPressSave = useCallback(async () => {
- if (isCurateList) {
- track('CreateList:SaveCurateList')
- } else {
- track('CreateList:SaveModList')
- }
const nameTrimmed = name.trim()
if (!nameTrimmed) {
setError(_(msg`Name is required`))
@@ -200,7 +192,6 @@ export function Component({
}
setProcessing(false)
}, [
- track,
setProcessing,
setError,
error,
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index beea3ca1a8..1e94f483ec 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -15,7 +15,6 @@ import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {MAX_DESCRIPTION, MAX_DISPLAY_NAME} from '#/lib/constants'
import {usePalette} from '#/lib/hooks/usePalette'
import {compressIfNeeded} from '#/lib/media/manip'
@@ -27,11 +26,11 @@ import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useProfileUpdateMutation} from '#/state/queries/profile'
+import {Text} from '#/view/com/util/text/Text'
+import * as Toast from '#/view/com/util/Toast'
+import {EditableUserAvatar} from '#/view/com/util/UserAvatar'
+import {UserBanner} from '#/view/com/util/UserBanner'
import {ErrorMessage} from '../util/error/ErrorMessage'
-import {Text} from '../util/text/Text'
-import * as Toast from '../util/Toast'
-import {EditableUserAvatar} from '../util/UserAvatar'
-import {UserBanner} from '../util/UserBanner'
const AnimatedTouchableOpacity =
Animated.createAnimatedComponent(TouchableOpacity)
@@ -47,7 +46,6 @@ export function Component({
}) {
const pal = usePalette('default')
const theme = useTheme()
- const {track} = useAnalytics()
const {_} = useLingui()
const {closeModal} = useModalControls()
const updateMutation = useProfileUpdateMutation()
@@ -81,7 +79,6 @@ export function Component({
setUserAvatar(null)
return
}
- track('EditProfile:AvatarSelected')
try {
const finalImg = await compressIfNeeded(img, 1000000)
setNewUserAvatar(finalImg)
@@ -90,7 +87,7 @@ export function Component({
setImageError(cleanError(e))
}
},
- [track, setNewUserAvatar, setUserAvatar, setImageError],
+ [setNewUserAvatar, setUserAvatar, setImageError],
)
const onSelectNewBanner = useCallback(
@@ -101,7 +98,6 @@ export function Component({
setUserBanner(null)
return
}
- track('EditProfile:BannerSelected')
try {
const finalImg = await compressIfNeeded(img, 1000000)
setNewUserBanner(finalImg)
@@ -110,11 +106,10 @@ export function Component({
setImageError(cleanError(e))
}
},
- [track, setNewUserBanner, setUserBanner, setImageError],
+ [setNewUserBanner, setUserBanner, setImageError],
)
const onPressSave = useCallback(async () => {
- track('EditProfile:Save')
setImageError('')
try {
await updateMutation.mutateAsync({
@@ -133,7 +128,6 @@ export function Component({
logger.error('Failed to update user profile', {message: String(e)})
}
}, [
- track,
updateMutation,
profile,
onUpdate,
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 5fbaaa1550..3c1f51249f 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -625,7 +625,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
},
layoutIcon: {
- width: 70,
+ width: 60,
alignItems: 'flex-end',
paddingTop: 2,
},
diff --git a/src/view/com/pager/PagerHeaderContext.tsx b/src/view/com/pager/PagerHeaderContext.tsx
new file mode 100644
index 0000000000..fd4cc74632
--- /dev/null
+++ b/src/view/com/pager/PagerHeaderContext.tsx
@@ -0,0 +1,41 @@
+import React, {useContext} from 'react'
+import {SharedValue} from 'react-native-reanimated'
+
+import {isIOS} from '#/platform/detection'
+
+export const PagerHeaderContext =
+ React.createContext | null>(null)
+
+/**
+ * Passes the scrollY value to the pager header's banner, so it can grow on
+ * overscroll on iOS. Not necessary to use this context provider on other platforms.
+ *
+ * @platform ios
+ */
+export function PagerHeaderProvider({
+ scrollY,
+ children,
+}: {
+ scrollY: SharedValue
+ children: React.ReactNode
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function usePagerHeaderContext() {
+ const scrollY = useContext(PagerHeaderContext)
+ if (isIOS) {
+ if (!scrollY) {
+ throw new Error(
+ 'usePagerHeaderContext must be used within a HeaderProvider',
+ )
+ }
+ return {scrollY}
+ } else {
+ return null
+ }
+}
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index 7b1d8b78f4..6d601c2899 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -19,9 +19,10 @@ import Animated, {
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {ScrollProvider} from '#/lib/ScrollContext'
-import {isIOS} from 'platform/detection'
-import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
+import {isIOS} from '#/platform/detection'
+import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
import {ListMethods} from '../util/List'
+import {PagerHeaderProvider} from './PagerHeaderContext'
import {TabBar} from './TabBar'
export interface PagerWithHeaderChildParams {
@@ -41,6 +42,7 @@ export interface PagerWithHeaderProps {
initialPage?: number
onPageSelected?: (index: number) => void
onCurrentPageSelected?: (index: number) => void
+ allowHeaderOverScroll?: boolean
}
export const PagerWithHeader = React.forwardRef(
function PageWithHeaderImpl(
@@ -53,6 +55,7 @@ export const PagerWithHeader = React.forwardRef(
initialPage,
onPageSelected,
onCurrentPageSelected,
+ allowHeaderOverScroll,
}: PagerWithHeaderProps,
ref,
) {
@@ -80,19 +83,22 @@ export const PagerWithHeader = React.forwardRef(
const renderTabBar = React.useCallback(
(props: RenderTabBarFnProps) => {
return (
-
+
+
+
)
},
[
@@ -106,6 +112,7 @@ export const PagerWithHeader = React.forwardRef(
onHeaderOnlyLayout,
scrollY,
testID,
+ allowHeaderOverScroll,
],
)
@@ -154,10 +161,17 @@ export const PagerWithHeader = React.forwardRef(
(e: NativeScrollEvent) => {
'worklet'
const nextScrollY = e.contentOffset.y
- scrollY.value = nextScrollY
- runOnJS(queueThrottledOnScroll)()
+ // HACK: onScroll is reporting some strange values on load (negative header height).
+ // Highly improbable that you'd be overscrolled by over 400px -
+ // in fact, I actually can't do it, so let's just ignore those. -sfn
+ const isPossiblyInvalid =
+ headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
+ if (!isPossiblyInvalid) {
+ scrollY.value = nextScrollY
+ runOnJS(queueThrottledOnScroll)()
+ }
},
- [scrollY, queueThrottledOnScroll],
+ [scrollY, queueThrottledOnScroll, headerHeight],
)
const onPageSelectedInner = React.useCallback(
@@ -216,6 +230,7 @@ let PagerTabBar = ({
onTabBarLayout,
onCurrentPageSelected,
onSelect,
+ allowHeaderOverScroll,
}: {
currentPage: number
headerOnlyHeight: number
@@ -228,14 +243,20 @@ let PagerTabBar = ({
onTabBarLayout: (e: LayoutChangeEvent) => void
onCurrentPageSelected?: (index: number) => void
onSelect?: (index: number) => void
+ allowHeaderOverScroll?: boolean
}): React.ReactNode => {
- const headerTransform = useAnimatedStyle(() => ({
- transform: [
- {
- translateY: Math.min(Math.min(scrollY.value, headerOnlyHeight) * -1, 0),
- },
- ],
- }))
+ const headerTransform = useAnimatedStyle(() => {
+ const translateY = Math.min(scrollY.value, headerOnlyHeight) * -1
+ return {
+ transform: [
+ {
+ translateY: allowHeaderOverScroll
+ ? translateY
+ : Math.min(translateY, 0),
+ },
+ ],
+ }
+ })
const headerRef = React.useRef(null)
return (
{
+ playHaptics('Light')
+ setTimeout(
+ () => {
+ onPressCompose()
+ },
+ isHapticsDisabled ? 0 : 75,
+ )
+ }
+
return (
- onPressCompose()}
+
-
-
+
- Write your reply
-
-
+
+
+ Write your reply
+
+
+
)
}
-
-const styles = StyleSheet.create({
- prompt: {
- paddingHorizontal: 16,
- paddingTop: 10,
- paddingBottom: 10,
- flexDirection: 'row',
- alignItems: 'center',
- borderTopWidth: StyleSheet.hairlineWidth,
- },
- labelMobile: {
- paddingLeft: 12,
- },
- labelDesktopWeb: {
- paddingLeft: 12,
- },
-})
diff --git a/src/view/com/post-thread/PostThreadFollowBtn.tsx b/src/view/com/post-thread/PostThreadFollowBtn.tsx
index 914ee3b667..b75731f6f3 100644
--- a/src/view/com/post-thread/PostThreadFollowBtn.tsx
+++ b/src/view/com/post-thread/PostThreadFollowBtn.tsx
@@ -6,19 +6,18 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {s} from '#/lib/styles'
import {logger} from '#/logger'
-import {track} from 'lib/analytics/analytics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {s} from 'lib/styles'
-import {Shadow, useProfileShadow} from 'state/cache/profile-shadow'
+import {Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
import {
useProfileFollowMutationQueue,
useProfileQuery,
-} from 'state/queries/profile'
-import {useRequireAuth} from 'state/session'
-import {Text} from 'view/com/util/text/Text'
-import * as Toast from 'view/com/util/Toast'
+} from '#/state/queries/profile'
+import {useRequireAuth} from '#/state/session'
+import {Text} from '#/view/com/util/text/Text'
+import * as Toast from '#/view/com/util/Toast'
export function PostThreadFollowBtn({did}: {did: string}) {
const {data: profile, isLoading} = useProfileQuery({did})
@@ -89,7 +88,6 @@ function PostThreadFollowBtnLoaded({
if (!isFollowing) {
requireAuth(async () => {
try {
- track('ProfileHeader:FollowButtonClicked')
await queueFollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
@@ -101,7 +99,6 @@ function PostThreadFollowBtnLoaded({
} else {
requireAuth(async () => {
try {
- track('ProfileHeader:UnfollowButtonClicked')
await queueUnfollow()
} catch (e: any) {
if (e?.name !== 'AbortError') {
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 3fb2309b96..ead9df1161 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -558,18 +558,14 @@ let PostThreadItemLoaded = ({
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 9033fb96f7..ec730a5e16 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -163,7 +163,7 @@ function PostInner({
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index d712605d6e..905c1e0e02 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -15,7 +15,9 @@ import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {DISCOVER_FEED_URI, KNOWN_SHUTDOWN_FEEDS} from '#/lib/constants'
+import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {logEvent, useGate} from '#/lib/statsig/statsig'
+import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events'
@@ -30,9 +32,6 @@ import {
usePostFeedQuery,
} from '#/state/queries/post-feed'
import {useSession} from '#/state/session'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
-import {useTheme} from 'lib/ThemeContext'
import {
ProgressGuide,
SuggestedFeeds,
@@ -167,6 +166,7 @@ let Feed = ({
renderEndOfFeed,
testID,
headerOffset = 0,
+ progressViewOffset,
desktopFixedHeightOffset,
ListHeaderComponent,
extraData,
@@ -187,6 +187,7 @@ let Feed = ({
renderEndOfFeed?: () => JSX.Element
testID?: string
headerOffset?: number
+ progressViewOffset?: number
desktopFixedHeightOffset?: number
ListHeaderComponent?: () => JSX.Element
extraData?: any
@@ -194,7 +195,6 @@ let Feed = ({
initialNumToRender?: number
}): React.ReactNode => {
const theme = useTheme()
- const {track} = useAnalytics()
const {_} = useLingui()
const queryClient = useQueryClient()
const {currentAccount, hasSession} = useSession()
@@ -403,7 +403,6 @@ let Feed = ({
// =
const onRefresh = React.useCallback(async () => {
- track('Feed:onRefresh')
logEvent('feed:refresh:sampled', {
feedType: feedType,
feedUrl: feed,
@@ -417,7 +416,7 @@ let Feed = ({
logger.error('Failed to refresh posts feed', {message: err})
}
setIsPTRing(false)
- }, [refetch, track, setIsPTRing, onHasNew, feed, feedType])
+ }, [refetch, setIsPTRing, onHasNew, feed, feedType])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
@@ -427,7 +426,6 @@ let Feed = ({
feedUrl: feed,
itemCount: feedItems.length,
})
- track('Feed:onEndReached')
try {
await fetchNextPage()
} catch (err) {
@@ -438,7 +436,6 @@ let Feed = ({
hasNextPage,
isError,
fetchNextPage,
- track,
feed,
feedType,
feedItems.length,
@@ -548,6 +545,7 @@ let Feed = ({
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
+ progressViewOffset={progressViewOffset}
contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5,
}}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index b1509b2719..28b8f4ceb2 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -38,7 +38,8 @@ import {PostMeta} from '#/view/com/util/PostMeta'
import {Text} from '#/view/com/util/text/Text'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a} from '#/alf'
-import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
+import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
+import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
import {ContentHider} from '#/components/moderation/ContentHider'
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
import {PostAlerts} from '#/components/moderation/PostAlerts'
@@ -52,6 +53,7 @@ interface FeedItemProps {
record: AppBskyFeedPost.Record
reason:
| AppBskyFeedDefs.ReasonRepost
+ | AppBskyFeedDefs.ReasonPin
| ReasonFeedSource
| {[k: string]: unknown; $type: string}
| undefined
@@ -245,7 +247,7 @@ let FeedItemInner = ({
onBeforePress={onBeforePress}
dataSet={{feedContext}}>
-
+
{isThreadChild && (
-
+ ) : AppBskyFeedDefs.isReasonPin(reason) ? (
+
+
+
+ Pinned
+
+
) : null}
@@ -345,7 +362,7 @@ let FeedItemInner = ({
{
- track('ProfileHeader:ShareButtonClicked')
shareUrl(toShareUrl(makeProfileLink(profile)))
- }, [track, profile])
+ }, [profile])
const onPressAddRemoveLists = React.useCallback(() => {
- track('ProfileHeader:AddToListsButtonClicked')
openModal({
name: 'user-add-remove-lists',
subject: profile.did,
@@ -97,11 +93,10 @@ let ProfileMenu = ({
onAdd: invalidateProfileQuery,
onRemove: invalidateProfileQuery,
})
- }, [track, profile, openModal, invalidateProfileQuery])
+ }, [profile, openModal, invalidateProfileQuery])
const onPressMuteAccount = React.useCallback(async () => {
if (profile.viewer?.muted) {
- track('ProfileHeader:UnmuteAccountButtonClicked')
try {
await queueUnmute()
Toast.show(_(msg`Account unmuted`))
@@ -112,7 +107,6 @@ let ProfileMenu = ({
}
}
} else {
- track('ProfileHeader:MuteAccountButtonClicked')
try {
await queueMute()
Toast.show(_(msg`Account muted`))
@@ -123,11 +117,10 @@ let ProfileMenu = ({
}
}
}
- }, [profile.viewer?.muted, track, queueUnmute, _, queueMute])
+ }, [profile.viewer?.muted, queueUnmute, _, queueMute])
const blockAccount = React.useCallback(async () => {
if (profile.viewer?.blocking) {
- track('ProfileHeader:UnblockAccountButtonClicked')
try {
await queueUnblock()
Toast.show(_(msg`Account unblocked`))
@@ -138,7 +131,6 @@ let ProfileMenu = ({
}
}
} else {
- track('ProfileHeader:BlockAccountButtonClicked')
try {
await queueBlock()
Toast.show(_(msg`Account blocked`))
@@ -149,10 +141,9 @@ let ProfileMenu = ({
}
}
}
- }, [profile.viewer?.blocking, track, _, queueUnblock, queueBlock])
+ }, [profile.viewer?.blocking, _, queueUnblock, queueBlock])
const onPressFollowAccount = React.useCallback(async () => {
- track('ProfileHeader:FollowButtonClicked')
try {
await queueFollow()
Toast.show(_(msg`Account followed`))
@@ -162,10 +153,9 @@ let ProfileMenu = ({
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
- }, [_, queueFollow, track])
+ }, [_, queueFollow])
const onPressUnfollowAccount = React.useCallback(async () => {
- track('ProfileHeader:UnfollowButtonClicked')
try {
await queueUnfollow()
Toast.show(_(msg`Account unfollowed`))
@@ -175,12 +165,11 @@ let ProfileMenu = ({
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
}
}
- }, [_, queueUnfollow, track])
+ }, [_, queueUnfollow])
const onPressReportAccount = React.useCallback(() => {
- track('ProfileHeader:ReportAccountButtonClicked')
reportDialogControl.open()
- }, [track, reportDialogControl])
+ }, [reportDialogControl])
return (
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index f9aeae1a86..53b0547d45 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -4,11 +4,11 @@ import {runOnJS, useSharedValue} from 'react-native-reanimated'
import {updateActiveVideoViewAsync} from '@haileyok/bluesky-video'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
-import {usePalette} from '#/lib/hooks/usePalette'
+import {useDedupe} from '#/lib/hooks/useDedupe'
import {useScrollHandlers} from '#/lib/ScrollContext'
-import {useDedupe} from 'lib/hooks/useDedupe'
-import {addStyle} from 'lib/styles'
-import {isIOS} from 'platform/detection'
+import {addStyle} from '#/lib/styles'
+import {isIOS} from '#/platform/detection'
+import {useTheme} from '#/alf'
import {FlatList_INTERNAL} from './Views'
export type ListMethods = FlatList_INTERNAL
@@ -44,12 +44,13 @@ function ListImpl(
onItemSeen,
headerOffset,
style,
+ progressViewOffset,
...props
}: ListProps,
ref: React.Ref,
) {
const isScrolledDown = useSharedValue(false)
- const pal = usePalette('default')
+ const t = useTheme()
const dedupe = useDedupe(400)
function handleScrolledDownChange(didScrollDown: boolean) {
@@ -120,9 +121,9 @@ function ListImpl(
)
}
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index 6e75e88ca6..6620eb8e28 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -7,9 +7,9 @@ import {
ViewStyle,
} from 'react-native'
-import {usePalette} from 'lib/hooks/usePalette'
-import {s} from 'lib/styles'
-import {useTheme} from 'lib/ThemeContext'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {s} from '#/lib/styles'
+import {useTheme} from '#/lib/ThemeContext'
import {atoms as a, useTheme as useTheme_NEW} from '#/alf'
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
import {
@@ -53,8 +53,8 @@ export function PostLoadingPlaceholder({
return (
onOpenAuthor?: () => void
style?: StyleProp
}
let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
- const {i18n} = useLingui()
+ const t = useTheme()
+ const {i18n, _} = useLingui()
- const pal = usePalette('default')
const displayName = opts.author.displayName || opts.author.handle
const handle = opts.author.handle
const profileLink = makeProfileLink(opts.author)
@@ -53,9 +50,18 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
}, [queryClient, opts.author])
return (
-
+
{opts.showAvatar && (
-
+
{
)}
-
-
+
- {forceLTR(
- sanitizeDisplayName(
- displayName,
- opts.moderation?.ui('displayName'),
- ),
- )}
-
- }
- href={profileLink}
- onBeforePress={onBeforePressAuthor}
- />
-
+
+ {forceLTR(
+ sanitizeDisplayName(
+ displayName,
+ opts.moderation?.ui('displayName'),
+ ),
+ )}
+
+
+
- {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
-
- }
- href={profileLink}
- onBeforePress={onBeforePressAuthor}
- anchorNoUnderline
- />
+ disableUnderline
+ onPress={onBeforePressAuthor}
+ style={[a.text_md, t.atoms.text_contrast_medium, a.leading_snug]}>
+
+ {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
+
+
+
{!isAndroid && (
-
+
·
)}
+
{({timeElapsed}) => (
-
+ disableMismatchWarning
+ disableUnderline
+ onPress={onBeforePressPost}
+ style={[
+ a.text_md,
+ t.atoms.text_contrast_medium,
+ a.leading_snug,
+ web({
+ whiteSpace: 'nowrap',
+ }),
+ ]}>
+ {timeElapsed}
+
)}
@@ -129,21 +137,3 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
}
PostMeta = memo(PostMeta)
export {PostMeta}
-
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'row',
- alignItems: 'flex-end',
- paddingBottom: 2,
- gap: 4,
- zIndex: 1,
- flex: 1,
- },
- avatar: {
- alignSelf: 'center',
- },
- maxWidth: {
- flex: isAndroid ? 1 : undefined,
- flexShrink: isAndroid ? undefined : 1,
- },
-})
diff --git a/src/view/com/util/SimpleViewHeader.tsx b/src/view/com/util/SimpleViewHeader.tsx
index dc14723d2f..78b66a9296 100644
--- a/src/view/com/util/SimpleViewHeader.tsx
+++ b/src/view/com/util/SimpleViewHeader.tsx
@@ -9,12 +9,11 @@ import {
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {NavigationProp} from '#/lib/routes/types'
import {isWeb} from '#/platform/detection'
import {useSetDrawerOpen} from '#/state/shell'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {NavigationProp} from 'lib/routes/types'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {CenteredView} from './Views'
@@ -31,7 +30,6 @@ export function SimpleViewHeader({
const pal = usePalette('default')
const setDrawerOpen = useSetDrawerOpen()
const navigation = useNavigation()
- const {track} = useAnalytics()
const {isMobile} = useWebMediaQueries()
const canGoBack = navigation.canGoBack()
@@ -44,9 +42,8 @@ export function SimpleViewHeader({
}, [navigation])
const onPressMenu = React.useCallback(() => {
- track('ViewHeader:MenuButtonClicked')
setDrawerOpen(true)
- }, [track, setDrawerOpen])
+ }, [setDrawerOpen])
const Container = isMobile ? View : CenteredView
return (
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 76d9d1503e..2b4376b698 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -327,7 +327,8 @@ let EditableUserAvatar = ({
onSelectNewAvatar(croppedImage)
} catch (e: any) {
- if (!String(e).includes('Canceled')) {
+ // Don't log errors for cancelling selection to sentry on ios or android
+ if (!String(e).toLowerCase().includes('cancel')) {
logger.error('Failed to crop banner', {error: e})
}
}
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index e5121b350a..64fa504ebe 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -6,7 +6,6 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
@@ -42,7 +41,6 @@ export function ViewHeader({
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const navigation = useNavigation()
- const {track} = useAnalytics()
const {isDesktop, isTablet} = useWebMediaQueries()
const t = useTheme()
@@ -55,9 +53,8 @@ export function ViewHeader({
}, [navigation])
const onPressMenu = React.useCallback(() => {
- track('ViewHeader:MenuButtonClicked')
setDrawerOpen(true)
- }, [track, setDrawerOpen])
+ }, [setDrawerOpen])
if (isDesktop) {
if (showOnDesktop) {
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
index 03b6dd233c..fe6efc02fa 100644
--- a/src/view/com/util/forms/PostDropdownBtn.tsx
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -1,4 +1,4 @@
-import React, {memo} from 'react'
+import React, {memo, useCallback} from 'react'
import {
Platform,
Pressable,
@@ -18,9 +18,13 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
+import {getCurrentRoute} from '#/lib/routes/helpers'
import {makeProfileLink} from '#/lib/routes/links'
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
+import {shareUrl} from '#/lib/sharing'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
+import {toShareUrl} from '#/lib/strings/url-helpers'
+import {useTheme} from '#/lib/ThemeContext'
import {getTranslatorLink} from '#/locale/helpers'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
@@ -29,6 +33,7 @@ import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {useLanguagePrefs} from '#/state/preferences'
import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
import {useOpenLink} from '#/state/preferences/in-app-browser'
+import {usePinnedPostMutation} from '#/state/queries/pinned-post'
import {
usePostDeleteMutation,
useThreadMuteMutationQueue,
@@ -38,10 +43,6 @@ import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util'
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
import {useSession} from '#/state/session'
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
-import {getCurrentRoute} from 'lib/routes/helpers'
-import {shareUrl} from 'lib/sharing'
-import {toShareUrl} from 'lib/strings/url-helpers'
-import {useTheme} from 'lib/ThemeContext'
import {atoms as a, useBreakpoints, useTheme as useAlf} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
@@ -65,6 +66,7 @@ import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/E
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane'
+import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
@@ -106,7 +108,9 @@ let PostDropdownBtn = ({
const {_} = useLingui()
const defaultCtrlColor = theme.palette.default.postCtrl
const langPrefs = useLanguagePrefs()
- const postDeleteMutation = usePostDeleteMutation()
+ const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
+ const {mutateAsync: pinPostMutate, isPending: isPinPending} =
+ usePinnedPostMutation()
const hiddenPosts = useHiddenPosts()
const {hidePost} = useHiddenPostsApi()
const feedFeedback = useFeedFeedbackContext()
@@ -149,8 +153,9 @@ let PostDropdownBtn = ({
threadgateRecord,
})
const isReplyHiddenByThreadgate = threadgateHiddenReplies.has(postUri)
+ const isPinned = post.viewer?.pinned
- const {mutateAsync: toggleQuoteDetachment, isPending} =
+ const {mutateAsync: toggleQuoteDetachment, isPending: isDetachPending} =
useToggleQuoteDetachmentMutation()
const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({
@@ -169,7 +174,7 @@ let PostDropdownBtn = ({
)
const onDeletePost = React.useCallback(() => {
- postDeleteMutation.mutateAsync({uri: postUri}).then(
+ deletePostMutate({uri: postUri}).then(
() => {
Toast.show(_(msg`Post deleted`))
@@ -197,7 +202,7 @@ let PostDropdownBtn = ({
}, [
navigation,
postUri,
- postDeleteMutation,
+ deletePostMutate,
postAuthor,
currentAccount,
isAuthor,
@@ -344,6 +349,14 @@ let PostDropdownBtn = ({
toggleReplyVisibility,
])
+ const onPressPin = useCallback(() => {
+ pinPostMutate({
+ postUri,
+ postCid,
+ action: isPinned ? 'unpin' : 'pin',
+ })
+ }, [isPinned, pinPostMutate, postCid, postUri])
+
return (
@@ -372,6 +385,33 @@ let PostDropdownBtn = ({
+ {isAuthor && (
+ <>
+
+
+
+ {isPinned
+ ? _(msg`Unpin from profile`)
+ : _(msg`Pin to your profile`)}
+
+
+
+
+
+ >
+ )}
+
{(!hideInPWI || hasSession) && (
<>
@@ -536,7 +576,7 @@ let PostDropdownBtn = ({
{canDetachQuote && (
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx
index b49c49e4ab..fa2b7e3d3f 100644
--- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx
@@ -1,7 +1,7 @@
import React, {useEffect, useId, useRef, useState} from 'react'
import {View} from 'react-native'
import {AppBskyEmbedVideo} from '@atproto/api'
-import Hls, {Events, FragChangedData, Fragment} from 'hls.js'
+import type * as HlsTypes from 'hls.js'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {atoms as a} from '#/alf'
@@ -23,6 +23,7 @@ export function VideoEmbedInnerWeb({
const videoRef = useRef(null)
const [focused, setFocused] = useState(false)
const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false)
+ const [hlsLoading, setHlsLoading] = React.useState(false)
const figId = useId()
// send error up to error boundary
@@ -37,6 +38,7 @@ export function VideoEmbedInnerWeb({
setHasSubtitleTrack,
setError,
videoRef,
+ setHlsLoading,
})
return (
@@ -77,6 +79,7 @@ export function VideoEmbedInnerWeb({
setActive={setActive}
focused={focused}
setFocused={setFocused}
+ hlsLoading={hlsLoading}
onScreen={onScreen}
fullscreenRef={containerRef}
hasSubtitleTrack={hasSubtitleTrack}
@@ -99,31 +102,62 @@ export class VideoNotFoundError extends Error {
}
}
+type CachedPromise = Promise & {value: undefined | T}
+const promiseForHls = import(
+ // @ts-ignore
+ 'hls.js/dist/hls.min'
+).then(mod => mod.default) as CachedPromise
+promiseForHls.value = undefined
+promiseForHls.then(Hls => {
+ promiseForHls.value = Hls
+})
+
function useHLS({
focused,
playlist,
setHasSubtitleTrack,
setError,
videoRef,
+ setHlsLoading,
}: {
focused: boolean
playlist: string
setHasSubtitleTrack: (v: boolean) => void
setError: (v: Error | null) => void
videoRef: React.RefObject
+ setHlsLoading: (v: boolean) => void
}) {
- const hlsRef = useRef(undefined)
- const [lowQualityFragments, setLowQualityFragments] = useState([])
+ const [Hls, setHls] = useState(
+ () => promiseForHls.value,
+ )
+ useEffect(() => {
+ if (!Hls) {
+ setHlsLoading(true)
+ promiseForHls.then(loadedHls => {
+ setHls(() => loadedHls)
+ setHlsLoading(false)
+ })
+ }
+ }, [Hls, setHlsLoading])
+
+ const hlsRef = useRef(undefined)
+ const [lowQualityFragments, setLowQualityFragments] = useState<
+ HlsTypes.Fragment[]
+ >([])
// purge low quality segments from buffer on next frag change
const handleFragChange = useNonReactiveCallback(
- (_event: Events.FRAG_CHANGED, {frag}: FragChangedData) => {
+ (
+ _event: HlsTypes.Events.FRAG_CHANGED,
+ {frag}: HlsTypes.FragChangedData,
+ ) => {
+ if (!Hls) return
if (!hlsRef.current) return
const hls = hlsRef.current
if (focused && hls.nextAutoLevel > 0) {
// if the current quality level goes above 0, flush the low quality segments
- const flushed: Fragment[] = []
+ const flushed: HlsTypes.Fragment[] = []
for (const lowQualFrag of lowQualityFragments) {
// avoid if close to the current fragment
@@ -147,12 +181,15 @@ function useHLS({
useEffect(() => {
if (!videoRef.current) return
- if (!Hls.isSupported()) throw new HLSUnsupportedError()
+ if (!Hls) return
+ if (!Hls.isSupported()) {
+ throw new HLSUnsupportedError()
+ }
const hls = new Hls({
maxMaxBufferLength: 10, // only load 10s ahead
// note: the amount buffered is affected by both maxBufferLength and maxBufferSize
- // it will buffer until it it's greater than *both* of those values
+ // it will buffer until it is greater than *both* of those values
// so we use maxMaxBufferLength to set the actual maximum amount of buffering instead
})
hlsRef.current = hls
@@ -211,7 +248,7 @@ function useHLS({
hls.destroy()
abortController.abort()
}
- }, [playlist, setError, setHasSubtitleTrack, videoRef, handleFragChange])
+ }, [playlist, setError, setHasSubtitleTrack, videoRef, handleFragChange, Hls])
return hlsRef
}
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx
index 2d1427347d..dd0dafc335 100644
--- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx
@@ -43,6 +43,7 @@ export function Controls({
setFocused,
onScreen,
fullscreenRef,
+ hlsLoading,
hasSubtitleTrack,
}: {
videoRef: React.RefObject
@@ -53,6 +54,7 @@ export function Controls({
setFocused: (focused: boolean) => void
onScreen: boolean
fullscreenRef: React.RefObject
+ hlsLoading: boolean
hasSubtitleTrack: boolean
}) {
const {
@@ -80,6 +82,7 @@ export function Controls({
const [isFullscreen, toggleFullscreen] = useFullscreen(fullscreenRef)
const {state: hasFocus, onIn: onFocus, onOut: onBlur} = useInteractionState()
const [interactingViaKeypress, setInteractingViaKeypress] = useState(false)
+ const showSpinner = hlsLoading || buffering
const {
state: volumeHovered,
onIn: onVolumeHover,
@@ -409,11 +412,11 @@ export function Controls({
)}
- {(buffering || error) && (
+ {(showSpinner || error) && (
- {buffering && }
+ {showSpinner && }
{error && (
An error occurred
diff --git a/src/view/screens/AccessibilitySettings.tsx b/src/view/screens/AccessibilitySettings.tsx
index 158dc8b8da..5d314e8e69 100644
--- a/src/view/screens/AccessibilitySettings.tsx
+++ b/src/view/screens/AccessibilitySettings.tsx
@@ -4,7 +4,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
@@ -36,7 +35,6 @@ type Props = NativeStackScreenProps<
export function AccessibilitySettingsScreen({}: Props) {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
- const {screen} = useAnalytics()
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
const {_} = useLingui()
@@ -51,9 +49,8 @@ export function AccessibilitySettingsScreen({}: Props) {
useFocusEffect(
React.useCallback(() => {
- screen('PreferencesExternalEmbeds')
setMinimalShellMode(false)
- }, [screen, setMinimalShellMode]),
+ }, [setMinimalShellMode]),
)
return (
diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx
index 6f1cd1bb8f..21a9cb0eb7 100644
--- a/src/view/screens/AppPasswords.tsx
+++ b/src/view/screens/AppPasswords.tsx
@@ -12,7 +12,6 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams} from '#/lib/routes/types'
@@ -28,7 +27,7 @@ import {Button} from '#/view/com/util/forms/Button'
import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {CenteredView} from 'view/com/util/Views'
+import {CenteredView} from '#/view/com/util/Views'
import {atoms as a} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
@@ -38,16 +37,14 @@ export function AppPasswords({}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
- const {screen} = useAnalytics()
const {isTabletOrDesktop} = useWebMediaQueries()
const {openModal} = useModalControls()
const {data: appPasswords, error} = useAppPasswordsQuery()
useFocusEffect(
React.useCallback(() => {
- screen('AppPasswords')
setMinimalShellMode(false)
- }, [screen, setMinimalShellMode]),
+ }, [setMinimalShellMode]),
)
const onAdd = React.useCallback(async () => {
diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx
index bd69d7a550..c1daa54e6c 100644
--- a/src/view/screens/LanguageSettings.tsx
+++ b/src/view/screens/LanguageSettings.tsx
@@ -10,7 +10,6 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
@@ -32,21 +31,18 @@ export function LanguageSettingsScreen(_props: Props) {
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const {isTabletOrDesktop} = useWebMediaQueries()
- const {screen, track} = useAnalytics()
const setMinimalShellMode = useSetMinimalShellMode()
const {openModal} = useModalControls()
useFocusEffect(
React.useCallback(() => {
- screen('Settings')
setMinimalShellMode(false)
- }, [screen, setMinimalShellMode]),
+ }, [setMinimalShellMode]),
)
const onPressContentLanguages = React.useCallback(() => {
- track('Settings:ContentlanguagesButtonClicked')
openModal({name: 'content-languages-settings'})
- }, [track, openModal])
+ }, [openModal])
const onChangePrimaryLanguage = React.useCallback(
(value: Parameters[0]) => {
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
index ebd9bb23e9..88a5df7ece 100644
--- a/src/view/screens/ModerationBlockedAccounts.tsx
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -12,16 +12,15 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {CommonNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts'
import {useSetMinimalShellMode} from '#/state/shell'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {CommonNavigatorParams} from 'lib/routes/types'
-import {ProfileCard} from 'view/com/profile/ProfileCard'
-import {CenteredView} from 'view/com/util/Views'
+import {ProfileCard} from '#/view/com/profile/ProfileCard'
+import {CenteredView} from '#/view/com/util/Views'
import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {Text} from '../com/util/text/Text'
import {ViewHeader} from '../com/util/ViewHeader'
@@ -35,7 +34,6 @@ export function ModerationBlockedAccounts({}: Props) {
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries()
- const {screen} = useAnalytics()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
@@ -58,9 +56,8 @@ export function ModerationBlockedAccounts({}: Props) {
useFocusEffect(
React.useCallback(() => {
- screen('BlockedAccounts')
setMinimalShellMode(false)
- }, [screen, setMinimalShellMode]),
+ }, [setMinimalShellMode]),
)
const onRefresh = React.useCallback(async () => {
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
index e395a3a5ba..bd29cb2d93 100644
--- a/src/view/screens/ModerationMutedAccounts.tsx
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -12,16 +12,15 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {CommonNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts'
import {useSetMinimalShellMode} from '#/state/shell'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {CommonNavigatorParams} from 'lib/routes/types'
-import {ProfileCard} from 'view/com/profile/ProfileCard'
-import {CenteredView} from 'view/com/util/Views'
+import {ProfileCard} from '#/view/com/profile/ProfileCard'
+import {CenteredView} from '#/view/com/util/Views'
import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {Text} from '../com/util/text/Text'
import {ViewHeader} from '../com/util/ViewHeader'
@@ -35,7 +34,6 @@ export function ModerationMutedAccounts({}: Props) {
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries()
- const {screen} = useAnalytics()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
@@ -58,9 +56,8 @@ export function ModerationMutedAccounts({}: Props) {
useFocusEffect(
React.useCallback(() => {
- screen('MutedAccounts')
setMinimalShellMode(false)
- }, [screen, setMinimalShellMode]),
+ }, [setMinimalShellMode]),
)
const onRefresh = React.useCallback(async () => {
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 073e91c45e..818d3d0ed4 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -5,7 +5,6 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {ComposeIcon2} from '#/lib/icons'
@@ -27,11 +26,11 @@ import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {Feed} from '#/view/com/notifications/Feed'
import {FAB} from '#/view/com/util/fab/FAB'
+import {ListMethods} from '#/view/com/util/List'
+import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {ListMethods} from 'view/com/util/List'
-import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
-import {CenteredView} from 'view/com/util/Views'
+import {CenteredView} from '#/view/com/util/Views'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2'
@@ -49,7 +48,6 @@ export function NotificationsScreen({route: {params}}: Props) {
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
const scrollElRef = React.useRef(null)
- const {screen} = useAnalytics()
const t = useTheme()
const {isDesktop} = useWebMediaQueries()
const queryClient = useQueryClient()
@@ -101,9 +99,8 @@ export function NotificationsScreen({route: {params}}: Props) {
React.useCallback(() => {
setMinimalShellMode(false)
logger.debug('NotificationsScreen: Focus')
- screen('Notifications')
onFocusCheckLatest()
- }, [screen, setMinimalShellMode, onFocusCheckLatest]),
+ }, [setMinimalShellMode, onFocusCheckLatest]),
)
React.useEffect(() => {
if (!isScreenFocused) {
diff --git a/src/view/screens/PreferencesExternalEmbeds.tsx b/src/view/screens/PreferencesExternalEmbeds.tsx
index 8b3550d6b3..ae23b6e950 100644
--- a/src/view/screens/PreferencesExternalEmbeds.tsx
+++ b/src/view/screens/PreferencesExternalEmbeds.tsx
@@ -3,7 +3,6 @@ import {StyleSheet, View} from 'react-native'
import {Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
@@ -30,14 +29,12 @@ type Props = NativeStackScreenProps<
export function PreferencesExternalEmbeds({}: Props) {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
- const {screen} = useAnalytics()
const {isTabletOrMobile} = useWebMediaQueries()
useFocusEffect(
React.useCallback(() => {
- screen('PreferencesExternalEmbeds')
setMinimalShellMode(false)
- }, [screen, setMinimalShellMode]),
+ }, [setMinimalShellMode]),
)
return (
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 810bbff889..7726256952 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -16,7 +16,6 @@ import {
useQueryClient,
} from '@tanstack/react-query'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {ComposeIcon2} from '#/lib/icons'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
@@ -167,7 +166,6 @@ function ProfileScreenLoaded({
const {hasSession, currentAccount} = useSession()
const setMinimalShellMode = useSetMinimalShellMode()
const {openComposer} = useComposerControls()
- const {screen, track} = useAnalytics()
const {
data: labelerInfo,
error: labelerError,
@@ -296,11 +294,10 @@ function ProfileScreenLoaded({
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
- screen('Profile')
return listenSoftReset(() => {
scrollSectionToTop(currentPage)
})
- }, [setMinimalShellMode, screen, currentPage, scrollSectionToTop]),
+ }, [setMinimalShellMode, currentPage, scrollSectionToTop]),
)
useFocusEffect(
@@ -316,7 +313,6 @@ function ProfileScreenLoaded({
// =
const onPressCompose = () => {
- track('ProfileScreen:PressCompose')
const mention =
profile.handle === currentAccount?.handle ||
isInvalidHandle(profile.handle)
@@ -363,7 +359,8 @@ function ProfileScreenLoaded({
items={sectionTitles}
onPageSelected={onPageSelected}
onCurrentPageSelected={onCurrentPageSelected}
- renderHeader={renderHeader}>
+ renderHeader={renderHeader}
+ allowHeaderOverScroll>
{showFiltersTab
? ({headerHeight, isFocused, scrollElRef}) => (
(null)
const isScreenFocused = useIsFocused()
@@ -247,8 +245,7 @@ export function ProfileFeedScreenInner({
const onPressShare = React.useCallback(() => {
const url = toShareUrl(feedInfo.route.href)
shareUrl(url)
- track('CustomFeed:Share')
- }, [feedInfo, track])
+ }, [feedInfo])
const onPressReport = React.useCallback(() => {
reportDialogControl.open()
@@ -515,7 +512,6 @@ function AboutSection({
const {_} = useLingui()
const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
const {hasSession} = useSession()
- const {track} = useAnalytics()
const playHaptic = useHaptics()
const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
@@ -532,11 +528,9 @@ function AboutSection({
if (isLiked && likeUri) {
await unlikeFeed({uri: likeUri})
- track('CustomFeed:Unlike')
setLikeUri('')
} else {
const res = await likeFeed({uri: feedInfo.uri, cid: feedInfo.cid})
- track('CustomFeed:Like')
setLikeUri(res.uri)
}
} catch (err) {
@@ -548,7 +542,7 @@ function AboutSection({
)
logger.error('Failed up toggle like', {message: err})
}
- }, [playHaptic, isLiked, likeUri, unlikeFeed, track, likeFeed, feedInfo, _])
+ }, [playHaptic, isLiked, likeUri, unlikeFeed, likeFeed, feedInfo, _])
return (
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 0c2c6405fc..e0fd18ae9a 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -14,8 +14,19 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
-import {useAnalytics} from '#/lib/analytics/analytics'
+import {useHaptics} from '#/lib/haptics'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {ComposeIcon2} from '#/lib/icons'
+import {makeListLink, makeProfileLink} from '#/lib/routes/links'
+import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {NavigationProp} from '#/lib/routes/types'
+import {shareUrl} from '#/lib/sharing'
import {cleanError} from '#/lib/strings/errors'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {toShareUrl} from '#/lib/strings/url-helpers'
+import {s} from '#/lib/styles'
import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {listenSoftReset} from '#/state/events'
@@ -41,33 +52,24 @@ import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
-import {useHaptics} from 'lib/haptics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useSetTitle} from 'lib/hooks/useSetTitle'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {ComposeIcon2} from 'lib/icons'
-import {makeListLink, makeProfileLink} from 'lib/routes/links'
-import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
-import {NavigationProp} from 'lib/routes/types'
-import {shareUrl} from 'lib/sharing'
-import {sanitizeHandle} from 'lib/strings/handles'
-import {toShareUrl} from 'lib/strings/url-helpers'
-import {s} from 'lib/styles'
import {ListMembers} from '#/view/com/lists/ListMembers'
-import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
-import {Feed} from 'view/com/posts/Feed'
-import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
-import {EmptyState} from 'view/com/util/EmptyState'
-import {FAB} from 'view/com/util/fab/FAB'
-import {Button} from 'view/com/util/forms/Button'
-import {DropdownItem, NativeDropdown} from 'view/com/util/forms/NativeDropdown'
-import {TextLink} from 'view/com/util/Link'
-import {ListRef} from 'view/com/util/List'
-import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
-import {LoadingScreen} from 'view/com/util/LoadingScreen'
-import {Text} from 'view/com/util/text/Text'
-import * as Toast from 'view/com/util/Toast'
-import {CenteredView} from 'view/com/util/Views'
+import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
+import {Feed} from '#/view/com/posts/Feed'
+import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
+import {EmptyState} from '#/view/com/util/EmptyState'
+import {FAB} from '#/view/com/util/fab/FAB'
+import {Button} from '#/view/com/util/forms/Button'
+import {
+ DropdownItem,
+ NativeDropdown,
+} from '#/view/com/util/forms/NativeDropdown'
+import {TextLink} from '#/view/com/util/Link'
+import {ListRef} from '#/view/com/util/List'
+import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
+import {LoadingScreen} from '#/view/com/util/LoadingScreen'
+import {Text} from '#/view/com/util/text/Text'
+import * as Toast from '#/view/com/util/Toast'
+import {CenteredView} from '#/view/com/util/Views'
import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
import {atoms as a, useTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
@@ -306,7 +308,6 @@ function Header({
const isBlocking = !!list.viewer?.blocked
const isMuting = !!list.viewer?.muted
const isOwner = list.creator.did === currentAccount?.did
- const {track} = useAnalytics()
const playHaptic = useHaptics()
const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
@@ -384,7 +385,6 @@ function Header({
try {
await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
Toast.show(_(msg`List muted`))
- track('Lists:Mute')
} catch {
Toast.show(
_(
@@ -392,13 +392,12 @@ function Header({
),
)
}
- }, [list, listMuteMutation, track, _])
+ }, [list, listMuteMutation, _])
const onUnsubscribeMute = useCallback(async () => {
try {
await listMuteMutation.mutateAsync({uri: list.uri, mute: false})
Toast.show(_(msg`List unmuted`))
- track('Lists:Unmute')
} catch {
Toast.show(
_(
@@ -406,13 +405,12 @@ function Header({
),
)
}
- }, [list, listMuteMutation, track, _])
+ }, [list, listMuteMutation, _])
const onSubscribeBlock = useCallback(async () => {
try {
await listBlockMutation.mutateAsync({uri: list.uri, block: true})
Toast.show(_(msg`List blocked`))
- track('Lists:Block')
} catch {
Toast.show(
_(
@@ -420,13 +418,12 @@ function Header({
),
)
}
- }, [list, listBlockMutation, track, _])
+ }, [list, listBlockMutation, _])
const onUnsubscribeBlock = useCallback(async () => {
try {
await listBlockMutation.mutateAsync({uri: list.uri, block: false})
Toast.show(_(msg`List unblocked`))
- track('Lists:Unblock')
} catch {
Toast.show(
_(
@@ -434,7 +431,7 @@ function Header({
),
)
}
- }, [list, listBlockMutation, track, _])
+ }, [list, listBlockMutation, _])
const onPressEdit = useCallback(() => {
openModal({
@@ -451,7 +448,6 @@ function Header({
}
Toast.show(_(msg`List deleted`))
- track('Lists:Delete')
if (navigation.canGoBack()) {
navigation.goBack()
} else {
@@ -461,7 +457,6 @@ function Header({
list,
listDeleteMutation,
navigation,
- track,
_,
removeSavedFeed,
savedFeedConfig,
@@ -474,8 +469,7 @@ function Header({
const onPressShare = useCallback(() => {
const url = toShareUrl(`/profile/${list.creator.did}/lists/${rkey}`)
shareUrl(url)
- track('Lists:Share')
- }, [list, rkey, track])
+ }, [list, rkey])
const dropdownItems: DropdownItem[] = useMemo(() => {
let items: DropdownItem[] = [
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 3402db2c19..66bbd9b8a0 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -7,7 +7,11 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
-import {track} from '#/lib/analytics/analytics'
+import {useHaptics} from '#/lib/haptics'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {colors, s} from '#/lib/styles'
import {logger} from '#/logger'
import {
useOverwriteSavedFeedsMutation,
@@ -16,18 +20,12 @@ import {
} from '#/state/queries/preferences'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
import {useSetMinimalShellMode} from '#/state/shell'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {useHaptics} from 'lib/haptics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {CommonNavigatorParams} from 'lib/routes/types'
-import {colors, s} from 'lib/styles'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-import {TextLink} from 'view/com/util/Link'
-import {Text} from 'view/com/util/text/Text'
-import * as Toast from 'view/com/util/Toast'
-import {ViewHeader} from 'view/com/util/ViewHeader'
-import {CenteredView, ScrollView} from 'view/com/util/Views'
+import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard'
+import {TextLink} from '#/view/com/util/Link'
+import {Text} from '#/view/com/util/text/Text'
+import * as Toast from '#/view/com/util/Toast'
+import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {NoFollowingFeed} from '#/screens/Feeds/NoFollowingFeed'
import {NoSavedFeedsOfAnyType} from '#/screens/Feeds/NoSavedFeedsOfAnyType'
import {atoms as a, useTheme} from '#/alf'
@@ -51,7 +49,6 @@ export function SavedFeeds({}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
- const {screen} = useAnalytics()
const setMinimalShellMode = useSetMinimalShellMode()
const {data: preferences} = usePreferencesQuery()
const {
@@ -77,9 +74,8 @@ export function SavedFeeds({}: Props) {
useFocusEffect(
React.useCallback(() => {
- screen('SavedFeeds')
setMinimalShellMode(false)
- }, [screen, setMinimalShellMode]),
+ }, [setMinimalShellMode]),
)
return (
@@ -256,10 +252,6 @@ function ListItem({
try {
await overwriteSavedFeeds(nextFeeds)
- track('CustomFeed:Reorder', {
- uri: feed.value,
- index: nextIndex,
- })
} catch (e) {
Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to set pinned feed order', {message: e})
@@ -282,10 +274,6 @@ function ListItem({
try {
await overwriteSavedFeeds(nextFeeds)
- track('CustomFeed:Reorder', {
- uri: feed.value,
- index: nextIndex,
- })
} catch (e) {
Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to set pinned feed order', {message: e})
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index 07d762c0fe..66c354b3be 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -11,6 +11,7 @@ import {
View,
} from 'react-native'
import {ScrollView as RNGHScrollView} from 'react-native-gesture-handler'
+import RNPickerSelect from 'react-native-picker-select'
import {AppBskyActorDefs, AppBskyFeedDefs, moderateProfile} from '@atproto/api'
import {
FontAwesomeIcon,
@@ -21,7 +22,7 @@ import {useLingui} from '@lingui/react'
import AsyncStorage from '@react-native-async-storage/async-storage'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
+import {LANGUAGES} from '#/lib/../locale/languages'
import {createHitslop} from '#/lib/constants'
import {HITSLOP_10} from '#/lib/constants'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -35,10 +36,10 @@ import {
SearchTabNavigatorParams,
} from '#/lib/routes/types'
import {augmentSearchQuery} from '#/lib/strings/helpers'
-import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {listenSoftReset} from '#/state/events'
+import {useLanguagePrefs} from '#/state/preferences/languages'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useActorSearch} from '#/state/queries/actor-search'
@@ -57,9 +58,16 @@ import {Text} from '#/view/com/util/text/Text'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {Explore} from '#/view/screens/Search/Explore'
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
-import {atoms as a, useTheme as useThemeNew} from '#/alf'
+import {makeSearchQuery, parseSearchQuery} from '#/screens/Search/utils'
+import {atoms as a, useBreakpoints, useTheme as useThemeNew, web} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard'
+import * as TextField from '#/components/forms/TextField'
+import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
+import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
+import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
+import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
function Loader() {
const pal = usePalette('default')
@@ -251,7 +259,7 @@ let SearchScreenUserResults = ({
const {_} = useLingui()
const {data: results, isFetched} = useActorSearch({
- query: query,
+ query,
enabled: active,
})
@@ -324,7 +332,138 @@ let SearchScreenFeedsResults = ({
}
SearchScreenFeedsResults = React.memo(SearchScreenFeedsResults)
-let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => {
+function SearchLanguageDropdown({
+ value,
+ onChange,
+}: {
+ value: string
+ onChange(value: string): void
+}) {
+ const t = useThemeNew()
+ const {contentLanguages} = useLanguagePrefs()
+
+ const items = React.useMemo(() => {
+ return LANGUAGES.filter(l => Boolean(l.code2))
+ .map(l => ({
+ label: l.name,
+ inputLabel: l.name,
+ value: l.code2,
+ key: l.code2 + l.code3,
+ }))
+ .sort(a => (contentLanguages.includes(a.value) ? -1 : 1))
+ }, [contentLanguages])
+
+ const style = {
+ backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
+ color: t.atoms.text.color,
+ fontSize: a.text_xs.fontSize,
+ fontFamily: 'inherit',
+ fontWeight: a.font_bold.fontWeight,
+ paddingHorizontal: 14,
+ paddingRight: 32,
+ paddingVertical: 8,
+ borderRadius: a.rounded_full.borderRadius,
+ borderWidth: a.border.borderWidth,
+ borderColor: t.atoms.border_contrast_low.borderColor,
+ }
+
+ return (
+ (
+
+ )}
+ useNativeAndroidPickerStyle={false}
+ style={{
+ iconContainer: {
+ pointerEvents: 'none',
+ right: a.px_sm.paddingRight,
+ top: 0,
+ bottom: 0,
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ inputAndroid: {
+ ...style,
+ paddingVertical: 2,
+ },
+ inputIOS: {
+ ...style,
+ },
+ inputWeb: web({
+ ...style,
+ cursor: 'pointer',
+ // @ts-ignore web only
+ '-moz-appearance': 'none',
+ '-webkit-appearance': 'none',
+ appearance: 'none',
+ outline: 0,
+ borderWidth: 0,
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ textOverflow: 'ellipsis',
+ }),
+ }}
+ />
+ )
+}
+
+function useQueryManager({initialQuery}: {initialQuery: string}) {
+ const {contentLanguages} = useLanguagePrefs()
+ const {query, params: initialParams} = React.useMemo(() => {
+ return parseSearchQuery(initialQuery || '')
+ }, [initialQuery])
+ const prevInitialQuery = React.useRef(initialQuery)
+ const [lang, setLang] = React.useState(
+ initialParams.lang || contentLanguages[0],
+ )
+
+ if (initialQuery !== prevInitialQuery.current) {
+ // handle new queryParam change (from manual search entry)
+ prevInitialQuery.current = initialQuery
+ setLang(initialParams.lang || contentLanguages[0])
+ }
+
+ const params = React.useMemo(
+ () => ({
+ // default stuff
+ ...initialParams,
+ // managed stuff
+ lang,
+ }),
+ [lang, initialParams],
+ )
+ const handlers = React.useMemo(
+ () => ({
+ setLang,
+ }),
+ [setLang],
+ )
+
+ return React.useMemo(() => {
+ return {
+ query,
+ queryWithParams: makeSearchQuery(query, params),
+ params: {
+ ...params,
+ ...handlers,
+ },
+ }
+ }, [query, params, handlers])
+}
+
+let SearchScreenInner = ({
+ query,
+ queryWithParams,
+ headerHeight,
+}: {
+ query: string
+ queryWithParams: string
+ headerHeight: number
+}): React.ReactNode => {
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
@@ -349,7 +488,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => {
title: _(msg`Top`),
component: (
@@ -359,7 +498,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => {
title: _(msg`Latest`),
component: (
@@ -378,7 +517,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => {
),
},
]
- }, [_, query, activeTab])
+ }, [_, query, queryWithParams, activeTab])
return query ? (
{
renderTabBar={props => (
+ style={[
+ pal.border,
+ pal.view,
+ web({
+ position: isWeb ? 'sticky' : '',
+ zIndex: 1,
+ }),
+ {top: isWeb ? headerHeight : undefined},
+ ]}>
section.title)} {...props} />
)}
@@ -448,14 +595,13 @@ SearchScreenInner = React.memo(SearchScreenInner)
export function SearchScreen(
props: NativeStackScreenProps,
) {
+ const t = useThemeNew()
+ const {gtMobile} = useBreakpoints()
const navigation = useNavigation()
const textInput = React.useRef(null)
const {_} = useLingui()
- const pal = usePalette('default')
- const {track} = useAnalytics()
const setDrawerOpen = useSetDrawerOpen()
const setMinimalShellMode = useSetMinimalShellMode()
- const {isTabletOrDesktop, isTabletOrMobile} = useWebMediaQueries()
// Query terms
const queryParam = props.route?.params?.q ?? ''
@@ -469,6 +615,17 @@ export function SearchScreen(
AppBskyActorDefs.ProfileViewBasic[]
>([])
+ const {params, query, queryWithParams} = useQueryManager({
+ initialQuery: queryParam,
+ })
+ const showFiltersButton = Boolean(query && !showAutocomplete)
+ const [showFilters, setShowFilters] = React.useState(false)
+ /*
+ * Arbitrary sizing, so guess and check, used for sticky header alignment and
+ * sizing.
+ */
+ const headerHeight = 56 + (showFilters ? 40 : 0)
+
useFocusEffect(
useNonReactiveCallback(() => {
if (isWeb) {
@@ -497,9 +654,8 @@ export function SearchScreen(
}, [])
const onPressMenu = React.useCallback(() => {
- track('ViewHeader:MenuButtonClicked')
setDrawerOpen(true)
- }, [track, setDrawerOpen])
+ }, [setDrawerOpen])
const onPressClearQuery = React.useCallback(() => {
scrollToTopWeb()
@@ -507,13 +663,6 @@ export function SearchScreen(
textInput.current?.focus()
}, [])
- const onPressCancelSearch = React.useCallback(() => {
- scrollToTopWeb()
- textInput.current?.blur()
- setShowAutocomplete(false)
- setSearchText(queryParam)
- }, [queryParam])
-
const onChangeText = React.useCallback(async (text: string) => {
scrollToTopWeb()
setSearchText(text)
@@ -586,6 +735,13 @@ export function SearchScreen(
[updateSearchHistory, navigation],
)
+ const onPressCancelSearch = React.useCallback(() => {
+ scrollToTopWeb()
+ textInput.current?.blur()
+ setShowAutocomplete(false)
+ setSearchText(queryParam)
+ }, [setShowAutocomplete, setSearchText, queryParam])
+
const onSubmit = React.useCallback(() => {
navigateToItem(searchText)
}, [navigateToItem, searchText])
@@ -624,6 +780,7 @@ export function SearchScreen(
setSearchText('')
navigation.setParams({q: ''})
}
+ setShowFilters(false)
}, [navigation])
useFocusEffect(
@@ -663,50 +820,107 @@ export function SearchScreen(
[selectedProfiles],
)
+ const onSearchInputFocus = React.useCallback(() => {
+ if (isWeb) {
+ // Prevent a jump on iPad by ensuring that
+ // the initial focused render has no result list.
+ requestAnimationFrame(() => {
+ setShowAutocomplete(true)
+ })
+ } else {
+ setShowAutocomplete(true)
+ }
+ setShowFilters(false)
+ }, [setShowAutocomplete])
+
return (
- {isTabletOrMobile && (
-
-
-
- )}
-
- {showAutocomplete && (
-
-
+
+ {!gtMobile && (
+
+
+
+ )}
+
+ {showFiltersButton && (
+ setShowFilters(!showFilters)}
+ hitSlop={HITSLOP_10}
+ label={_(msg`Show advanced filters`)}
+ size="large"
+ variant="solid"
+ color="secondary"
+ shape="square">
+
+
+ )}
+ {showAutocomplete && (
+
-
+
Cancel
-
-
+
+
+ )}
+
+
+ {showFilters && (
+
+
+
+
)}
+
-
+
)
@@ -747,7 +965,7 @@ let SearchInputBox = ({
textInput,
searchText,
showAutocomplete,
- setShowAutocomplete,
+ onFocus,
onChangeText,
onSubmit,
onPressClearQuery,
@@ -755,83 +973,62 @@ let SearchInputBox = ({
textInput: React.RefObject
searchText: string
showAutocomplete: boolean
- setShowAutocomplete: (show: boolean) => void
+ onFocus: () => void
onChangeText: (text: string) => void
onSubmit: () => void
onPressClearQuery: () => void
}): React.ReactNode => {
- const pal = usePalette('default')
const {_} = useLingui()
- const theme = useTheme()
+ const t = useThemeNew()
+
return (
- {
- textInput.current?.focus()
- }}>
-
- {
- if (isWeb) {
- // Prevent a jump on iPad by ensuring that
- // the initial focused render has no result list.
- requestAnimationFrame(() => {
- setShowAutocomplete(true)
- })
- } else {
- setShowAutocomplete(true)
- }
- }}
- onChangeText={onChangeText}
- onSubmitEditing={onSubmit}
- autoFocus={false}
- accessibilityRole="search"
- accessibilityLabel={_(msg`Search`)}
- accessibilityHint=""
- autoCorrect={false}
- autoComplete="off"
- autoCapitalize="none"
- />
+
+
+
+
+
+
{showAutocomplete && searchText.length > 0 && (
-
-
-
+
+
+
+
+
)}
-
+
)
}
SearchInputBox = React.memo(SearchInputBox)
@@ -1029,21 +1226,7 @@ function scrollToTopWeb() {
}
}
-const HEADER_HEIGHT = 46
-
const styles = StyleSheet.create({
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 12,
- paddingLeft: 13,
- paddingVertical: 4,
- height: HEADER_HEIGHT,
- // @ts-ignore web only
- position: isWeb ? 'sticky' : '',
- top: 0,
- zIndex: 1,
- },
headerMenuBtn: {
width: 30,
height: 30,
@@ -1075,12 +1258,6 @@ const styles = StyleSheet.create({
zIndex: -1,
elevation: -1, // For Android
},
- tabBarContainer: {
- // @ts-ignore web only
- position: isWeb ? 'sticky' : '',
- top: isWeb ? HEADER_HEIGHT : 0,
- zIndex: 1,
- },
searchHistoryContainer: {
width: '100%',
paddingHorizontal: 12,
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index 737ca2d28a..a2b7670979 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -18,7 +18,6 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info'
import {STATUS_PAGE_URL} from '#/lib/constants'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
@@ -94,10 +93,14 @@ function SettingsAccountCard({
/>
-
+
{profile?.displayName || account.handle}
-
+
{account.handle}
@@ -142,7 +145,6 @@ export function SettingsScreen({}: Props) {
const onboardingDispatch = useOnboardingDispatch()
const navigation = useNavigation()
const {isMobile} = useWebMediaQueries()
- const {screen, track} = useAnalytics()
const {openModal} = useModalControls()
const {accounts, currentAccount} = useSession()
const {mutate: clearPreferences} = useClearPreferencesMutation()
@@ -174,19 +176,16 @@ export function SettingsScreen({}: Props) {
useFocusEffect(
React.useCallback(() => {
- screen('Settings')
setMinimalShellMode(false)
- }, [screen, setMinimalShellMode]),
+ }, [setMinimalShellMode]),
)
const onPressAddAccount = React.useCallback(() => {
- track('Settings:AddAccountButtonClicked')
setShowLoggedOut(true)
closeAllActiveElements()
- }, [track, setShowLoggedOut, closeAllActiveElements])
+ }, [setShowLoggedOut, closeAllActiveElements])
const onPressChangeHandle = React.useCallback(() => {
- track('Settings:ChangeHandleButtonClicked')
openModal({
name: 'change-handle',
onChanged() {
@@ -198,7 +197,7 @@ export function SettingsScreen({}: Props) {
}
},
})
- }, [track, queryClient, openModal, currentAccount])
+ }, [queryClient, openModal, currentAccount])
const onPressExportRepository = React.useCallback(() => {
exportCarControl.open()
diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx
index fc414d31f3..8ec118ae3e 100644
--- a/src/view/screens/Storybook/Forms.tsx
+++ b/src/view/screens/Storybook/Forms.tsx
@@ -32,7 +32,7 @@ export function Forms() {
label="Text field"
/>
-
+
diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx
index 049f35d35d..4a07cf9d96 100644
--- a/src/view/shell/Composer.tsx
+++ b/src/view/shell/Composer.tsx
@@ -54,6 +54,7 @@ export function Composer({winHeight}: {winHeight: number}) {
mention={state.mention}
text={state.text}
imageUris={state.imageUris}
+ videoUri={state.videoUri}
/>
)
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 226fe24966..ca2cbb3e08 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -14,7 +14,6 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
import {usePalette} from '#/lib/hooks/usePalette'
@@ -146,7 +145,6 @@ let DrawerContent = ({}: {}): React.ReactNode => {
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const navigation = useNavigation()
- const {track} = useAnalytics()
const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} =
useNavigationTabState()
const {hasSession, currentAccount} = useSession()
@@ -157,7 +155,6 @@ let DrawerContent = ({}: {}): React.ReactNode => {
const onPressTab = React.useCallback(
(tab: string) => {
- track('Menu:ItemClicked', {url: tab})
const state = navigation.getState()
setDrawerOpen(false)
if (isWeb) {
@@ -180,7 +177,7 @@ let DrawerContent = ({}: {}): React.ReactNode => {
}
}
},
- [track, navigation, setDrawerOpen, currentAccount],
+ [navigation, setDrawerOpen, currentAccount],
)
const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
@@ -200,37 +197,32 @@ let DrawerContent = ({}: {}): React.ReactNode => {
}, [onPressTab])
const onPressMyFeeds = React.useCallback(() => {
- track('Menu:ItemClicked', {url: 'Feeds'})
navigation.navigate('Feeds')
setDrawerOpen(false)
- }, [navigation, setDrawerOpen, track])
+ }, [navigation, setDrawerOpen])
const onPressLists = React.useCallback(() => {
- track('Menu:ItemClicked', {url: 'Lists'})
navigation.navigate('Lists')
setDrawerOpen(false)
- }, [navigation, track, setDrawerOpen])
+ }, [navigation, setDrawerOpen])
const onPressSettings = React.useCallback(() => {
- track('Menu:ItemClicked', {url: 'Settings'})
navigation.navigate('Settings')
setDrawerOpen(false)
- }, [navigation, track, setDrawerOpen])
+ }, [navigation, setDrawerOpen])
const onPressFeedback = React.useCallback(() => {
- track('Menu:FeedbackClicked')
Linking.openURL(
FEEDBACK_FORM_URL({
email: currentAccount?.email,
handle: currentAccount?.handle,
}),
)
- }, [track, currentAccount])
+ }, [currentAccount])
const onPressHelp = React.useCallback(() => {
- track('Menu:HelpClicked')
Linking.openURL(HELP_DESK_URL)
- }, [track])
+ }, [])
// rendering
// =
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index f6d16ae8e5..af06134fce 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -1,5 +1,5 @@
import React, {ComponentProps} from 'react'
-import {GestureResponderEvent, TouchableOpacity, View} from 'react-native'
+import {GestureResponderEvent, View} from 'react-native'
import Animated from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg, Trans} from '@lingui/macro'
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
import {StackActions} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
+import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {useHaptics} from '#/lib/haptics'
import {useDedupe} from '#/lib/hooks/useDedupe'
import {useMinimalShellFooterTransform} from '#/lib/hooks/useMinimalShellTransform'
@@ -29,6 +29,7 @@ import {Text} from '#/view/com/util/text/Text'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
+import {atoms as a} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
import {
@@ -60,7 +61,6 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const pal = usePalette('default')
const {_} = useLingui()
const safeAreaInsets = useSafeAreaInsets()
- const {track} = useAnalytics()
const {footerHeight} = useShellLayout()
const {isAtHome, isAtSearch, isAtNotifications, isAtMyProfile, isAtMessages} =
useNavigationTabState()
@@ -88,7 +88,6 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const onPressTab = React.useCallback(
(tab: TabOptions) => {
- track(`MobileShell:${tab}ButtonPressed`)
const state = navigation.getState()
const tabState = getTabState(state, tab)
if (tabState === TabState.InsideAtRoot) {
@@ -99,7 +98,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
dedupe(() => navigation.navigate(`${tab}Tab`))
}
},
- [track, navigation, dedupe],
+ [navigation, dedupe],
)
const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
const onPressSearch = React.useCallback(
@@ -326,7 +325,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
interface BtnProps
extends Pick<
- ComponentProps,
+ ComponentProps,
| 'accessible'
| 'accessibilityRole'
| 'accessibilityHint'
@@ -350,21 +349,21 @@ function Btn({
accessibilityLabel,
}: BtnProps) {
return (
-
+ accessibilityHint={accessibilityHint}
+ targetScale={0.8}>
{icon}
{notificationCount ? (
{notificationCount}
) : undefined}
-
+
)
}
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 6cceaccd92..ecd00a9192 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -18,7 +18,6 @@ import {getCurrentRoute, isStateAtTabRoot, isTab} from '#/lib/routes/helpers'
import {makeProfileLink} from '#/lib/routes/links'
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {isInvalidHandle} from '#/lib/strings/handles'
-import {colors, s} from '#/lib/styles'
import {emitSoftReset} from '#/state/events'
import {useFetchHandle} from '#/state/queries/handle'
import {useUnreadMessageCount} from '#/state/queries/messages/list-converations'
@@ -29,9 +28,10 @@ import {useComposerControls} from '#/state/shell/composer'
import {Link} from '#/view/com/util/Link'
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {PressableWithHover} from '#/view/com/util/PressableWithHover'
-import {Text} from '#/view/com/util/text/Text'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {NavSignupCard} from '#/view/shell/NavSignupCard'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {
Bell_Filled_Corner0_Rounded as BellFilled,
Bell_Stroke2_Corner0_Rounded as Bell,
@@ -63,6 +63,7 @@ import {
UserCircle_Filled_Corner0_Rounded as UserCircleFilled,
UserCircle_Stroke2_Corner0_Rounded as UserCircle,
} from '#/components/icons/UserCircle'
+import {Text} from '#/components/Typography'
import {router} from '../../../routes'
const NAV_ICON_WIDTH = 28
@@ -149,9 +150,10 @@ interface NavItemProps {
label: string
}
function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
- const pal = usePalette('default')
+ const t = useTheme()
const {currentAccount} = useSession()
- const {isDesktop, isTablet} = useWebMediaQueries()
+ const {gtMobile, gtTablet} = useBreakpoints()
+ const isTablet = gtMobile && !gtTablet
const [pathName] = React.useMemo(() => router.matchPath(href), [href])
const currentRouteInfo = useNavigationState(state => {
if (!state) {
@@ -183,8 +185,8 @@ function NavItem({count, href, icon, iconFilled, label}: NavItemProps) {
return (
{isCurrent ? iconFilled : icon}
{typeof count === 'string' && count ? (
{count}
) : null}
- {isDesktop && (
-
+ {gtTablet && (
+
{label}
)}
@@ -268,21 +297,20 @@ function ComposeBtn() {
return null
}
return (
-
-
+
-
-
-
-
+ size="large"
+ variant="solid"
+ color="primary"
+ style={[a.rounded_full]}>
+
+
New Post
-
-
+
+
)
}
@@ -440,67 +468,4 @@ const styles = StyleSheet.create({
width: 30,
height: 30,
},
-
- navItemWrapper: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 12,
- padding: 12,
- borderRadius: 8,
- gap: 10,
- },
- navItemIconWrapper: {
- alignItems: 'center',
- justifyContent: 'center',
- width: 28,
- height: 24,
- marginTop: 2,
- zIndex: 1,
- },
- navItemIconWrapperTablet: {
- width: 40,
- height: 40,
- },
- navItemCount: {
- position: 'absolute',
- top: 0,
- left: 15,
- backgroundColor: colors.blue3,
- color: colors.white,
- fontSize: 12,
- fontWeight: '600',
- paddingHorizontal: 4,
- borderRadius: 6,
- },
- navItemCountTablet: {
- left: 18,
- fontSize: 14,
- },
-
- newPostBtnContainer: {
- flexDirection: 'row',
- },
- newPostBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- borderRadius: 24,
- paddingTop: 10,
- paddingBottom: 12, // visually aligns the text vertically inside the button
- paddingLeft: 16,
- paddingRight: 18, // looks nicer like this
- backgroundColor: colors.blue3,
- marginLeft: 12,
- marginTop: 20,
- marginBottom: 10,
- gap: 8,
- },
- newPostBtnIconWrapper: {
- marginTop: 2, // aligns the icon visually with the text
- },
- newPostBtnLabel: {
- color: colors.white,
- fontSize: 16,
- fontWeight: '600',
- },
})
diff --git a/yarn.lock b/yarn.lock
index 6ed9482eff..04c94c380e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -85,15 +85,15 @@
multiformats "^9.9.0"
tlds "^1.234.0"
-"@atproto/api@^0.13.7":
- version "0.13.7"
- resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.7.tgz#072eba2025d5251505f17b0b5d2de33749ea5ee4"
- integrity sha512-41kSLmFWDbuPOenb52WRq1lnBkSZrL+X29tWcvEt6SZXK4xBoKAalw1MjF+oabhzff12iMtNaNvmmt2fu1L+cw==
+"@atproto/api@^0.13.8":
+ version "0.13.8"
+ resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.8.tgz#44aa4992442812604bccf9eebe4d1db9ed64c179"
+ integrity sha512-1RlvMg8iAT5k3F0U3549ct9+jXthlXtfFXIfTXLyXXFe9Exfvmr7ZJ1ra41vU1nXGsoouCoTxj7kdzC4MY8JZg==
dependencies:
- "@atproto/common-web" "^0.3.0"
- "@atproto/lexicon" "^0.4.1"
+ "@atproto/common-web" "^0.3.1"
+ "@atproto/lexicon" "^0.4.2"
"@atproto/syntax" "^0.3.0"
- "@atproto/xrpc" "^0.6.2"
+ "@atproto/xrpc" "^0.6.3"
await-lock "^2.2.2"
multiformats "^9.9.0"
tlds "^1.234.0"
@@ -179,6 +179,16 @@
uint8arrays "3.0.0"
zod "^3.21.4"
+"@atproto/common-web@^0.3.1":
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.1.tgz#86f8efb10a4b9073839cee914c6c08a664917cc4"
+ integrity sha512-N7wiTnus5vAr+lT//0y8m/FaHHLJ9LpGuEwkwDAeV3LCiPif4m/FS8x/QOYrx1PdZQwKso95RAPzCGWQBH5j6Q==
+ dependencies:
+ graphemer "^1.4.0"
+ multiformats "^9.9.0"
+ uint8arrays "3.0.0"
+ zod "^3.23.8"
+
"@atproto/common@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210"
@@ -292,6 +302,17 @@
multiformats "^9.9.0"
zod "^3.23.8"
+"@atproto/lexicon@^0.4.2":
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.2.tgz#fcc92cdb82ae248b034b172763d6dbadfb00a829"
+ integrity sha512-CXoOkhcdF3XVUnR2oNgCs2ljWfo/8zUjxL5RIhJW/UNLp/FSl+KpF8Jm5fbk8Y/XXVPGRAsv9OYfxyU/14N/pw==
+ dependencies:
+ "@atproto/common-web" "^0.3.1"
+ "@atproto/syntax" "^0.3.0"
+ iso-datestring-validator "^2.2.2"
+ multiformats "^9.9.0"
+ zod "^3.23.8"
+
"@atproto/oauth-provider@^0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@atproto/oauth-provider/-/oauth-provider-0.1.2.tgz#a576a4c7795c7938a994e76192c19a2e73ffcddf"
@@ -444,12 +465,12 @@
"@atproto/lexicon" "^0.4.1"
zod "^3.23.8"
-"@atproto/xrpc@^0.6.2":
- version "0.6.2"
- resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.2.tgz#634228a7e533de01bda2214837d11574fdadad55"
- integrity sha512-as/gb08xJb02HAGNrSQSumCe10WnOAcnM6bR6KMatQyQJuEu7OY6ZDSTM/4HfjjoxsNqdvPmbYuoUab1bKTNlA==
+"@atproto/xrpc@^0.6.3":
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.3.tgz#5942fc24644ad182b913af526efaa06a43d89478"
+ integrity sha512-S3tRvOdA9amPkKLll3rc4vphlDitLrkN5TwWh5Tu/jzk7mnobVVE3akYgICV9XCNHKjWM+IAPxFFI2qi+VW6nQ==
dependencies:
- "@atproto/lexicon" "^0.4.1"
+ "@atproto/lexicon" "^0.4.2"
zod "^3.23.8"
"@aws-crypto/crc32@3.0.0":
@@ -5551,13 +5572,6 @@
dependencies:
merge-options "^3.0.4"
-"@react-native-async-storage/async-storage@^1.15.15":
- version "1.19.2"
- resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.19.2.tgz#44f0af5927a04436b3f67aae67f028b888ff452c"
- integrity sha512-7jTQKbT3BdhFHQMnfElsLeeyVqNICv72lPKbeNHnNgLP9eH3+Yk1GFMWWb7A8qRMYianSmwmx1cYofNe9H9hLQ==
- dependencies:
- merge-options "^3.0.4"
-
"@react-native-async-storage/async-storage@^1.15.2":
version "1.22.0"
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.22.0.tgz#202a9afd15a5b829c39b709d0ca3942612441efc"
@@ -6101,79 +6115,6 @@
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69"
integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==
-"@segment/analytics-core@1.3.0":
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/@segment/analytics-core/-/analytics-core-1.3.0.tgz#677d1087af44d31266ddf60f72f93b5d5693c933"
- integrity sha512-ujScWZH49NK1hYlp2/EMw45nOPEh+pmTydAnR6gSkRNucZD4fuinvpPL03rmFCw8ibaMuKLAdgPJfQ0gkLKZ5A==
- dependencies:
- "@lukeed/uuid" "^2.0.0"
- dset "^3.1.2"
- tslib "^2.4.1"
-
-"@segment/analytics-next@^1.51.3":
- version "1.53.3"
- resolved "https://registry.yarnpkg.com/@segment/analytics-next/-/analytics-next-1.53.3.tgz#d6ae5c96648d309efae51a02d34136b15ed06cd5"
- integrity sha512-aAkVDm5HUaHtNrpQR0uXoGDkMKHbBK2ut+Quq0QGN4JNIpNX4j77vhCIZKkzNEResxH94d45Hm3uEFhQ0ycCBQ==
- dependencies:
- "@lukeed/uuid" "^2.0.0"
- "@segment/analytics-core" "1.3.0"
- "@segment/analytics.js-video-plugins" "^0.2.1"
- "@segment/facade" "^3.4.9"
- "@segment/tsub" "^2.0.0"
- dset "^3.1.2"
- js-cookie "3.0.1"
- node-fetch "^2.6.7"
- spark-md5 "^3.0.1"
- tslib "^2.4.1"
- unfetch "^4.1.0"
-
-"@segment/analytics-react-native@^2.10.1":
- version "2.16.0"
- resolved "https://registry.yarnpkg.com/@segment/analytics-react-native/-/analytics-react-native-2.16.0.tgz#91be02d9241043dfcbb1ced7ee79db2ba399c78b"
- integrity sha512-SkTw6bphIr4B4fcu5SDluS12mS08e4iRSt55F+PuM7vv4gxR9bp1/uOF24nJkV2ovBZ31pCDHd/cd+COi6xiMQ==
- dependencies:
- "@segment/sovran-react-native" "^1"
- "@segment/tsub" "^2"
- deepmerge "^4.2.2"
- js-base64 "^3.7.2"
- uuid "^9.0.0"
-
-"@segment/analytics-react@^1.0.0-rc1":
- version "1.0.0-rc1"
- resolved "https://registry.yarnpkg.com/@segment/analytics-react/-/analytics-react-1.0.0-rc1.tgz#4ef7683f291b26ce731fdccecd2151cd00facf5a"
- integrity sha512-b3bH8sYV2i4GfdqntuNTak6OlnQBF7LGsb11y6HZlIbYRNxW3kdr1HnLlR0HcaALLopMy9dZn/5PaeVKsaVezQ==
- dependencies:
- tslib "^2.4.0"
-
-"@segment/analytics.js-video-plugins@^0.2.1":
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/@segment/analytics.js-video-plugins/-/analytics.js-video-plugins-0.2.1.tgz#3596fa3887dcd9df5978dc566edf4a0aea2a9b1e"
- integrity sha512-lZwCyEXT4aaHBLNK433okEKdxGAuyrVmop4BpQqQSJuRz0DglPZgd9B/XjiiWs1UyOankg2aNYMN3VcS8t4eSQ==
- dependencies:
- unfetch "^3.1.1"
-
-"@segment/facade@^3.4.9":
- version "3.4.10"
- resolved "https://registry.yarnpkg.com/@segment/facade/-/facade-3.4.10.tgz#118fab29cf2250d3128f9b2a16d6ec76f86e3710"
- integrity sha512-xVQBbB/lNvk/u8+ey0kC/+g8pT3l0gCT8O2y9Z+StMMn3KAFAQ9w8xfgef67tJybktOKKU7pQGRPolRM1i1pdA==
- dependencies:
- "@segment/isodate-traverse" "^1.1.1"
- inherits "^2.0.4"
- new-date "^1.0.3"
- obj-case "0.2.1"
-
-"@segment/isodate-traverse@^1.1.1":
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/@segment/isodate-traverse/-/isodate-traverse-1.1.1.tgz#37e1a68b5e48a841260145f1be86d342995dfc64"
- integrity sha512-+G6e1SgAUkcq0EDMi+SRLfT48TNlLPF3QnSgFGVs0V9F3o3fq/woQ2rHFlW20W0yy5NnCUH0QGU3Am2rZy/E3w==
- dependencies:
- "@segment/isodate" "^1.0.3"
-
-"@segment/isodate@1.0.3", "@segment/isodate@^1.0.3":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/@segment/isodate/-/isodate-1.0.3.tgz#f44e8202d5edd277ce822785239474b2c9411d4a"
- integrity sha512-BtanDuvJqnACFkeeYje7pWULVv8RgZaqKHWwGFnL/g/TH/CcZjkIVTfGDp/MAxmilYHUkrX70SqwnYSTNEaN7A==
-
"@segment/loosely-validate-event@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz#87dfc979e5b4e7b82c5f1d8b722dfd5d77644681"
@@ -6182,35 +6123,6 @@
component-type "^1.2.1"
join-component "^1.1.0"
-"@segment/sovran-react-native@^0.4.5":
- version "0.4.5"
- resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-0.4.5.tgz#2ae057790623cfbd84eb15e4a3bb9835d108f815"
- integrity sha512-/dvud4hkszRNgTHdb7p822U+NXTuPxifqQzhcrfdjmFoXMafnzOUAi1KxeGV6Qdb73VAIxcnr/8hkTqdjZ3YLw==
- dependencies:
- "@react-native-async-storage/async-storage" "^1.15.15"
- ansi-regex "5.0.1"
- deepmerge "^4.2.2"
- shell-quote "1.7.3"
-
-"@segment/sovran-react-native@^1":
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/@segment/sovran-react-native/-/sovran-react-native-1.0.4.tgz#862c9c01c15f5ea81a77012c6e720ebd56153ccc"
- integrity sha512-oeIjspLOX9V+YmM7arZ+OXCyjooKdurnyVji+sj8OVkw+tUcXtvy+m9oC8E4yqazR/tghWCYE+tCriNx4jsVTw==
- dependencies:
- ansi-regex "5.0.1"
- deepmerge "^4.2.2"
- shell-quote "1.8.0"
-
-"@segment/tsub@^2", "@segment/tsub@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@segment/tsub/-/tsub-2.0.0.tgz#321e781a38fcd3720853f2da7574b523b447a296"
- integrity sha512-NzkBK8GwPsyQ74AceLjENbUoaFrObnzEKOX4ko2wZDuIyK+DnDm3B//8xZYI2LCKt+wUD55l6ygfjCoVs8RMWw==
- dependencies:
- "@stdlib/math-base-special-ldexp" "^0.0.5"
- dlv "^1.1.3"
- dset "^3.1.1"
- tiny-hashes "^1.0.1"
-
"@sentry-internal/feedback@7.119.0":
version "7.119.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.119.0.tgz#429b3ea0fd34e928d2e7de5dcbe9377272a3f221"
@@ -6889,854 +6801,6 @@
"@smithy/types" "^2.6.0"
tslib "^2.5.0"
-"@stdlib/array-float32@^0.0.x":
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/@stdlib/array-float32/-/array-float32-0.0.6.tgz#7a1c89db3c911183ec249fa32455abd9328cfa27"
- integrity sha512-QgKT5UaE92Rv7cxfn7wBKZAlwFFHPla8eXsMFsTGt5BiL4yUy36lwinPUh4hzybZ11rw1vifS3VAPuk6JP413Q==
- dependencies:
- "@stdlib/assert-has-float32array-support" "^0.0.x"
-
-"@stdlib/array-float64@^0.0.x":
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/@stdlib/array-float64/-/array-float64-0.0.6.tgz#02d1c80dd4c38a0f1ec150ddfefe706e148bfc10"
- integrity sha512-oE8y4a84LyBF1goX5//sU1mOjet8gLI0/6wucZcjg+j/yMmNV1xFu84Az9GOGmFSE6Ze6lirGOhfBeEWNNNaJg==
- dependencies:
- "@stdlib/assert-has-float64array-support" "^0.0.x"
-
-"@stdlib/array-uint16@^0.0.x":
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/@stdlib/array-uint16/-/array-uint16-0.0.6.tgz#2545110f0b611a1d55b01e52bd9160aaa67d6973"
- integrity sha512-/A8Tr0CqJ4XScIDRYQawosko8ha1Uy+50wsTgJhjUtXDpPRp7aUjmxvYkbe7Rm+ImYYbDQVix/uCiPAFQ8ed4Q==
- dependencies:
- "@stdlib/assert-has-uint16array-support" "^0.0.x"
-
-"@stdlib/array-uint32@^0.0.x":
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/@stdlib/array-uint32/-/array-uint32-0.0.6.tgz#5a923576475f539bfb2fda4721ea7bac6e993949"
- integrity sha512-2hFPK1Fg7obYPZWlGDjW9keiIB6lXaM9dKmJubg/ergLQCsJQJZpYsG6mMAfTJi4NT1UF4jTmgvyKD+yf0D9cA==
- dependencies:
- "@stdlib/assert-has-uint32array-support" "^0.0.x"
-
-"@stdlib/array-uint8@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/array-uint8/-/array-uint8-0.0.7.tgz#56f82b361da6bd9caad0e1d05e7f6ef20af9c895"
- integrity sha512-qYJQQfGKIcky6TzHFIGczZYTuVlut7oO+V8qUBs7BJC9TwikVnnOmb3hY3jToY4xaoi5p9OvgdJKPInhyIhzFg==
- dependencies:
- "@stdlib/assert-has-uint8array-support" "^0.0.x"
-
-"@stdlib/assert-has-float32array-support@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-has-float32array-support/-/assert-has-float32array-support-0.0.8.tgz#77371183726e26ca9e6f9db41d34543607074067"
- integrity sha512-Yrg7K6rBqwCzDWZ5bN0VWLS5dNUWcoSfUeU49vTERdUmZID06J069CDc07UUl8vfQWhFgBWGocH3rrpKm1hi9w==
- dependencies:
- "@stdlib/assert-is-float32array" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/constants-float64-pinf" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/assert-has-float64array-support@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-has-float64array-support/-/assert-has-float64array-support-0.0.8.tgz#4d154994d348f5d894f63b3fbb9d7a6e2e4e5311"
- integrity sha512-UVQcoeWqgMw9b8PnAmm/sgzFnuWkZcNhJoi7xyMjbiDV/SP1qLCrvi06mq86cqS3QOCma1fEayJdwgteoXyyuw==
- dependencies:
- "@stdlib/assert-is-float64array" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/assert-has-node-buffer-support@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-has-node-buffer-support/-/assert-has-node-buffer-support-0.0.8.tgz#5564d8e797c850f6ffc522b720eab1f6cba9c814"
- integrity sha512-fgI+hW4Yg4ciiv4xVKH+1rzdV7e5+6UKgMnFbc1XDXHcxLub3vOr8+H6eDECdAIfgYNA7X0Dxa/DgvX9dwDTAQ==
- dependencies:
- "@stdlib/assert-is-buffer" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/assert-has-own-property@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-has-own-property/-/assert-has-own-property-0.0.7.tgz#8b55b38e25db8366b028cb871905ac09c9c253fb"
- integrity sha512-3YHwSWiUqGlTLSwxAWxrqaD1PkgcJniGyotJeIt5X0tSNmSW0/c9RWroCImTUUB3zBkyBJ79MyU9Nf4Qgm59fQ==
-
-"@stdlib/assert-has-symbol-support@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-has-symbol-support/-/assert-has-symbol-support-0.0.8.tgz#8606b247f0d023f2a7a6aa8a6fe5e346aa802a8f"
- integrity sha512-PoQ9rk8DgDCuBEkOIzGGQmSnjtcdagnUIviaP5YskB45/TJHXseh4NASWME8FV77WFW9v/Wt1MzKFKMzpDFu4Q==
- dependencies:
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/assert-has-tostringtag-support@^0.0.x":
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-has-tostringtag-support/-/assert-has-tostringtag-support-0.0.9.tgz#1080ef0a4be576a72d19a819498719265456f170"
- integrity sha512-UTsqdkrnQ7eufuH5BeyWOJL3ska3u5nvDWKqw3onNNZ2mvdgkfoFD7wHutVGzAA2rkTsSJAMBHVwWLsm5SbKgw==
- dependencies:
- "@stdlib/assert-has-symbol-support" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/assert-has-uint16array-support@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-has-uint16array-support/-/assert-has-uint16array-support-0.0.8.tgz#083828067d55e3cc896796bc63cbf5726f67eecf"
- integrity sha512-vqFDn30YrtzD+BWnVqFhB130g3cUl2w5AdOxhIkRkXCDYAM5v7YwdNMJEON+D4jI8YB4D5pEYjqKweYaCq4nyg==
- dependencies:
- "@stdlib/assert-is-uint16array" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/constants-uint16-max" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/assert-has-uint32array-support@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-has-uint32array-support/-/assert-has-uint32array-support-0.0.8.tgz#a98c431fee45743088adb9602ef753c7552f9155"
- integrity sha512-tJtKuiFKwFSQQUfRXEReOVGXtfdo6+xlshSfwwNWXL1WPP2LrceoiUoQk7zMCMT6VdbXgGH92LDjVcPmSbH4Xw==
- dependencies:
- "@stdlib/assert-is-uint32array" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/constants-uint32-max" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/assert-has-uint8array-support@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-has-uint8array-support/-/assert-has-uint8array-support-0.0.8.tgz#9bed19de9834c3ced633551ed630982f0f424724"
- integrity sha512-ie4vGTbAS/5Py+LLjoSQi0nwtYBp+WKk20cMYCzilT0rCsBI/oez0RqHrkYYpmt4WaJL4eJqC+/vfQ5NsI7F5w==
- dependencies:
- "@stdlib/assert-is-uint8array" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/constants-uint8-max" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/assert-is-array@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-array/-/assert-is-array-0.0.7.tgz#7f30904f88a195d918c588540a6807d1ae639d79"
- integrity sha512-/o6KclsGkNcZ5hiROarsD9XUs6xQMb4lTwF6O71UHbKWTtomEF/jD0rxLvlvj0BiCxfKrReddEYd2CnhUyskMA==
- dependencies:
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-is-big-endian@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-big-endian/-/assert-is-big-endian-0.0.7.tgz#25ca21fb1ae0ec8201a716731497a2a15f315a7f"
- integrity sha512-BvutsX84F76YxaSIeS5ZQTl536lz+f+P7ew68T1jlFqxBhr4v7JVYFmuf24U040YuK1jwZ2sAq+bPh6T09apwQ==
- dependencies:
- "@stdlib/array-uint16" "^0.0.x"
- "@stdlib/array-uint8" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/assert-is-boolean@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-boolean/-/assert-is-boolean-0.0.8.tgz#6b38c2e799e4475d7647fb0e44519510e67080ce"
- integrity sha512-PRCpslMXSYqFMz1Yh4dG2K/WzqxTCtlKbgJQD2cIkAtXux4JbYiXCtepuoV7l4Wv1rm0a1eU8EqNPgnOmWajGw==
- dependencies:
- "@stdlib/assert-has-tostringtag-support" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-is-buffer@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-buffer/-/assert-is-buffer-0.0.8.tgz#633b98bc342979e9ed8ed71c3a0f1366782d1412"
- integrity sha512-SYmGwOXkzZVidqUyY1IIx6V6QnSL36v3Lcwj8Rvne/fuW0bU2OomsEBzYCFMvcNgtY71vOvgZ9VfH3OppvV6eA==
- dependencies:
- "@stdlib/assert-is-object-like" "^0.0.x"
-
-"@stdlib/assert-is-float32array@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-float32array/-/assert-is-float32array-0.0.8.tgz#a43f6106a2ef8797496ab85aaf6570715394654a"
- integrity sha512-Phk0Ze7Vj2/WLv5Wy8Oo7poZIDMSTiTrEnc1t4lBn3Svz2vfBXlvCufi/i5d93vc4IgpkdrOEwfry6nldABjNQ==
- dependencies:
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-is-float64array@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-float64array/-/assert-is-float64array-0.0.8.tgz#8c27204ae6cf309e16f0bbad1937f8aa06c2a812"
- integrity sha512-UC0Av36EEYIgqBbCIz1lj9g7qXxL5MqU1UrWun+n91lmxgdJ+Z77fHy75efJbJlXBf6HXhcYXECIsc0u3SzyDQ==
- dependencies:
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-is-function@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-function/-/assert-is-function-0.0.8.tgz#e4925022b7dd8c4a67e86769691d1d29ab159db9"
- integrity sha512-M55Dt2njp5tnY8oePdbkKBRIypny+LpCMFZhEjJIxjLE4rA6zSlHs1yRMqD4PmW+Wl9WTeEM1GYO4AQHl1HAjA==
- dependencies:
- "@stdlib/utils-type-of" "^0.0.x"
-
-"@stdlib/assert-is-little-endian@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-little-endian/-/assert-is-little-endian-0.0.7.tgz#f369fa3ec05c0e3a813738174b6821aacda6e323"
- integrity sha512-SPObC73xXfDXY0dOewXR0LDGN3p18HGzm+4K8azTj6wug0vpRV12eB3hbT28ybzRCa6TAKUjwM/xY7Am5QzIlA==
- dependencies:
- "@stdlib/array-uint16" "^0.0.x"
- "@stdlib/array-uint8" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/assert-is-number@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-number/-/assert-is-number-0.0.7.tgz#82b07cda4045bd0ecc846d3bc26d39dca7041c61"
- integrity sha512-mNV4boY1cUOmoWWfA2CkdEJfXA6YvhcTvwKC0Fzq+HoFFOuTK/scpTd9HanUyN6AGBlWA8IW+cQ1ZwOT3XMqag==
- dependencies:
- "@stdlib/assert-has-tostringtag-support" "^0.0.x"
- "@stdlib/number-ctor" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-is-object-like@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-object-like/-/assert-is-object-like-0.0.8.tgz#f6fc36eb7b612d650c6201d177214733426f0c56"
- integrity sha512-pe9selDPYAu/lYTFV5Rj4BStepgbzQCr36b/eC8EGSJh6gMgRXgHVv0R+EbdJ69KNkHvKKRjnWj0A/EmCwW+OA==
- dependencies:
- "@stdlib/assert-tools-array-function" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
-
-"@stdlib/assert-is-object@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-object/-/assert-is-object-0.0.8.tgz#0220dca73bc3df044fc43e73b02963d5ef7ae489"
- integrity sha512-ooPfXDp9c7w+GSqD2NBaZ/Du1JRJlctv+Abj2vRJDcDPyrnRTb1jmw+AuPgcW7Ca7op39JTbArI+RVHm/FPK+Q==
- dependencies:
- "@stdlib/assert-is-array" "^0.0.x"
-
-"@stdlib/assert-is-plain-object@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-plain-object/-/assert-is-plain-object-0.0.7.tgz#0c3679faf61b03023363f1ce30f8d00f8ed1c37b"
- integrity sha512-t/CEq2a083ajAgXgSa5tsH8l3kSoEqKRu1qUwniVLFYL4RGv3615CrpJUDQKVtEX5S/OKww5q0Byu3JidJ4C5w==
- dependencies:
- "@stdlib/assert-has-own-property" "^0.0.x"
- "@stdlib/assert-is-function" "^0.0.x"
- "@stdlib/assert-is-object" "^0.0.x"
- "@stdlib/utils-get-prototype-of" "^0.0.x"
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-is-regexp-string@^0.0.x":
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-regexp-string/-/assert-is-regexp-string-0.0.9.tgz#424f77b4aaa46a19f4b60ba4b671893a2e5df066"
- integrity sha512-FYRJJtH7XwXEf//X6UByUC0Eqd0ZYK5AC8or5g5m5efQrgr2lOaONHyDQ3Scj1A2D6QLIJKZc9XBM4uq5nOPXA==
- dependencies:
- "@stdlib/assert-is-string" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
- "@stdlib/process-read-stdin" "^0.0.x"
- "@stdlib/regexp-eol" "^0.0.x"
- "@stdlib/regexp-regexp" "^0.0.x"
- "@stdlib/streams-node-stdin" "^0.0.x"
-
-"@stdlib/assert-is-regexp@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-regexp/-/assert-is-regexp-0.0.7.tgz#430fe42417114e7ea01d21399a70ed9c4cbae867"
- integrity sha512-ty5qvLiqkDq6AibHlNJe0ZxDJ9Mg896qolmcHb69mzp64vrsORnPPOTzVapAq0bEUZbXoypeijypLPs9sCGBSQ==
- dependencies:
- "@stdlib/assert-has-tostringtag-support" "^0.0.x"
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-is-string@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-string/-/assert-is-string-0.0.8.tgz#b07e4a4cbd93b13d38fa5ebfaa281ccd6ae9e43f"
- integrity sha512-Uk+bR4cglGBbY0q7O7HimEJiW/DWnO1tSzr4iAGMxYgf+VM2PMYgI5e0TLy9jOSOzWon3YS39lc63eR3a9KqeQ==
- dependencies:
- "@stdlib/assert-has-tostringtag-support" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-is-uint16array@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-uint16array/-/assert-is-uint16array-0.0.8.tgz#770cc5d86906393d30d387a291e81df0a984fdfb"
- integrity sha512-M+qw7au+qglRXcXHjvoUZVLlGt1mPjuKudrVRto6KL4+tDsP2j+A89NDP3Fz8/XIUD+5jhj+65EOKHSMvDYnng==
- dependencies:
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-is-uint32array@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-uint32array/-/assert-is-uint32array-0.0.8.tgz#2a7f1265db25d728e3fc084f0f59be5f796efac5"
- integrity sha512-cnZi2DicYcplMnkJ3dBxBVKsRNFjzoGpmG9A6jXq4KH5rFl52SezGAXSVY9o5ZV7bQGaF5JLyCLp6n9Y74hFGg==
- dependencies:
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-is-uint8array@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-is-uint8array/-/assert-is-uint8array-0.0.8.tgz#4521054b5d3a2206b406cad7368e0a50eaee4dec"
- integrity sha512-8cqpDQtjnJAuVtRkNAktn45ixq0JHaGJxVsSiK79k7GRggvMI6QsbzO6OvcLnZ/LimD42FmgbLd13Yc2esDmZw==
- dependencies:
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/assert-tools-array-function@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/assert-tools-array-function/-/assert-tools-array-function-0.0.7.tgz#34e9e5a3fca62ea75da99fc9995ba845ba514988"
- integrity sha512-3lqkaCIBMSJ/IBHHk4NcCnk2NYU52tmwTYbbqhAmv7vim8rZPNmGfj3oWkzrCsyCsyTF7ooD+In2x+qTmUbCtQ==
- dependencies:
- "@stdlib/assert-is-array" "^0.0.x"
-
-"@stdlib/buffer-ctor@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/buffer-ctor/-/buffer-ctor-0.0.7.tgz#d05b7f4a6ef26defe6cdd41ca244a927b96c55ec"
- integrity sha512-4IyTSGijKUQ8+DYRaKnepf9spvKLZ+nrmZ+JrRcB3FrdTX/l9JDpggcUcC/Fe+A4KIZOnClfxLn6zfIlkCZHNA==
- dependencies:
- "@stdlib/assert-has-node-buffer-support" "^0.0.x"
-
-"@stdlib/buffer-from-string@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/buffer-from-string/-/buffer-from-string-0.0.8.tgz#0901a6e66c278db84836e483a7278502e2a33994"
- integrity sha512-Dws5ZbK2M9l4Bkn/ODHFm3lNZ8tWko+NYXqGS/UH/RIQv3PGp+1tXFUSvjwjDneM6ppjQVExzVedUH1ftABs9A==
- dependencies:
- "@stdlib/assert-is-function" "^0.0.x"
- "@stdlib/assert-is-string" "^0.0.x"
- "@stdlib/buffer-ctor" "^0.0.x"
- "@stdlib/string-format" "^0.0.x"
-
-"@stdlib/cli-ctor@^0.0.x":
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/@stdlib/cli-ctor/-/cli-ctor-0.0.3.tgz#5b0a6d253217556c778015eee6c14be903f82c2b"
- integrity sha512-0zCuZnzFyxj66GoF8AyIOhTX5/mgGczFvr6T9h4mXwegMZp8jBC/ZkOGMwmp+ODLBTvlcnnDNpNFkDDyR6/c2g==
- dependencies:
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
- "@stdlib/utils-noop" "^0.0.x"
- minimist "^1.2.0"
-
-"@stdlib/complex-float32@^0.0.7", "@stdlib/complex-float32@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/complex-float32/-/complex-float32-0.0.7.tgz#fb9a0c34254eaf3ed91c39983e19ef131fc18bc1"
- integrity sha512-POCtQcBZnPm4IrFmTujSaprR1fcOFr/MRw2Mt7INF4oed6b1nzeG647K+2tk1m4mMrMPiuXCdvwJod4kJ0SXxQ==
- dependencies:
- "@stdlib/assert-is-number" "^0.0.x"
- "@stdlib/number-float64-base-to-float32" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
- "@stdlib/utils-define-property" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/complex-float64@^0.0.8", "@stdlib/complex-float64@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/complex-float64/-/complex-float64-0.0.8.tgz#00ee3a0629d218a01b830a20406aea7d7aff6fb3"
- integrity sha512-lUJwsXtGEziOWAqCcnKnZT4fcVoRsl6t6ECaCJX45Z7lAc70yJLiwUieLWS5UXmyoADHuZyUXkxtI4oClfpnaw==
- dependencies:
- "@stdlib/assert-is-number" "^0.0.x"
- "@stdlib/complex-float32" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
- "@stdlib/utils-define-property" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/complex-reim@^0.0.6", "@stdlib/complex-reim@^0.0.x":
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/@stdlib/complex-reim/-/complex-reim-0.0.6.tgz#9657971e36f2a1f1930a21249c1934c8c5087efd"
- integrity sha512-28WXfPSIFMtHb0YgdatkGS4yxX5sPYea5MiNgqPv3E78+tFcg8JJG52NQ/MviWP2wsN9aBQAoCPeu8kXxSPdzA==
- dependencies:
- "@stdlib/array-float64" "^0.0.x"
- "@stdlib/complex-float64" "^0.0.x"
- "@stdlib/types" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/complex-reimf@^0.0.1", "@stdlib/complex-reimf@^0.0.x":
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/@stdlib/complex-reimf/-/complex-reimf-0.0.1.tgz#6797bc1bfb668a30511611f2544d0cff4d297775"
- integrity sha512-P9zu05ZW2i68Oppp3oHelP7Tk0D7tGBL0hGl1skJppr2vY9LltuNbeYI3C96tQe/7Enw/5GyAWgxoQI4cWccQA==
- dependencies:
- "@stdlib/array-float32" "^0.0.x"
- "@stdlib/complex-float32" "^0.0.x"
- "@stdlib/types" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-float64-exponent-bias@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-float64-exponent-bias/-/constants-float64-exponent-bias-0.0.8.tgz#f5069931a9a16d69e90a7c925739d7f64e4d725e"
- integrity sha512-IzBJQw9hYgWCki7VoC/zJxEA76Nmf8hmY+VkOWnJ8IyfgTXClgY8tfDGS1cc4l/hCOEllxGp9FRvVdn24A5tKQ==
- dependencies:
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-float64-high-word-abs-mask@^0.0.x":
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-float64-high-word-abs-mask/-/constants-float64-high-word-abs-mask-0.0.1.tgz#efb4cd3c13c301a3e9da83e8065dd2479e2c976e"
- integrity sha512-1vy8SUyMHFBwqUUVaZFA7r4/E3cMMRKSwsaa/EZ15w7Kmc01W/ZmaaTLevRcIdACcNgK+8i8813c8H7LScXNcQ==
- dependencies:
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-float64-high-word-exponent-mask@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-float64-high-word-exponent-mask/-/constants-float64-high-word-exponent-mask-0.0.8.tgz#c5671d462674ab09e48f25c2b3ca4d6d5cc4d875"
- integrity sha512-z28/EQERc0VG7N36bqdvtrRWjFc8600PKkwvl/nqx6TpKAzMXNw55BS1xT4C28Sa9Z7uBWeUj3UbIFedbkoyMw==
- dependencies:
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-float64-high-word-sign-mask@^0.0.x":
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-float64-high-word-sign-mask/-/constants-float64-high-word-sign-mask-0.0.1.tgz#d45bdec657199cdf522240d02ccd4b04040f58ca"
- integrity sha512-hmTr5caK1lh1m0eyaQqt2Vt3y+eEdAx57ndbADEbXhxC9qSGd0b4bLSzt/Xp4MYBYdQkHAE/BlkgUiRThswhCg==
- dependencies:
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-float64-max-base2-exponent-subnormal@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-float64-max-base2-exponent-subnormal/-/constants-float64-max-base2-exponent-subnormal-0.0.8.tgz#a24288c9c5e401eeb28d29f808c00a0bad481280"
- integrity sha512-YGBZykSiXFebznnJfWFDwhho2Q9xhUWOL+X0lZJ4ItfTTo40W6VHAyNYz98tT/gJECFype0seNzzo1nUxCE7jQ==
- dependencies:
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-float64-max-base2-exponent@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-float64-max-base2-exponent/-/constants-float64-max-base2-exponent-0.0.8.tgz#1d93dd829129a9e77133c5ad4f8c390c93f31bcb"
- integrity sha512-xBAOtso1eiy27GnTut2difuSdpsGxI8dJhXupw0UukGgvy/3CSsyNm+a1Suz/dhqK4tPOTe5QboIdNMw5IgXKQ==
- dependencies:
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-float64-min-base2-exponent-subnormal@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-float64-min-base2-exponent-subnormal/-/constants-float64-min-base2-exponent-subnormal-0.0.8.tgz#a5bd5a84ae2dec5694daccdaf2da54759185b727"
- integrity sha512-bt81nBus/91aEqGRQBenEFCyWNsf8uaxn4LN1NjgkvY92S1yVxXFlC65fJHsj9FTqvyZ+uj690/gdMKUDV3NjQ==
- dependencies:
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-float64-ninf@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-float64-ninf/-/constants-float64-ninf-0.0.8.tgz#4a83691d4d46503e2339fa3ec21d0440877b5bb7"
- integrity sha512-bn/uuzCne35OSLsQZJlNrkvU1/40spGTm22g1+ZI1LL19J8XJi/o4iupIHRXuLSTLFDBqMoJlUNphZlWQ4l8zw==
- dependencies:
- "@stdlib/number-ctor" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-float64-pinf@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-float64-pinf/-/constants-float64-pinf-0.0.8.tgz#ad3d5b267b142b0927363f6eda74c94b8c4be8bf"
- integrity sha512-I3R4rm2cemoMuiDph07eo5oWZ4ucUtpuK73qBJiJPDQKz8fSjSe4wJBAigq2AmWYdd7yJHsl5NJd8AgC6mP5Qw==
- dependencies:
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-float64-smallest-normal@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-float64-smallest-normal/-/constants-float64-smallest-normal-0.0.8.tgz#ea1b2335175480f7e846fdf5bbe378a31b7409b6"
- integrity sha512-Qwxpn5NA3RXf+mQcffCWRcsHSPTUQkalsz0+JDpblDszuz2XROcXkOdDr5LKgTAUPIXsjOgZzTsuRONENhsSEg==
- dependencies:
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/constants-uint16-max@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-uint16-max/-/constants-uint16-max-0.0.7.tgz#c20dbe90cf3825f03f5f44b9ee7e8cbada26f4f1"
- integrity sha512-7TPoku7SlskA67mAm7mykIAjeEnkQJemw1cnKZur0mT5W4ryvDR6iFfL9xBiByVnWYq/+ei7DHbOv6/2b2jizw==
-
-"@stdlib/constants-uint32-max@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-uint32-max/-/constants-uint32-max-0.0.7.tgz#60bda569b226120a5d2e01f3066da8e2d3b8e21a"
- integrity sha512-8+NK0ewqc1vnEZNqzwFJgFSy3S543Eft7i8WyW/ygkofiqEiLAsujvYMHzPAB8/3D+PYvjTSe37StSwRwvQ6uw==
-
-"@stdlib/constants-uint8-max@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/constants-uint8-max/-/constants-uint8-max-0.0.7.tgz#d50affeaeb6e67a0f39059a8f5122f3fd5ff4447"
- integrity sha512-fqV+xds4jgwFxwWu08b8xDuIoW6/D4/1dtEjZ1sXVeWR7nf0pjj1cHERq4kdkYxsvOGu+rjoR3MbjzpFc4fvSw==
-
-"@stdlib/fs-exists@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/fs-exists/-/fs-exists-0.0.8.tgz#391b2cee3e014a3b20266e5d047847f68ef82331"
- integrity sha512-mZktcCxiLmycCJefm1+jbMTYkmhK6Jk1ShFmUVqJvs+Ps9/2EEQXfPbdEniLoVz4HeHLlcX90JWobUEghOOnAQ==
- dependencies:
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
- "@stdlib/process-cwd" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
-
-"@stdlib/fs-read-file@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/fs-read-file/-/fs-read-file-0.0.8.tgz#2f12669fa6dd2d330fb5006a94dc8896f0aaa0e0"
- integrity sha512-pIZID/G91+q7ep4x9ECNC45+JT2j0+jdz/ZQVjCHiEwXCwshZPEvxcPQWb9bXo6coOY+zJyX5TwBIpXBxomWFg==
- dependencies:
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
-
-"@stdlib/fs-resolve-parent-path@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/fs-resolve-parent-path/-/fs-resolve-parent-path-0.0.8.tgz#628119952dfaae78afe3916dca856408a4f5c1eb"
- integrity sha512-ok1bTWsAziChibQE3u7EoXwbCQUDkFjjRAHSxh7WWE5JEYVJQg1F0o3bbjRr4D/wfYYPWLAt8AFIKBUDmWghpg==
- dependencies:
- "@stdlib/assert-has-own-property" "^0.0.x"
- "@stdlib/assert-is-function" "^0.0.x"
- "@stdlib/assert-is-plain-object" "^0.0.x"
- "@stdlib/assert-is-string" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-exists" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
- "@stdlib/process-cwd" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
-
-"@stdlib/math-base-assert-is-infinite@^0.0.x":
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/@stdlib/math-base-assert-is-infinite/-/math-base-assert-is-infinite-0.0.9.tgz#f9aa84e43a01ce4ccd976b20fbe7c508de884a90"
- integrity sha512-JuPDdmxd+AtPWPHu9uaLvTsnEPaZODZk+zpagziNbDKy8DRiU1cy+t+QEjB5WizZt0A5MkuxDTjZ/8/sG5GaYQ==
- dependencies:
- "@stdlib/constants-float64-ninf" "^0.0.x"
- "@stdlib/constants-float64-pinf" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/math-base-assert-is-nan@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/math-base-assert-is-nan/-/math-base-assert-is-nan-0.0.8.tgz#0cd6a546ca1e758251f04898fc906f6fce9e0f80"
- integrity sha512-m+gCVBxLFW8ZdAfdkATetYMvM7sPFoMKboacHjb1pe21jHQqVb+/4bhRSDg6S7HGX7/8/bSzEUm9zuF7vqK5rQ==
- dependencies:
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/math-base-napi-binary@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/math-base-napi-binary/-/math-base-napi-binary-0.0.8.tgz#b2754b021e40e3982c5f22b853ca50724b9eb8de"
- integrity sha512-B8d0HBPhfXefbdl/h0h5c+lM2sE+/U7Fb7hY/huVeoQtBtEx0Jbx/qKvPSVxMjmWCKfWlbPpbgKpN5GbFgLiAg==
- dependencies:
- "@stdlib/complex-float32" "^0.0.x"
- "@stdlib/complex-float64" "^0.0.x"
- "@stdlib/complex-reim" "^0.0.x"
- "@stdlib/complex-reimf" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/math-base-napi-unary@^0.0.x":
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/@stdlib/math-base-napi-unary/-/math-base-napi-unary-0.0.9.tgz#3a70fa64128aca7011c5a477110d2682d06c8ea8"
- integrity sha512-2WNKhjCygkGMp0RgjaD7wAHJTqPZmuVW7yPOc62Tnz2U+Ad8q/tcOcN+uvq2dtKsAGr1HDMIQxZ/XrrThMePyA==
- dependencies:
- "@stdlib/complex-float32" "^0.0.7"
- "@stdlib/complex-float64" "^0.0.8"
- "@stdlib/complex-reim" "^0.0.6"
- "@stdlib/complex-reimf" "^0.0.1"
- "@stdlib/utils-library-manifest" "^0.0.8"
-
-"@stdlib/math-base-special-abs@^0.0.x":
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/@stdlib/math-base-special-abs/-/math-base-special-abs-0.0.6.tgz#1e95dbeaf417ef779c6ab6beaf15f9f96cae6fa9"
- integrity sha512-FaaMUnYs2qIVN3kI5m/qNlBhDnjszhDOzEhxGEoQWR/k0XnxbCsTyjNesR2DkpiKuoAXAr9ojoDe2qBYdirWoQ==
- dependencies:
- "@stdlib/math-base-napi-unary" "^0.0.x"
- "@stdlib/number-float64-base-to-words" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/math-base-special-copysign@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/math-base-special-copysign/-/math-base-special-copysign-0.0.7.tgz#d2ead27ff93a84a46263ecfa5f9838a8ab809cfc"
- integrity sha512-7Br7oeuVJSBKG8BiSk/AIRFTBd2sbvHdV3HaqRj8tTZHX8BQomZ3Vj4Qsiz3kPyO4d6PpBLBTYlGTkSDlGOZJA==
- dependencies:
- "@stdlib/constants-float64-high-word-abs-mask" "^0.0.x"
- "@stdlib/constants-float64-high-word-sign-mask" "^0.0.x"
- "@stdlib/math-base-napi-binary" "^0.0.x"
- "@stdlib/number-float64-base-from-words" "^0.0.x"
- "@stdlib/number-float64-base-get-high-word" "^0.0.x"
- "@stdlib/number-float64-base-to-words" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/math-base-special-ldexp@^0.0.5":
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/@stdlib/math-base-special-ldexp/-/math-base-special-ldexp-0.0.5.tgz#df5a1fc0252a6d6cc5f12126af903e7391d78aad"
- integrity sha512-RLRsPpCdcJZMhwb4l4B/FsmGfEPEWAsik6KYUkUSSHb7ok/gZWt8LgVScxGMpJMpl5IV0v9qG4ZINVONKjX5KA==
- dependencies:
- "@stdlib/constants-float64-exponent-bias" "^0.0.x"
- "@stdlib/constants-float64-max-base2-exponent" "^0.0.x"
- "@stdlib/constants-float64-max-base2-exponent-subnormal" "^0.0.x"
- "@stdlib/constants-float64-min-base2-exponent-subnormal" "^0.0.x"
- "@stdlib/constants-float64-ninf" "^0.0.x"
- "@stdlib/constants-float64-pinf" "^0.0.x"
- "@stdlib/math-base-assert-is-infinite" "^0.0.x"
- "@stdlib/math-base-assert-is-nan" "^0.0.x"
- "@stdlib/math-base-special-copysign" "^0.0.x"
- "@stdlib/number-float64-base-exponent" "^0.0.x"
- "@stdlib/number-float64-base-from-words" "^0.0.x"
- "@stdlib/number-float64-base-normalize" "^0.0.x"
- "@stdlib/number-float64-base-to-words" "^0.0.x"
-
-"@stdlib/number-ctor@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/number-ctor/-/number-ctor-0.0.7.tgz#e97a66664639c9853b6c80bc7a15f7d67a2fc991"
- integrity sha512-kXNwKIfnb10Ro3RTclhAYqbE3DtIXax+qpu0z1/tZpI2vkmTfYDQLno2QJrzJsZZgdeFtXIws+edONN9kM34ow==
-
-"@stdlib/number-float64-base-exponent@^0.0.x":
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/@stdlib/number-float64-base-exponent/-/number-float64-base-exponent-0.0.6.tgz#cd4483d9faccaf7324c385da8e37d5ecf2f120b0"
- integrity sha512-wLXsG+cvynmapoffmj5hVNDH7BuHIGspBcTCdjPaD+tnqPDIm03qV5Z9YBhDh91BdOCuPZQ8Ovu2WBpX+ySeGg==
- dependencies:
- "@stdlib/constants-float64-exponent-bias" "^0.0.x"
- "@stdlib/constants-float64-high-word-exponent-mask" "^0.0.x"
- "@stdlib/number-float64-base-get-high-word" "^0.0.x"
-
-"@stdlib/number-float64-base-from-words@^0.0.x":
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/@stdlib/number-float64-base-from-words/-/number-float64-base-from-words-0.0.6.tgz#886e7dedd086e97d38b7e5fcf4c310467dbaac3c"
- integrity sha512-r0elnekypCN831aw9Gp8+08br8HHAqvqtc5uXaxEh3QYIgBD/QM5qSb3b7WSAQ0ZxJJKdoykupODWWBkWQTijg==
- dependencies:
- "@stdlib/array-float64" "^0.0.x"
- "@stdlib/array-uint32" "^0.0.x"
- "@stdlib/assert-is-little-endian" "^0.0.x"
- "@stdlib/number-float64-base-to-words" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/number-float64-base-get-high-word@^0.0.x":
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/@stdlib/number-float64-base-get-high-word/-/number-float64-base-get-high-word-0.0.6.tgz#4d3b8731a22017521cc7fc3ba57c7915b3e20fee"
- integrity sha512-jSFSYkgiG/IzDurbwrDKtWiaZeSEJK8iJIsNtbPG1vOIdQMRyw+t0bf3Kf3vuJu/+bnSTvYZLqpCO6wzT/ve9g==
- dependencies:
- "@stdlib/array-float64" "^0.0.x"
- "@stdlib/array-uint32" "^0.0.x"
- "@stdlib/assert-is-little-endian" "^0.0.x"
- "@stdlib/number-float64-base-to-words" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/number-float64-base-normalize@^0.0.x":
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/@stdlib/number-float64-base-normalize/-/number-float64-base-normalize-0.0.9.tgz#9e98eda47faa9ffc24bcf8161e587ae7b5f96a39"
- integrity sha512-+rm7RQJEj8zHkqYFE2a6DgNQSB5oKE/IydHAajgZl40YB91BoYRYf/ozs5/tTwfy2Fc04+tIpSfFtzDr4ZY19Q==
- dependencies:
- "@stdlib/constants-float64-smallest-normal" "^0.0.x"
- "@stdlib/math-base-assert-is-infinite" "^0.0.x"
- "@stdlib/math-base-assert-is-nan" "^0.0.x"
- "@stdlib/math-base-special-abs" "^0.0.x"
- "@stdlib/types" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/number-float64-base-to-float32@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/number-float64-base-to-float32/-/number-float64-base-to-float32-0.0.7.tgz#c7b82bb26cb7404017ede32cebe5864fd84c0e35"
- integrity sha512-PNUSi6+cqfFiu4vgFljUKMFY2O9PxI6+T+vqtIoh8cflf+PjSGj3v4QIlstK9+6qU40eGR5SHZyLTWdzmNqLTQ==
- dependencies:
- "@stdlib/array-float32" "^0.0.x"
-
-"@stdlib/number-float64-base-to-words@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/number-float64-base-to-words/-/number-float64-base-to-words-0.0.7.tgz#b3e88daa82334d90cf416f5387f503f66849545e"
- integrity sha512-7wsYuq+2MGp9rAkTnQ985rah7EJI9TfgHrYSSd4UIu4qIjoYmWIKEhIDgu7/69PfGrls18C3PxKg1pD/v7DQTg==
- dependencies:
- "@stdlib/array-float64" "^0.0.x"
- "@stdlib/array-uint32" "^0.0.x"
- "@stdlib/assert-is-little-endian" "^0.0.x"
- "@stdlib/os-byte-order" "^0.0.x"
- "@stdlib/os-float-word-order" "^0.0.x"
- "@stdlib/types" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/os-byte-order@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/os-byte-order/-/os-byte-order-0.0.7.tgz#131e02fb2ec67d172b9fe57caa629809fba11e7f"
- integrity sha512-rRJWjFM9lOSBiIX4zcay7BZsqYBLoE32Oz/Qfim8cv1cN1viS5D4d3DskRJcffw7zXDnG3oZAOw5yZS0FnlyUg==
- dependencies:
- "@stdlib/assert-is-big-endian" "^0.0.x"
- "@stdlib/assert-is-little-endian" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/os-float-word-order@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/os-float-word-order/-/os-float-word-order-0.0.7.tgz#067914ee1d1196b20d136c2eb55db6fd217833b4"
- integrity sha512-gXIcIZf+ENKP7E41bKflfXmPi+AIfjXW/oU+m8NbP3DQasqHaZa0z5758qvnbO8L1lRJb/MzLOkIY8Bx/0cWEA==
- dependencies:
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
- "@stdlib/os-byte-order" "^0.0.x"
- "@stdlib/utils-library-manifest" "^0.0.x"
-
-"@stdlib/process-cwd@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/process-cwd/-/process-cwd-0.0.8.tgz#5eef63fb75ffb5fc819659d2f450fa3ee2aa10bf"
- integrity sha512-GHINpJgSlKEo9ODDWTHp0/Zc/9C/qL92h5Mc0QlIFBXAoUjy6xT4FB2U16wCNZMG3eVOzt5+SjmCwvGH0Wbg3Q==
- dependencies:
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
-
-"@stdlib/process-read-stdin@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/process-read-stdin/-/process-read-stdin-0.0.7.tgz#684ad531759c6635715a67bdd8721fc249baa200"
- integrity sha512-nep9QZ5iDGrRtrZM2+pYAvyCiYG4HfO0/9+19BiLJepjgYq4GKeumPAQo22+1xawYDL7Zu62uWzYszaVZcXuyw==
- dependencies:
- "@stdlib/assert-is-function" "^0.0.x"
- "@stdlib/assert-is-string" "^0.0.x"
- "@stdlib/buffer-ctor" "^0.0.x"
- "@stdlib/buffer-from-string" "^0.0.x"
- "@stdlib/streams-node-stdin" "^0.0.x"
- "@stdlib/utils-next-tick" "^0.0.x"
-
-"@stdlib/regexp-eol@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/regexp-eol/-/regexp-eol-0.0.7.tgz#cf1667fdb5da1049c2c2f8d5c47dcbaede8650a4"
- integrity sha512-BTMpRWrmlnf1XCdTxOrb8o6caO2lmu/c80XSyhYCi1DoizVIZnqxOaN5yUJNCr50g28vQ47PpsT3Yo7J3SdlRA==
- dependencies:
- "@stdlib/assert-has-own-property" "^0.0.x"
- "@stdlib/assert-is-boolean" "^0.0.x"
- "@stdlib/assert-is-plain-object" "^0.0.x"
- "@stdlib/assert-is-string" "^0.0.x"
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
-
-"@stdlib/regexp-extended-length-path@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/regexp-extended-length-path/-/regexp-extended-length-path-0.0.7.tgz#7f76641c29895771e6249930e1863e7e137a62e0"
- integrity sha512-z6uqzMWq3WPDKbl4MIZJoNA5ZsYLQI9G3j2TIvhU8X2hnhlku8p4mvK9F+QmoVvgPxKliwNnx/DAl7ltutSDKw==
- dependencies:
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
-
-"@stdlib/regexp-function-name@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/regexp-function-name/-/regexp-function-name-0.0.7.tgz#e8dc6c7fe9276f0a8b4bc7f630a9e32ba9f37250"
- integrity sha512-MaiyFUUqkAUpUoz/9F6AMBuMQQfA9ssQfK16PugehLQh4ZtOXV1LhdY8e5Md7SuYl9IrvFVg1gSAVDysrv5ZMg==
- dependencies:
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
-
-"@stdlib/regexp-regexp@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/regexp-regexp/-/regexp-regexp-0.0.8.tgz#50221b52088cd427ef19fae6593977c1c3f77e87"
- integrity sha512-S5PZICPd/XRcn1dncVojxIDzJsHtEleuJHHD7ji3o981uPHR7zI2Iy9a1eV2u7+ABeUswbI1Yuix6fXJfcwV1w==
- dependencies:
- "@stdlib/utils-define-nonenumerable-read-only-property" "^0.0.x"
-
-"@stdlib/streams-node-stdin@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/streams-node-stdin/-/streams-node-stdin-0.0.7.tgz#65ff09a2140999702a1ad885e6505334d947428f"
- integrity sha512-gg4lgrjuoG3V/L29wNs32uADMCqepIcmoOFHJCTAhVe0GtHDLybUVnLljaPfdvmpPZmTvmusPQtIcscbyWvAyg==
-
-"@stdlib/string-base-format-interpolate@^0.0.x":
- version "0.0.4"
- resolved "https://registry.yarnpkg.com/@stdlib/string-base-format-interpolate/-/string-base-format-interpolate-0.0.4.tgz#297eeb23c76f745dcbb3d9dbd24e316773944538"
- integrity sha512-8FC8+/ey+P5hf1B50oXpXzRzoAgKI1rikpyKZ98Xmjd5rcbSq3NWYi8TqOF8mUHm9hVZ2CXWoNCtEe2wvMQPMg==
-
-"@stdlib/string-base-format-tokenize@^0.0.x":
- version "0.0.4"
- resolved "https://registry.yarnpkg.com/@stdlib/string-base-format-tokenize/-/string-base-format-tokenize-0.0.4.tgz#c1fc612ee0c0de5516dbf083e88c11d14748c30e"
- integrity sha512-+vMIkheqAhDeT/iF5hIQo95IMkt5IzC68eR3CxW1fhc48NMkKFE2UfN73ET8fmLuOanLo/5pO2E90c2G7PExow==
-
-"@stdlib/string-format@^0.0.x":
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/@stdlib/string-format/-/string-format-0.0.3.tgz#e916a7be14d83c83716f5d30b1b1af94c4e105b9"
- integrity sha512-1jiElUQXlI/tTkgRuzJi9jUz/EjrO9kzS8VWHD3g7gdc3ZpxlA5G9JrIiPXGw/qmZTi0H1pXl6KmX+xWQEQJAg==
- dependencies:
- "@stdlib/string-base-format-interpolate" "^0.0.x"
- "@stdlib/string-base-format-tokenize" "^0.0.x"
-
-"@stdlib/string-lowercase@^0.0.x":
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/@stdlib/string-lowercase/-/string-lowercase-0.0.9.tgz#487361a10364bd0d9b5ee44f5cc654c7da79b66d"
- integrity sha512-tXFFjbhIlDak4jbQyV1DhYiSTO8b1ozS2g/LELnsKUjIXECDKxGFyWYcz10KuyAWmFotHnCJdIm8/blm2CfDIA==
- dependencies:
- "@stdlib/assert-is-string" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
- "@stdlib/process-read-stdin" "^0.0.x"
- "@stdlib/streams-node-stdin" "^0.0.x"
- "@stdlib/string-format" "^0.0.x"
-
-"@stdlib/string-replace@^0.0.x":
- version "0.0.11"
- resolved "https://registry.yarnpkg.com/@stdlib/string-replace/-/string-replace-0.0.11.tgz#5e8790cdf4d9805ab78cc5798ab3d364dfbf5016"
- integrity sha512-F0MY4f9mRE5MSKpAUfL4HLbJMCbG6iUTtHAWnNeAXIvUX1XYIw/eItkA58R9kNvnr1l5B08bavnjrgTJGIKFFQ==
- dependencies:
- "@stdlib/assert-is-function" "^0.0.x"
- "@stdlib/assert-is-regexp" "^0.0.x"
- "@stdlib/assert-is-regexp-string" "^0.0.x"
- "@stdlib/assert-is-string" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
- "@stdlib/process-read-stdin" "^0.0.x"
- "@stdlib/regexp-eol" "^0.0.x"
- "@stdlib/streams-node-stdin" "^0.0.x"
- "@stdlib/string-format" "^0.0.x"
- "@stdlib/utils-escape-regexp-string" "^0.0.x"
- "@stdlib/utils-regexp-from-string" "^0.0.x"
-
-"@stdlib/types@^0.0.x":
- version "0.0.14"
- resolved "https://registry.yarnpkg.com/@stdlib/types/-/types-0.0.14.tgz#02d3aab7a9bfaeb86e34ab749772ea22f7b2f7e0"
- integrity sha512-AP3EI9/il/xkwUazcoY+SbjtxHRrheXgSbWZdEGD+rWpEgj6n2i63hp6hTOpAB5NipE0tJwinQlDGOuQ1lCaCw==
-
-"@stdlib/utils-constructor-name@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-constructor-name/-/utils-constructor-name-0.0.8.tgz#ef63d17466c555b58b348a0c1175cee6044b8848"
- integrity sha512-GXpyNZwjN8u3tyYjL2GgGfrsxwvfogUC3gg7L7NRZ1i86B6xmgfnJUYHYOUnSfB+R531ET7NUZlK52GxL7P82Q==
- dependencies:
- "@stdlib/assert-is-buffer" "^0.0.x"
- "@stdlib/regexp-function-name" "^0.0.x"
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/utils-convert-path@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-convert-path/-/utils-convert-path-0.0.8.tgz#a959d02103eee462777d222584e72eceef8c223b"
- integrity sha512-GNd8uIswrcJCctljMbmjtE4P4oOjhoUIfMvdkqfSrRLRY+ZqPB2xM+yI0MQFfUq/0Rnk/xtESlGSVLz9ZDtXfA==
- dependencies:
- "@stdlib/assert-is-string" "^0.0.x"
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-read-file" "^0.0.x"
- "@stdlib/process-read-stdin" "^0.0.x"
- "@stdlib/regexp-eol" "^0.0.x"
- "@stdlib/regexp-extended-length-path" "^0.0.x"
- "@stdlib/streams-node-stdin" "^0.0.x"
- "@stdlib/string-lowercase" "^0.0.x"
- "@stdlib/string-replace" "^0.0.x"
-
-"@stdlib/utils-define-nonenumerable-read-only-property@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-define-nonenumerable-read-only-property/-/utils-define-nonenumerable-read-only-property-0.0.7.tgz#ee74540c07bfc3d997ef6f8a1b2df267ea0c07ca"
- integrity sha512-c7dnHDYuS4Xn3XBRWIQBPcROTtP/4lkcFyq0FrQzjXUjimfMgHF7cuFIIob6qUTnU8SOzY9p0ydRR2QJreWE6g==
- dependencies:
- "@stdlib/types" "^0.0.x"
- "@stdlib/utils-define-property" "^0.0.x"
-
-"@stdlib/utils-define-property@^0.0.x":
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-define-property/-/utils-define-property-0.0.9.tgz#2f40ad66e28099714e3774f3585db80b13816e76"
- integrity sha512-pIzVvHJvVfU/Lt45WwUAcodlvSPDDSD4pIPc9WmIYi4vnEBA9U7yHtiNz2aTvfGmBMTaLYTVVFIXwkFp+QotMA==
- dependencies:
- "@stdlib/types" "^0.0.x"
-
-"@stdlib/utils-escape-regexp-string@^0.0.x":
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-escape-regexp-string/-/utils-escape-regexp-string-0.0.9.tgz#36f25d78b2899384ca6c97f4064a8b48edfedb6e"
- integrity sha512-E+9+UDzf2mlMLgb+zYrrPy2FpzbXh189dzBJY6OG+XZqEJAXcjWs7DURO5oGffkG39EG5KXeaQwDXUavcMDCIw==
- dependencies:
- "@stdlib/assert-is-string" "^0.0.x"
- "@stdlib/string-format" "^0.0.x"
-
-"@stdlib/utils-get-prototype-of@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-get-prototype-of/-/utils-get-prototype-of-0.0.7.tgz#f677132bcbc0ec89373376637148d364435918df"
- integrity sha512-fCUk9lrBO2ELrq+/OPJws1/hquI4FtwG0SzVRH6UJmJfwb1zoEFnjcwyDAy+HWNVmo3xeRLsrz6XjHrJwer9pg==
- dependencies:
- "@stdlib/assert-is-function" "^0.0.x"
- "@stdlib/utils-native-class" "^0.0.x"
-
-"@stdlib/utils-global@^0.0.x":
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-global/-/utils-global-0.0.7.tgz#0d99dcd11b72ad10b97dfb43536ff50436db6fb4"
- integrity sha512-BBNYBdDUz1X8Lhfw9nnnXczMv9GztzGpQ88J/6hnY7PHJ71av5d41YlijWeM9dhvWjnH9I7HNE3LL7R07yw0kA==
- dependencies:
- "@stdlib/assert-is-boolean" "^0.0.x"
-
-"@stdlib/utils-library-manifest@^0.0.8", "@stdlib/utils-library-manifest@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-library-manifest/-/utils-library-manifest-0.0.8.tgz#61d3ed283e82c8f14b7f952d82cfb8e47d036825"
- integrity sha512-IOQSp8skSRQn9wOyMRUX9Hi0j/P5v5TvD8DJWTqtE8Lhr8kVVluMBjHfvheoeKHxfWAbNHSVpkpFY/Bdh/SHgQ==
- dependencies:
- "@stdlib/cli-ctor" "^0.0.x"
- "@stdlib/fs-resolve-parent-path" "^0.0.x"
- "@stdlib/utils-convert-path" "^0.0.x"
- debug "^2.6.9"
- resolve "^1.1.7"
-
-"@stdlib/utils-native-class@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-native-class/-/utils-native-class-0.0.8.tgz#2e79de97f85d88a2bb5baa7a4528add71448d2be"
- integrity sha512-0Zl9me2V9rSrBw/N8o8/9XjmPUy8zEeoMM0sJmH3N6C9StDsYTjXIAMPGzYhMEWaWHvGeYyNteFK2yDOVGtC3w==
- dependencies:
- "@stdlib/assert-has-own-property" "^0.0.x"
- "@stdlib/assert-has-tostringtag-support" "^0.0.x"
-
-"@stdlib/utils-next-tick@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-next-tick/-/utils-next-tick-0.0.8.tgz#72345745ec3b3aa2cedda056338ed95daae9388c"
- integrity sha512-l+hPl7+CgLPxk/gcWOXRxX/lNyfqcFCqhzzV/ZMvFCYLY/wI9lcWO4xTQNMALY2rp+kiV+qiAiO9zcO+hewwUg==
-
-"@stdlib/utils-noop@^0.0.x":
- version "0.0.14"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-noop/-/utils-noop-0.0.14.tgz#8a2077fae0877c4c9e4c5f72f3c9284ca109d4c3"
- integrity sha512-A5faFEUfszMgd93RCyB+aWb62hQxgP+dZ/l9rIOwNWbIrCYNwSuL4z50lNJuatnwwU4BQ4EjQr+AmBsnvuLcyQ==
-
-"@stdlib/utils-regexp-from-string@^0.0.x":
- version "0.0.9"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-regexp-from-string/-/utils-regexp-from-string-0.0.9.tgz#fe4745a9a000157b365971c513fd7d4b2cb9ad6e"
- integrity sha512-3rN0Mcyiarl7V6dXRjFAUMacRwe0/sYX7ThKYurf0mZkMW9tjTP+ygak9xmL9AL0QQZtbrFFwWBrDO+38Vnavw==
- dependencies:
- "@stdlib/assert-is-string" "^0.0.x"
- "@stdlib/regexp-regexp" "^0.0.x"
- "@stdlib/string-format" "^0.0.x"
-
-"@stdlib/utils-type-of@^0.0.x":
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/@stdlib/utils-type-of/-/utils-type-of-0.0.8.tgz#c62ed3fcf629471fe80d83f44c4e325860109cbe"
- integrity sha512-b4xqdy3AnnB7NdmBBpoiI67X4vIRxvirjg3a8BfhM5jPr2k0njby1jAbG9dUxJvgAV6o32S4kjUgfIdjEYpTNQ==
- dependencies:
- "@stdlib/utils-constructor-name" "^0.0.x"
- "@stdlib/utils-global" "^0.0.x"
-
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@@ -9084,16 +8148,16 @@ ansi-html-community@^0.0.8:
resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
-ansi-regex@5.0.1, ansi-regex@^5.0.0, ansi-regex@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
- integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
ansi-regex@^4.0.0, ansi-regex@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed"
integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==
+ansi-regex@^5.0.0, ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
ansi-regex@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
@@ -11531,11 +10595,6 @@ dotenv@^16.4.4, dotenv@~16.4.5:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
-dset@^3.1.1, dset@^3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.2.tgz#89c436ca6450398396dc6538ea00abc0c54cd45a"
- integrity sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==
-
duplexer@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
@@ -12405,6 +11464,11 @@ expo-asset@11.0.0-canary-20240912-1059f85:
invariant "^2.2.4"
md5-file "^3.2.3"
+expo-blur@^13.0.2:
+ version "13.0.2"
+ resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-13.0.2.tgz#c2d179b19b13830db1d8b90c51373235f462e958"
+ integrity sha512-t2p7BChO3Reykued++QJRMZ/og6J3aXtSQ+bU31YcBeXhZLkHwjWEhiPKPnJka7J2/yTs4+jOCNDY0kCZmcE3w==
+
expo-build-properties@^1.0.0-canary-20240912-1059f85:
version "1.0.0-canary-20240912-1059f85"
resolved "https://registry.yarnpkg.com/expo-build-properties/-/expo-build-properties-1.0.0-canary-20240912-1059f85.tgz#c833731b3ca33329f593260e08f94bd9f6d5f0da"
@@ -15754,16 +14818,6 @@ jose@^5.2.0:
resolved "https://registry.yarnpkg.com/jose/-/jose-5.6.3.tgz#415688bc84875461c86dfe271ea6029112a23e27"
integrity sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==
-js-base64@^3.7.2:
- version "3.7.5"
- resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca"
- integrity sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==
-
-js-cookie@3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414"
- integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==
-
js-sha256@^0.10.1:
version "0.10.1"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.10.1.tgz#b40104ba1368e823fdd5f41b66b104b15a0da60d"
@@ -17105,13 +16159,6 @@ nested-error-stacks@~2.0.1:
resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz#d2cc9fc5235ddb371fc44d506234339c8e4b0a4b"
integrity sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==
-new-date@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/new-date/-/new-date-1.0.3.tgz#a5956086d3f5ed43d0b210d87a10219ccb7a2326"
- integrity sha512-0fsVvQPbo2I18DT2zVHpezmeeNYV2JaJSrseiHLc17GNOxJzUdx5mvSigPu8LtIfZSij5i1wXnXFspEs2CD6hA==
- dependencies:
- "@segment/isodate" "1.0.3"
-
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -17303,11 +16350,6 @@ ob1@0.80.4:
resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.80.4.tgz#a2e77e2dbe144c76356c834b994e147e19bb472f"
integrity sha512-Lku8OBpq+fhF1ZdKUjbPnTNeqG+3OL0psGAEVJ8zcUiCB5/DPGR/rm3kLcjKDylzC9Rfv540/7I08+oImzfrhw==
-obj-case@0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/obj-case/-/obj-case-0.2.1.tgz#13a554d04e5ca32dfd9d566451fd2b0e11007f1a"
- integrity sha512-PquYBBTy+Y6Ob/O2574XHhDtHJlV1cJHMCgW+rDRc9J5hhmRelJB3k5dTK/3cVmFVtzvAKuENeuLpoyTzMzkOg==
-
object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -20483,16 +19525,6 @@ shebang-regex@^3.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
-shell-quote@1.7.3:
- version "1.7.3"
- resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123"
- integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==
-
-shell-quote@1.8.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba"
- integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==
-
shell-quote@^1.6.1, shell-quote@^1.7.3:
version "1.8.1"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
@@ -20698,11 +19730,6 @@ sourcemap-codec@^1.4.8:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
-spark-md5@^3.0.1:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc"
- integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==
-
spdy-transport@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
@@ -21008,7 +20035,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -21022,13 +20049,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -21477,11 +20497,6 @@ thunky@^1.0.2:
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
-tiny-hashes@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/tiny-hashes/-/tiny-hashes-1.0.1.tgz#ddbe9060312ddb4efe0a174bb3a27e1331c425a1"
- integrity sha512-knIN5zj4fl7kW4EBU5sLP20DWUvi/rVouvJezV0UAym2DkQaqm365Nyc8F3QEiOvunNDMxR8UhcXd1d5g+Wg1g==
-
tippy.js@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
@@ -21643,7 +20658,7 @@ tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0:
+tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
@@ -22106,11 +21121,6 @@ uuid@^8.0.0, uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
-uuid@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
- integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
-
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"