Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea77ec4681 | |||
| 685a8a6348 | |||
| 477e5f4ecf | |||
| b2aef1f803 | |||
| dbebb48043 | |||
| a26b20b56c | |||
| 18b258d060 | |||
| 093454f5b2 | |||
| 297b0e93e5 | |||
| 678c345526 | |||
| 1a386c0d3b | |||
| bbc08bb7c6 | |||
| f20de35aad | |||
| b82e9facfa | |||
| 9e186dd2b5 | |||
| 32dfcc3a54 | |||
| c20f0319a7 | |||
| 61004b887b | |||
| 143d5f3b81 | |||
| 7341294df6 | |||
| 1cee8a48ce | |||
| 269105371b | |||
| 3049797665 | |||
| 475f4f9eb6 | |||
| 1380c5ac67 | |||
| 48e22c2adb | |||
| 094a877403 | |||
| bfc30da46c | |||
| aa01620880 | |||
| 950856fb09 | |||
| 01b7f9036e | |||
| 383ecbf8fd | |||
| df935b99e3 | |||
| 3c5e91385b | |||
| b0e77f0e13 | |||
| 55a0a5126d | |||
| 2b6924b9d3 | |||
| 92520166e5 | |||
| 34a2abfdd8 | |||
| 43f55a0e37 | |||
| afcd9beef4 | |||
| 191b0261cb | |||
| 6384cd9f7e | |||
| f82ec3380a |
@@ -1,10 +1,9 @@
|
||||
import {deleteAsync} from 'expo-file-system'
|
||||
import {createDownloadResumable, deleteAsync} from 'expo-file-system'
|
||||
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
import {
|
||||
downloadAndResize,
|
||||
DownloadAndResizeOpts,
|
||||
type DownloadAndResizeOpts,
|
||||
getResizedDimensions,
|
||||
} from '../../src/lib/media/manip'
|
||||
|
||||
@@ -32,11 +31,12 @@ describe('downloadAndResize', () => {
|
||||
})
|
||||
|
||||
it('should return resized image for valid URI and options', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
||||
info: jest.fn().mockReturnValue({status: 200}),
|
||||
flush: jest.fn(),
|
||||
const mockedFetch = createDownloadResumable as jest.Mock
|
||||
mockedFetch.mockReturnValue({
|
||||
cancelAsync: jest.fn(),
|
||||
downloadAsync: jest
|
||||
.fn()
|
||||
.mockResolvedValue({uri: 'file://resized-image.jpg'}),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
@@ -50,13 +50,12 @@ describe('downloadAndResize', () => {
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(result).toEqual(mockResizedImage)
|
||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
||||
fileCache: true,
|
||||
appendExt: 'jpeg',
|
||||
})
|
||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'https://example.com/image.jpg',
|
||||
expect(createDownloadResumable).toHaveBeenCalledWith(
|
||||
opts.uri,
|
||||
expect.anything(),
|
||||
{
|
||||
cache: true,
|
||||
},
|
||||
)
|
||||
|
||||
// First time it gets called is to get dimensions
|
||||
@@ -86,28 +85,6 @@ describe('downloadAndResize', () => {
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for non-200 response', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
||||
info: jest.fn().mockReturnValue({status: 400}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
uri: 'https://example.com/image',
|
||||
width: 100,
|
||||
height: 100,
|
||||
maxSize: 500000,
|
||||
mode: 'cover',
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(errorSpy).not.toHaveBeenCalled()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not downsize whenever dimensions are below the max dimensions', () => {
|
||||
const initialDimensionsOne = {
|
||||
width: 1200,
|
||||
|
||||
+1
-1
@@ -219,7 +219,7 @@ module.exports = function (_config) {
|
||||
compileSdkVersion: 35,
|
||||
targetSdkVersion: 35,
|
||||
buildToolsVersion: '35.0.0',
|
||||
newArchEnabled: false,
|
||||
newArchEnabled: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.63 3.225a1 1 0 0 1 1.337.068l3 3 .068.076a1 1 0 0 1-1.406 1.406l-.076-.068L13 10.414V16a1 1 0 1 1-2 0v-5.586l-1.293 1.293a1 1 0 1 1-1.414-1.414l3-3 .076-.068Z"/></svg>
|
||||
|
After Width: | Height: | Size: 371 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm0 3a1 1 0 0 1 1 1v3h3l.102.005a1 1 0 0 1 0 1.99L16 13h-3v3a1 1 0 1 1-2 0v-3H8a1 1 0 0 1 0-2h3V8a1 1 0 0 1 1-1Z"/></svg>
|
||||
|
After Width: | Height: | Size: 321 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M6 2a2.998 2.998 0 0 1 1 5.825V8a2 2 0 0 0 2 2h1.174c.412-1.165 1.52-2 2.826-2h5a3 3 0 1 1 0 6h-5a3 3 0 0 1-2.826-2H9a4 4 0 0 1-2-.537V16a2 2 0 0 0 2 2h1.174c.412-1.165 1.52-2 2.826-2h5a3 3 0 1 1 0 6h-5a3 3 0 0 1-2.826-2H9a4 4 0 0 1-4-4V7.825A2.998 2.998 0 0 1 6 2Zm7 16a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm0-8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5ZM6 4a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"/></svg>
|
||||
|
After Width: | Height: | Size: 478 B |
@@ -1,6 +1,6 @@
|
||||
import {Express} from 'express'
|
||||
import {type Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {default as createShortLink} from './createShortLink.js'
|
||||
import {default as health} from './health.js'
|
||||
import {default as redirect} from './redirect.js'
|
||||
|
||||
@@ -2,9 +2,9 @@ import assert from 'node:assert'
|
||||
|
||||
import {DAY, SECOND} from '@atproto/common'
|
||||
import escapeHTML from 'escape-html'
|
||||
import {Express} from 'express'
|
||||
import {type Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {handler} from './util.js'
|
||||
|
||||
const INTERNAL_IP_REGEX = new RegExp(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Express} from 'express'
|
||||
import {type Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {handler} from './util.js'
|
||||
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
|
||||
+2
-7
@@ -33,15 +33,10 @@ jest.mock('react-native-safe-area-context', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('rn-fetch-blob', () => ({
|
||||
config: jest.fn().mockReturnThis(),
|
||||
cancel: jest.fn(),
|
||||
fetch: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('expo-file-system', () => ({
|
||||
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
|
||||
deleteAsync: jest.fn(),
|
||||
createDownloadResumable: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('expo-image-manipulator', () => ({
|
||||
@@ -101,7 +96,7 @@ jest.mock('expo-modules-core', () => ({
|
||||
}
|
||||
}
|
||||
}),
|
||||
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
|
||||
requireNativeViewManager: jest.fn().mockImplementation(_ => {
|
||||
return () => null
|
||||
}),
|
||||
}))
|
||||
|
||||
+44
-41
@@ -69,12 +69,12 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.15.9",
|
||||
"@atproto/api": "^0.15.14",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@expo/html-elements": "^0.12.4",
|
||||
"@expo/html-elements": "^0.12.5",
|
||||
"@expo/webpack-config": "^19.0.1",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@floating-ui/react-dom": "^2.0.8",
|
||||
@@ -85,12 +85,12 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||
"@haileyok/bluesky-video": "0.2.6",
|
||||
"@haileyok/bluesky-video": "0.3.1",
|
||||
"@ipld/dag-cbor": "^9.2.0",
|
||||
"@lingui/react": "^4.14.1",
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
||||
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
|
||||
"@miblanchard/react-native-slider": "^2.6.0",
|
||||
"@mozzius/expo-dynamic-app-icon": "1.5.0",
|
||||
"@react-native-async-storage/async-storage": "2.1.2",
|
||||
"@react-native-menu/menu": "^1.2.3",
|
||||
"@react-native-picker/picker": "2.11.0",
|
||||
@@ -98,7 +98,7 @@
|
||||
"@react-navigation/drawer": "^7.3.12",
|
||||
"@react-navigation/native": "^7.1.9",
|
||||
"@react-navigation/native-stack": "^7.3.13",
|
||||
"@sentry/react-native": "~6.10.0",
|
||||
"@sentry/react-native": "~6.14.0",
|
||||
"@tanstack/query-async-storage-persister": "^5.25.0",
|
||||
"@tanstack/react-query": "^5.8.1",
|
||||
"@tanstack/react-query-persist-client": "^5.25.0",
|
||||
@@ -130,33 +130,33 @@
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "^53.0.5",
|
||||
"expo": "53.0.11",
|
||||
"expo-application": "~6.1.4",
|
||||
"expo-blur": "~14.1.4",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-build-properties": "~0.14.6",
|
||||
"expo-camera": "~16.1.6",
|
||||
"expo-camera": "~16.1.8",
|
||||
"expo-clipboard": "~7.1.4",
|
||||
"expo-dev-client": "~5.1.7",
|
||||
"expo-dev-client": "~5.2.0",
|
||||
"expo-device": "~7.1.4",
|
||||
"expo-file-system": "~18.1.8",
|
||||
"expo-font": "~13.3.0",
|
||||
"expo-file-system": "~18.1.10",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-haptics": "~14.1.4",
|
||||
"expo-image": "~2.1.6",
|
||||
"expo-image": "~2.2.1",
|
||||
"expo-image-crop-tool": "^0.1.8",
|
||||
"expo-image-manipulator": "~13.1.5",
|
||||
"expo-image-manipulator": "~13.1.7",
|
||||
"expo-image-picker": "~16.1.4",
|
||||
"expo-linear-gradient": "~14.1.4",
|
||||
"expo-linking": "~7.1.4",
|
||||
"expo-linear-gradient": "~14.1.5",
|
||||
"expo-linking": "~7.1.5",
|
||||
"expo-localization": "~16.1.5",
|
||||
"expo-media-library": "~17.1.6",
|
||||
"expo-notifications": "~0.31.1",
|
||||
"expo-screen-orientation": "~8.1.5",
|
||||
"expo-media-library": "~17.1.7",
|
||||
"expo-notifications": "~0.31.3",
|
||||
"expo-screen-orientation": "~8.1.7",
|
||||
"expo-sharing": "~13.1.5",
|
||||
"expo-splash-screen": "~0.30.8",
|
||||
"expo-system-ui": "~5.0.7",
|
||||
"expo-splash-screen": "~0.30.9",
|
||||
"expo-system-ui": "~5.0.8",
|
||||
"expo-task-manager": "~13.1.5",
|
||||
"expo-updates": "~0.28.12",
|
||||
"expo-video": "~2.1.8",
|
||||
"expo-updates": "~0.28.14",
|
||||
"expo-video": "~2.2.1",
|
||||
"expo-web-browser": "~14.1.6",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
@@ -182,35 +182,34 @@
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-is": "19",
|
||||
"react-keyed-flatten-children": "^5.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-compressor": "1.11.0",
|
||||
"react-native": "^0.79.3",
|
||||
"react-native-compressor": "^1.11.0",
|
||||
"react-native-date-picker": "^5.0.12",
|
||||
"react-native-drawer-layout": "^4.1.6",
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-gesture-handler": "2.25.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.17.1",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.7.1",
|
||||
"react-native-pager-view": "^6.7.1",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
"react-native-reanimated": "~3.17.5",
|
||||
"react-native-root-siblings": "^4.1.1",
|
||||
"react-native-root-siblings": "^5.0.1",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "^4.11.1",
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-svg": "15.12.0",
|
||||
"react-native-uitextview": "^1.4.0",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-view-shot": "^4.0.3",
|
||||
"react-native-web": "~0.20.0",
|
||||
"react-native-web-webview": "^1.0.2",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-webview": "^13.13.5",
|
||||
"react-remove-scroll-bar": "^2.3.8",
|
||||
"react-responsive": "^10.0.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"statsig-react-native-expo": "^4.6.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
@@ -227,8 +226,9 @@
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@react-native/eslint-config": "^0.79.2",
|
||||
"@react-native/typescript-config": "^0.79.2",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/eslint-config": "^0.79.3",
|
||||
"@react-native/typescript-config": "^0.79.3",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.2.0",
|
||||
@@ -238,8 +238,8 @@
|
||||
"@types/lodash.isequal": "^4.5.6",
|
||||
"@types/lodash.shuffle": "^4.2.7",
|
||||
"@types/psl": "^1.1.1",
|
||||
"@types/react": "^19.1.7",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/react-responsive": "^8.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
@@ -247,6 +247,7 @@
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
||||
"babel-preset-expo": "~13.1.11",
|
||||
"browserslist": "^4.25.0",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-plugin-bsky-internal": "link:./eslint",
|
||||
"eslint-plugin-ft-flow": "^2.0.3",
|
||||
@@ -260,7 +261,7 @@
|
||||
"husky": "^8.0.3",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~53.0.3",
|
||||
"jest-expo": "~53.0.7",
|
||||
"jest-junit": "^16.0.0",
|
||||
"lint-staged": "^13.2.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
@@ -275,10 +276,11 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@expo/image-utils": "0.6.3",
|
||||
"@react-native/babel-preset": "0.79.2",
|
||||
"@react-native/normalize-colors": "0.79.2",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/normalize-colors": "0.79.3",
|
||||
"@types/react": "^18",
|
||||
"**/expo-constants": "17.0.3",
|
||||
"**/expo-device": "7.0.1",
|
||||
"**/expo-device": "7.1.4",
|
||||
"**/zod": "3.23.8",
|
||||
"**/multiformats": "9.9.0"
|
||||
},
|
||||
@@ -358,7 +360,8 @@
|
||||
],
|
||||
"allowedUrls": [
|
||||
"https://codeload.github.com/bluesky-social/react-native-bottom-sheet/tar.gz/28a87d1bb55e10fc355fa1455545a30734995908",
|
||||
"https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4"
|
||||
"https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4",
|
||||
"https://codeload.github.com/mattermost/react-native-paste-input/tar.gz/f260447edc645a817ab1ba7b46d8341d84dba8e9"
|
||||
],
|
||||
"emptyHostname": false,
|
||||
"validatePackageNames": true,
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/@discord/bottom-sheet/src/hooks/useStableCallback.ts b/node_modules/@discord/bottom-sheet/src/hooks/useStableCallback.ts
|
||||
index 1c788ab..d30f330 100644
|
||||
--- a/node_modules/@discord/bottom-sheet/src/hooks/useStableCallback.ts
|
||||
+++ b/node_modules/@discord/bottom-sheet/src/hooks/useStableCallback.ts
|
||||
@@ -6,7 +6,7 @@ type Callback = (...args: any[]) => any;
|
||||
* https://gist.github.com/JakeCoxon/c7ebf6e6496f8468226fd36b596e1985
|
||||
*/
|
||||
export const useStableCallback = (callback: Callback) => {
|
||||
- const callbackRef = useRef<Callback>();
|
||||
+ const callbackRef = useRef<Callback>(undefined);
|
||||
const memoCallback = useCallback(
|
||||
(...args: any) => callbackRef.current && callbackRef.current(...args),
|
||||
[]
|
||||
+109
-2
@@ -114,7 +114,7 @@ index e916023..5049c33 100644
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
@@ -62,7 +92,6 @@
|
||||
@@ -62,7 +92,6 @@ - (void)setSmartPunctuation:(NSString *)smartPunctuation {
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
RCTDirectEventBlock onScroll = self.onScroll;
|
||||
@@ -122,7 +122,7 @@ index e916023..5049c33 100644
|
||||
if (onScroll) {
|
||||
CGPoint contentOffset = scrollView.contentOffset;
|
||||
CGSize contentSize = scrollView.contentSize;
|
||||
@@ -71,22 +100,22 @@
|
||||
@@ -71,22 +100,22 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
|
||||
onScroll(@{
|
||||
@"contentOffset": @{
|
||||
@@ -155,3 +155,110 @@ index e916023..5049c33 100644
|
||||
},
|
||||
@"zoomScale": @(scrollView.zoomScale ?: 1),
|
||||
});
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||
index dd50053..2ed7017 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||
@@ -122,8 +122,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
|
||||
const auto &newTextInputProps = static_cast<const PasteTextInputProps &>(*props);
|
||||
|
||||
// Traits:
|
||||
- if (newTextInputProps.traits.multiline != oldTextInputProps.traits.multiline) {
|
||||
- [self _setMultiline:newTextInputProps.traits.multiline];
|
||||
+ if (newTextInputProps.multiline != oldTextInputProps.multiline) {
|
||||
+ [self _setMultiline:newTextInputProps.multiline];
|
||||
}
|
||||
|
||||
if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) {
|
||||
@@ -421,7 +421,7 @@ - (void)textInputDidChangeSelection
|
||||
return;
|
||||
}
|
||||
const auto &props = static_cast<const PasteTextInputProps &>(*_props);
|
||||
- if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
|
||||
+ if (props.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
|
||||
[self textInputDidChange];
|
||||
_ignoreNextTextInputCall = YES;
|
||||
}
|
||||
@@ -708,11 +708,11 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe
|
||||
- (SubmitBehavior)getSubmitBehavior
|
||||
{
|
||||
const auto &props = static_cast<const PasteTextInputProps &>(*_props);
|
||||
- const SubmitBehavior submitBehaviorDefaultable = props.traits.submitBehavior;
|
||||
+ const SubmitBehavior submitBehaviorDefaultable = props.submitBehavior;
|
||||
|
||||
// We should always have a non-default `submitBehavior`, but in case we don't, set it based on multiline.
|
||||
if (submitBehaviorDefaultable == SubmitBehavior::Default) {
|
||||
- return props.traits.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
|
||||
+ return props.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
|
||||
}
|
||||
|
||||
return submitBehaviorDefaultable;
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||
index 29e094f..7ef519a 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||
@@ -22,8 +22,7 @@ PasteTextInputProps::PasteTextInputProps(
|
||||
const PropsParserContext &context,
|
||||
const PasteTextInputProps &sourceProps,
|
||||
const RawProps& rawProps)
|
||||
- : ViewProps(context, sourceProps, rawProps),
|
||||
- BaseTextProps(context, sourceProps, rawProps),
|
||||
+ : BaseTextInputProps(context, sourceProps, rawProps),
|
||||
traits(convertRawProp(context, rawProps, sourceProps.traits, {})),
|
||||
smartPunctuation(convertRawProp(context, rawProps, "smartPunctuation", sourceProps.smartPunctuation, {})),
|
||||
disableCopyPaste(convertRawProp(context, rawProps, "disableCopyPaste", sourceProps.disableCopyPaste, {false})),
|
||||
@@ -133,7 +132,7 @@ TextAttributes PasteTextInputProps::getEffectiveTextAttributes(Float fontSizeMul
|
||||
ParagraphAttributes PasteTextInputProps::getEffectiveParagraphAttributes() const {
|
||||
auto result = paragraphAttributes;
|
||||
|
||||
- if (!traits.multiline) {
|
||||
+ if (!multiline) {
|
||||
result.maximumNumberOfLines = 1;
|
||||
}
|
||||
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||
index 723d00c..31cfe66 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <react/renderer/components/iostextinput/conversions.h>
|
||||
#include <react/renderer/components/iostextinput/primitives.h>
|
||||
#include <react/renderer/components/text/BaseTextProps.h>
|
||||
+#include <react/renderer/components/textinput/BaseTextInputProps.h>
|
||||
#include <react/renderer/components/view/ViewProps.h>
|
||||
#include <react/renderer/core/Props.h>
|
||||
#include <react/renderer/core/PropsParserContext.h>
|
||||
@@ -25,7 +26,7 @@
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
-class PasteTextInputProps final : public ViewProps, public BaseTextProps {
|
||||
+class PasteTextInputProps final : public BaseTextInputProps {
|
||||
public:
|
||||
PasteTextInputProps() = default;
|
||||
PasteTextInputProps(const PropsParserContext& context, const PasteTextInputProps& sourceProps, const RawProps& rawProps);
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||
index 31e07e3..7f0ebfb 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||
@@ -91,20 +91,11 @@ void PasteTextInputShadowNode::updateStateIfNeeded(
|
||||
const auto& state = getStateData();
|
||||
|
||||
react_native_assert(textLayoutManager_);
|
||||
- react_native_assert(
|
||||
- (!state.layoutManager || state.layoutManager == textLayoutManager_) &&
|
||||
- "`StateData` refers to a different `TextLayoutManager`");
|
||||
-
|
||||
- if (state.reactTreeAttributedString == reactTreeAttributedString &&
|
||||
- state.layoutManager == textLayoutManager_) {
|
||||
- return;
|
||||
- }
|
||||
|
||||
auto newState = TextInputState{};
|
||||
newState.attributedStringBox = AttributedStringBox{reactTreeAttributedString};
|
||||
newState.paragraphAttributes = getConcreteProps().paragraphAttributes;
|
||||
newState.reactTreeAttributedString = reactTreeAttributedString;
|
||||
- newState.layoutManager = textLayoutManager_;
|
||||
newState.mostRecentEventCount = getConcreteProps().mostRecentEventCount;
|
||||
setStateData(std::move(newState));
|
||||
}
|
||||
@@ -1,3 +1,32 @@
|
||||
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||
index 914a249..0deac55 100644
|
||||
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||
@@ -19,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
@interface RCTPullToRefreshViewComponentView : RCTViewComponentView <RCTCustomPullToRefreshViewProtocol>
|
||||
|
||||
+- (void)beginRefreshingProgrammatically;
|
||||
+
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
index d029337..0f63ea3 100644
|
||||
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
@@ -1003,6 +1003,11 @@ - (void)_adjustForMaintainVisibleContentPosition
|
||||
}
|
||||
}
|
||||
|
||||
++ (BOOL)shouldBeRecycled
|
||||
+{
|
||||
+ return NO;
|
||||
+}
|
||||
+
|
||||
@end
|
||||
|
||||
Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
|
||||
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||
index e9b330f..ec5f58c 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||
@@ -15,7 +44,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont
|
||||
index 53bfd04..ff1b1ed 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
@@ -23,6 +23,7 @@
|
||||
@@ -23,6 +23,7 @@ @implementation RCTRefreshControl {
|
||||
UIColor *_titleColor;
|
||||
CGFloat _progressViewOffset;
|
||||
BOOL _hasMovedToWindow;
|
||||
@@ -23,7 +52,7 @@ index 53bfd04..ff1b1ed 100644
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
@@ -58,6 +59,12 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
@@ -58,6 +59,12 @@ - (void)layoutSubviews
|
||||
_isInitialRender = false;
|
||||
}
|
||||
|
||||
@@ -36,7 +65,7 @@ index 53bfd04..ff1b1ed 100644
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
[super didMoveToWindow];
|
||||
@@ -221,4 +228,50 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
@@ -221,4 +228,50 @@ - (void)refreshControlValueChanged
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +120,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont
|
||||
index 40aaf9c..1c60164 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
@@ -22,11 +22,12 @@ RCT_EXPORT_MODULE()
|
||||
@@ -22,11 +22,12 @@ - (UIView *)view
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
|
||||
@@ -105,15 +134,3 @@ index 40aaf9c..1c60164 100644
|
||||
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
|
||||
{
|
||||
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
index cd1e7eb..c1d0172 100644
|
||||
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
@@ -83,6 +83,7 @@ RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval)
|
||||
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
||||
+RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(verticalScrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
|
||||
+4
-4
@@ -58,9 +58,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {Provider as UnstablePostSourceProvider} from '#/state/unstable-post-source'
|
||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Shell} from '#/view/shell'
|
||||
import {ThemeProvider as Alf} from '#/alf'
|
||||
@@ -70,9 +68,11 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
if (isIOS) {
|
||||
@@ -151,7 +151,7 @@ function InnerApp() {
|
||||
<MutedThreadsProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceAccountManager>
|
||||
<UnstablePostSourceProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<IntentDialogProvider>
|
||||
@@ -160,7 +160,7 @@ function InnerApp() {
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</GestureHandlerRootView>
|
||||
</UnstablePostSourceProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</ServiceAccountManager>
|
||||
</ProgressGuideProvider>
|
||||
</MutedThreadsProvider>
|
||||
|
||||
+5
-5
@@ -48,9 +48,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {Provider as UnstablePostSourceProvider} from '#/state/unstable-post-source'
|
||||
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {ToastContainer} from '#/view/com/util/Toast.web'
|
||||
import {Shell} from '#/view/shell/index'
|
||||
@@ -61,7 +58,10 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
||||
|
||||
/**
|
||||
* Begin geolocation ASAP
|
||||
@@ -132,12 +132,12 @@ function InnerApp() {
|
||||
<SafeAreaProvider>
|
||||
<ProgressGuideProvider>
|
||||
<ServiceConfigProvider>
|
||||
<UnstablePostSourceProvider>
|
||||
<HideBottomBarBorderProvider>
|
||||
<IntentDialogProvider>
|
||||
<Shell />
|
||||
<NuxDialogs />
|
||||
</IntentDialogProvider>
|
||||
</UnstablePostSourceProvider>
|
||||
</HideBottomBarBorderProvider>
|
||||
</ServiceConfigProvider>
|
||||
</ProgressGuideProvider>
|
||||
</SafeAreaProvider>
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import {type JSX, useCallback, useRef} from 'react'
|
||||
import * as React from 'react'
|
||||
import {i18n, type MessageDescriptor} from '@lingui/core'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {
|
||||
@@ -484,7 +484,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
||||
* in 3 distinct tab-stacks with a different root screen on each.
|
||||
*/
|
||||
function TabsNavigator() {
|
||||
const tabBar = useCallback(
|
||||
const tabBar = React.useCallback(
|
||||
(props: JSX.IntrinsicAttributes & BottomTabBarProps) => (
|
||||
<BottomBar {...props} />
|
||||
),
|
||||
@@ -712,7 +712,7 @@ const LINKING = {
|
||||
function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
const theme = useColorSchemeStyle(DefaultTheme, DarkTheme)
|
||||
const {currentAccount} = useSession()
|
||||
const prevLoggedRouteName = useRef<string | undefined>(undefined)
|
||||
const prevLoggedRouteName = React.useRef<string | undefined>(undefined)
|
||||
const emailDialogControl = useEmailDialogControl()
|
||||
|
||||
function onReady() {
|
||||
|
||||
@@ -1051,4 +1051,8 @@ export const atoms = {
|
||||
transform: [],
|
||||
},
|
||||
}) as {transform: Exclude<ViewStyle['transform'], string | undefined>},
|
||||
|
||||
pointer: web({
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
} as const
|
||||
|
||||
@@ -3,6 +3,7 @@ import {type TextProps as RNTextProps} from 'react-native'
|
||||
import {type StyleProp, type TextStyle} from 'react-native'
|
||||
import {UITextView} from 'react-native-uitextview'
|
||||
import createEmojiRegex from 'emoji-regex'
|
||||
import type React from 'react'
|
||||
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import {jest} from '@jest/globals'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {transparentifyColor} from '../colorGeneration'
|
||||
|
||||
jest.mock('#/logger', () => ({
|
||||
logger: {warn: jest.fn()},
|
||||
}))
|
||||
|
||||
describe('transparentifyColor', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('converts hsl() to hsla()', () => {
|
||||
const result = transparentifyColor('hsl(120 100% 50%)', 0.5)
|
||||
expect(result).toBe('hsla(120 100% 50%, 0.5)')
|
||||
})
|
||||
|
||||
it('converts hsl() to hsla() - fully transparent', () => {
|
||||
const result = transparentifyColor('hsl(120 100% 50%)', 0)
|
||||
expect(result).toBe('hsla(120 100% 50%, 0)')
|
||||
})
|
||||
|
||||
it('converts rgb() to rgba()', () => {
|
||||
const result = transparentifyColor('rgb(255 0 0)', 0.75)
|
||||
expect(result).toBe('rgba(255 0 0, 0.75)')
|
||||
})
|
||||
|
||||
it('expands 3-digit hex and appends alpha channel', () => {
|
||||
const result = transparentifyColor('#abc', 0.4)
|
||||
expect(result).toBe('#aabbcc66')
|
||||
})
|
||||
|
||||
it('appends alpha to 6-digit hex', () => {
|
||||
const result = transparentifyColor('#aabbcc', 0.4)
|
||||
expect(result).toBe('#aabbcc66')
|
||||
})
|
||||
|
||||
it('returns the original string and warns for unsupported formats', () => {
|
||||
const unsupported = 'blue'
|
||||
const result = transparentifyColor(unsupported, 0.5)
|
||||
expect(result).toBe(unsupported)
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
`Could not make '${unsupported}' transparent`,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,5 @@
|
||||
import {logger} from '#/logger'
|
||||
|
||||
export const BLUE_HUE = 211
|
||||
export const RED_HUE = 346
|
||||
export const GREEN_HUE = 152
|
||||
@@ -19,3 +21,29 @@ export function generateScale(start: number, end: number) {
|
||||
export const defaultScale = generateScale(6, 100)
|
||||
// dim shifted 6% lighter
|
||||
export const dimScale = generateScale(12, 100)
|
||||
|
||||
export function transparentifyColor(color: string, alpha: number) {
|
||||
if (color.startsWith('hsl(')) {
|
||||
return 'hsla(' + color.slice('hsl('.length, -1) + `, ${alpha})`
|
||||
} else if (color.startsWith('rgb(')) {
|
||||
return 'rgba(' + color.slice('rgb('.length, -1) + `, ${alpha})`
|
||||
} else if (color.startsWith('#')) {
|
||||
if (color.length === 7) {
|
||||
const alphaHex = Math.round(alpha * 255).toString(16)
|
||||
// Per MDN: If there is only one number, it is duplicated: e means ee
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color
|
||||
return color.slice(0, 7) + alphaHex.padStart(2, alphaHex)
|
||||
} else if (color.length === 4) {
|
||||
// convert to 6-digit hex before adding alpha
|
||||
const [r, g, b] = color.slice(1).split('')
|
||||
const alphaHex = Math.round(alpha * 255).toString(16)
|
||||
return `#${r.repeat(2)}${g.repeat(2)}${b.repeat(2)}${alphaHex.padStart(
|
||||
2,
|
||||
alphaHex,
|
||||
)}`
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Could not make '${color}' transparent`)
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {Theme} from '../types'
|
||||
import {type Theme} from '../types'
|
||||
|
||||
export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
|
||||
if (isAndroid) {
|
||||
|
||||
@@ -65,8 +65,8 @@ export type ButtonState = {
|
||||
export type ButtonContext = VariantProps & ButtonState
|
||||
|
||||
type NonTextElements =
|
||||
| React.ReactElement<any>
|
||||
| Iterable<React.ReactElement<any> | null | undefined | boolean>
|
||||
| React.ReactElement
|
||||
| Iterable<React.ReactElement | null | undefined | boolean>
|
||||
|
||||
export type ButtonProps = Pick<
|
||||
PressableProps,
|
||||
|
||||
@@ -2,7 +2,7 @@ import {Pressable} from 'react-native'
|
||||
import Animated, {
|
||||
Extrapolation,
|
||||
interpolate,
|
||||
SharedValue,
|
||||
type SharedValue,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
@@ -119,8 +119,7 @@ export function Root({children}: {children: React.ReactNode}) {
|
||||
const hoverablesSV = useSharedValue<
|
||||
Record<string, {id: string; rect: Measurement}>
|
||||
>({})
|
||||
const syncHoverablesThrottleRef =
|
||||
useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
const syncHoverablesThrottleRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const [hoveredMenuItem, setHoveredMenuItem] = useState<string | null>(null)
|
||||
|
||||
const onHoverableTouchUp = useCallback((id: string) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {type SharedValue} from 'react-native-reanimated'
|
||||
import type React from 'react'
|
||||
|
||||
import type * as Dialog from '#/components/Dialog'
|
||||
import {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import type React from 'react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import {
|
||||
type AccessibilityProps,
|
||||
type GestureResponderEvent,
|
||||
type ScrollViewProps,
|
||||
import React from 'react'
|
||||
import type {
|
||||
AccessibilityProps,
|
||||
GestureResponderEvent,
|
||||
ScrollViewProps,
|
||||
} from 'react-native'
|
||||
import {type ViewStyle} from 'react-native'
|
||||
import {type StyleProp} from 'react-native'
|
||||
import {ViewStyle} from 'react-native'
|
||||
import {StyleProp} from 'react-native'
|
||||
|
||||
import {type ViewStyleProp} from '#/alf'
|
||||
import {type BottomSheetViewProps} from '../../../modules/bottom-sheet'
|
||||
import {type BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
|
||||
import {ViewStyleProp} from '#/alf'
|
||||
import {BottomSheetViewProps} from '../../../modules/bottom-sheet'
|
||||
import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
|
||||
|
||||
type A11yProps = Required<AccessibilityProps>
|
||||
|
||||
@@ -33,7 +34,7 @@ export type DialogControlRefProps = {
|
||||
*/
|
||||
export type DialogControlProps = DialogControlRefProps & {
|
||||
id: string
|
||||
ref: React.RefObject<DialogControlRefProps | null>
|
||||
ref: React.RefObject<DialogControlRefProps>
|
||||
isOpen?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {SeenPost} from '#/state/userActionHistory'
|
||||
import {type SeenPost} from '#/state/userActionHistory'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
||||
import {
|
||||
atoms as a,
|
||||
useBreakpoints,
|
||||
useTheme,
|
||||
type ViewStyleProp,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
||||
@@ -27,7 +33,7 @@ import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/P
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {ProgressGuideList} from './ProgressGuide/List'
|
||||
|
||||
const MOBILE_CARD_WIDTH = 300
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import {createContext, useCallback, useContext} from 'react'
|
||||
import {GestureResponderEvent, Keyboard, View} from 'react-native'
|
||||
import {type GestureResponderEvent, Keyboard, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
import {
|
||||
atoms as a,
|
||||
platform,
|
||||
TextStyleProp,
|
||||
type TextStyleProp,
|
||||
useBreakpoints,
|
||||
useGutters,
|
||||
useLayoutBreakpoints,
|
||||
useTheme,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
|
||||
import {Button, ButtonIcon, type ButtonProps} from '#/components/Button'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||
import {
|
||||
type LinkProps as RNLinkProps,
|
||||
StackActions,
|
||||
useLinkBuilder,
|
||||
} from '@react-navigation/native'
|
||||
|
||||
import {BSKY_DOWNLOAD_URL} from '#/lib/constants'
|
||||
@@ -95,20 +94,19 @@ export function useLink({
|
||||
shouldProxy?: boolean
|
||||
}) {
|
||||
const navigation = useNavigationDeduped()
|
||||
const {buildHref} = useLinkBuilder()
|
||||
const href = useMemo(() => {
|
||||
return typeof to === 'string'
|
||||
? convertBskyAppUrlIfNeeded(sanitizeUrl(to))
|
||||
: to.screen
|
||||
? buildHref(to.screen, to.params)
|
||||
? router.matchName(to.screen)?.build(to.params)
|
||||
: to.href
|
||||
? convertBskyAppUrlIfNeeded(sanitizeUrl(to.href))
|
||||
: undefined
|
||||
}, [to, buildHref])
|
||||
}, [to])
|
||||
|
||||
if (!href) {
|
||||
throw new Error(
|
||||
'Link `to` prop must be a string or an object with `screen` and `params` properties',
|
||||
'Could not resolve screen. Link `to` prop must be a string or an object with `screen` and `params` properties',
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import type {ContextType, ItemContextType} from '#/components/Menu/types'
|
||||
import {type ContextType, type ItemContextType} from '#/components/Menu/types'
|
||||
|
||||
export const Context = React.createContext<ContextType | null>(null)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
type Component = React.ReactElement<any>
|
||||
type Component = React.ReactElement
|
||||
|
||||
type ContextType = {
|
||||
outlet: Component | null
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
|
||||
import {Fill} from '#/components/Fill'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
|
||||
export function ExternalGifEmbed({
|
||||
export function ExternalGif({
|
||||
link,
|
||||
params,
|
||||
}: {
|
||||
+1
-1
@@ -25,12 +25,12 @@ import {NavigationProp} from '#/lib/routes/types'
|
||||
import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
|
||||
import {Fill} from '#/components/Fill'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
|
||||
interface ShouldStartLoadRequest {
|
||||
url: string
|
||||
+5
-5
@@ -12,16 +12,16 @@ import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
|
||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||
import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed'
|
||||
import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed'
|
||||
import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ExternalGif} from './ExternalGif'
|
||||
import {ExternalPlayer} from './ExternalPlayer'
|
||||
import {GifEmbed} from './Gif'
|
||||
|
||||
export const ExternalLinkEmbed = ({
|
||||
export const ExternalEmbed = ({
|
||||
link,
|
||||
onOpen,
|
||||
style,
|
||||
@@ -106,7 +106,7 @@ export const ExternalLinkEmbed = ({
|
||||
) : undefined}
|
||||
|
||||
{embedPlayerParams?.isGif ? (
|
||||
<ExternalGifEmbed link={link} params={embedPlayerParams} />
|
||||
<ExternalGif link={link} params={embedPlayerParams} />
|
||||
) : embedPlayerParams ? (
|
||||
<ExternalPlayer link={link} params={embedPlayerParams} />
|
||||
) : undefined}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet} from 'react-native'
|
||||
import {moderateFeedGenerator} from '@atproto/api'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {type EmbedType} from '#/types/bsky/post'
|
||||
import {type CommonProps} from './types'
|
||||
|
||||
export function FeedEmbed({
|
||||
embed,
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'feed'>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<FeedSourceCard
|
||||
feedUri={embed.view.uri}
|
||||
style={[pal.view, pal.border, styles.customFeedOuter]}
|
||||
showLikes
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ModeratedFeedEmbed({
|
||||
embed,
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'feed'>
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const moderation = React.useMemo(() => {
|
||||
return moderationOpts
|
||||
? moderateFeedGenerator(embed.view, moderationOpts)
|
||||
: undefined
|
||||
}, [embed.view, moderationOpts])
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentList')}>
|
||||
<FeedEmbed embed={embed} />
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
customFeedOuter: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
import {InteractionManager, View} from 'react-native'
|
||||
import {
|
||||
type AnimatedRef,
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
} from 'react-native-reanimated'
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
import {useLightboxControls} from '#/state/lightbox'
|
||||
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
||||
import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {type EmbedType} from '#/types/bsky/post'
|
||||
import {type CommonProps} from './types'
|
||||
|
||||
export function ImageEmbed({
|
||||
embed,
|
||||
...rest
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'images'>
|
||||
}) {
|
||||
const {openLightbox} = useLightboxControls()
|
||||
const {images} = embed.view
|
||||
|
||||
if (images.length > 0) {
|
||||
const items = images.map(img => ({
|
||||
uri: img.fullsize,
|
||||
thumbUri: img.thumb,
|
||||
alt: img.alt,
|
||||
dimensions: img.aspectRatio ?? null,
|
||||
}))
|
||||
const _openLightbox = (
|
||||
index: number,
|
||||
thumbRects: (MeasuredDimensions | null)[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => {
|
||||
openLightbox({
|
||||
images: items.map((item, i) => ({
|
||||
...item,
|
||||
thumbRect: thumbRects[i] ?? null,
|
||||
thumbDimensions: fetchedDims[i] ?? null,
|
||||
type: 'image',
|
||||
})),
|
||||
index,
|
||||
})
|
||||
}
|
||||
const onPress = (
|
||||
index: number,
|
||||
refs: AnimatedRef<any>[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => {
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
const rects: (MeasuredDimensions | null)[] = []
|
||||
for (const r of refs) {
|
||||
rects.push(measure(r))
|
||||
}
|
||||
runOnJS(_openLightbox)(index, rects, fetchedDims)
|
||||
})()
|
||||
}
|
||||
const onPressIn = (_: number) => {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
Image.prefetch(items.map(i => i.uri))
|
||||
})
|
||||
}
|
||||
|
||||
if (images.length === 1) {
|
||||
const image = images[0]
|
||||
return (
|
||||
<View style={[a.mt_sm, rest.style]}>
|
||||
<AutoSizedImage
|
||||
crop={
|
||||
rest.viewContext === PostEmbedViewContext.ThreadHighlighted
|
||||
? 'none'
|
||||
: rest.viewContext ===
|
||||
PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
? 'square'
|
||||
: 'constrained'
|
||||
}
|
||||
image={image}
|
||||
onPress={(containerRef, dims) => onPress(0, [containerRef], [dims])}
|
||||
onPressIn={() => onPressIn(0)}
|
||||
hideBadge={
|
||||
rest.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.mt_sm, rest.style]}>
|
||||
<ImageLayoutGrid
|
||||
images={images}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
viewContext={rest.viewContext}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import {useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util'
|
||||
import {useResolveLinkQuery} from '#/state/queries/resolve-link'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {QuoteEmbed} from '#/components/Post/Embed'
|
||||
|
||||
export function LazyQuoteEmbed({uri}: {uri: string}) {
|
||||
const t = useTheme()
|
||||
const {data} = useResolveLinkQuery(uri)
|
||||
|
||||
const view = useMemo(() => {
|
||||
if (!data || data.type !== 'record' || data.kind !== 'post') return
|
||||
return createEmbedViewRecordFromPost(data.view)
|
||||
}, [data])
|
||||
|
||||
return view ? (
|
||||
<QuoteEmbed
|
||||
embed={{
|
||||
type: 'post',
|
||||
view,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
height: 68,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {moderateUserList} from '@atproto/api'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as ListCard from '#/components/ListCard'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {EmbedType} from '#/types/bsky/post'
|
||||
import {CommonProps} from './types'
|
||||
|
||||
export function ListEmbed({
|
||||
embed,
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'list'>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}>
|
||||
<ListCard.Default view={embed.view} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function ModeratedListEmbed({
|
||||
embed,
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'list'>
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const moderation = React.useMemo(() => {
|
||||
return moderationOpts
|
||||
? moderateUserList(embed.view, moderationOpts)
|
||||
: undefined
|
||||
}, [embed.view, moderationOpts])
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentList')}>
|
||||
<ListEmbed embed={embed} />
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {InfoCircleIcon} from '#/lib/icons'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
|
||||
export function PostPlaceholder({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View
|
||||
style={[styles.errorContainer, a.border, t.atoms.border_contrast_low]}>
|
||||
<InfoCircleIcon size={18} style={pal.text} />
|
||||
<Text type="lg" style={pal.text}>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
errorContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
borderRadius: 8,
|
||||
marginTop: 8,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 14,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
})
|
||||
+1
-1
@@ -7,7 +7,6 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {useAutoplayDisabled} from '#/state/preferences'
|
||||
import {useVideoMuteState} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
@@ -15,6 +14,7 @@ import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Paus
|
||||
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {TimeIndicator} from './TimeIndicator'
|
||||
|
||||
export const VideoEmbedInnerNative = React.forwardRef(
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect, useId, useRef, useState} from 'react'
|
||||
import {useEffect, useId, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
@@ -28,7 +28,7 @@ export function VideoEmbedInnerWeb({
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const [focused, setFocused] = useState(false)
|
||||
const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false)
|
||||
const [hlsLoading, setHlsLoading] = React.useState(false)
|
||||
const [hlsLoading, setHlsLoading] = useState(false)
|
||||
const figId = useId()
|
||||
const {_} = useLingui()
|
||||
|
||||
@@ -101,8 +101,8 @@ export function VideoEmbedInnerWeb({
|
||||
fullscreenRef={containerRef}
|
||||
hasSubtitleTrack={hasSubtitleTrack}
|
||||
/>
|
||||
<MediaInsetBorder />
|
||||
</div>
|
||||
<MediaInsetBorder />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -139,7 +139,7 @@ function useHLS({
|
||||
playlist: string
|
||||
setHasSubtitleTrack: (v: boolean) => void
|
||||
setError: (v: Error | null) => void
|
||||
videoRef: React.RefObject<HTMLVideoElement | null>
|
||||
videoRef: React.RefObject<HTMLVideoElement>
|
||||
setHlsLoading: (v: boolean) => void
|
||||
}) {
|
||||
const [Hls, setHls] = useState<typeof HlsTypes.default | undefined>(
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
import {type SvgProps} from 'react-native-svg'
|
||||
import React from 'react'
|
||||
import {SvgProps} from 'react-native-svg'
|
||||
|
||||
import {PressableWithHover} from '#/view/com/util/PressableWithHover'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {PressableWithHover} from '../../../PressableWithHover'
|
||||
|
||||
export function ControlButton({
|
||||
active,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
+5
-5
@@ -46,14 +46,14 @@ export function Controls({
|
||||
hlsLoading,
|
||||
hasSubtitleTrack,
|
||||
}: {
|
||||
videoRef: React.RefObject<HTMLVideoElement | null>
|
||||
hlsRef: React.RefObject<Hls | undefined | null>
|
||||
videoRef: React.RefObject<HTMLVideoElement>
|
||||
hlsRef: React.RefObject<Hls | undefined>
|
||||
active: boolean
|
||||
setActive: () => void
|
||||
focused: boolean
|
||||
setFocused: (focused: boolean) => void
|
||||
onScreen: boolean
|
||||
fullscreenRef: React.RefObject<HTMLDivElement | null>
|
||||
fullscreenRef: React.RefObject<HTMLDivElement>
|
||||
hlsLoading: boolean
|
||||
hasSubtitleTrack: boolean
|
||||
}) {
|
||||
@@ -232,7 +232,7 @@ export function Controls({
|
||||
}, [onSeek, videoRef])
|
||||
|
||||
const [showCursor, setShowCursor] = useState(true)
|
||||
const cursorTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
const cursorTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
const onPointerMoveEmptySpace = useCallback(() => {
|
||||
setShowCursor(true)
|
||||
if (cursorTimeoutRef.current) {
|
||||
@@ -264,7 +264,7 @@ export function Controls({
|
||||
[hovered],
|
||||
)
|
||||
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||
|
||||
const onHoverWithTimeout = useCallback(() => {
|
||||
onHover()
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import {useCallback} from 'react'
|
||||
import React, {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
@@ -8,7 +8,7 @@ import {isSafari, isTouchDevice} from '#/lib/browser'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {useVideoVolumeState} from '../../VideoVolumeContext'
|
||||
import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {ControlButton} from './ControlButton'
|
||||
|
||||
export function VolumeControl({
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {type RefObject, useCallback, useEffect, useRef, useState} from 'react'
|
||||
|
||||
import {isSafari} from '#/lib/browser'
|
||||
import {useVideoVolumeState} from '../../VideoVolumeContext'
|
||||
import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
|
||||
export function useVideoElement(ref: React.RefObject<HTMLVideoElement | null>) {
|
||||
export function useVideoElement(ref: RefObject<HTMLVideoElement>) {
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [muted, setMuted] = useState(true)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
+3
-3
@@ -1,17 +1,17 @@
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {ActivityIndicator, View} from 'react-native'
|
||||
import {ImageBackground} from 'expo-image'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {ErrorBoundary} from '../ErrorBoundary'
|
||||
import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative'
|
||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||
|
||||
interface Props {
|
||||
+8
-8
@@ -1,20 +1,20 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isFirefox} from '#/lib/browser'
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {useFullscreen} from '#/components/hooks/useFullscreen'
|
||||
import {
|
||||
HLSUnsupportedError,
|
||||
VideoEmbedInnerWeb,
|
||||
VideoNotFoundError,
|
||||
} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {useFullscreen} from '#/components/hooks/useFullscreen'
|
||||
import {ErrorBoundary} from '../ErrorBoundary'
|
||||
} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb'
|
||||
import {useActiveVideoWeb} from './ActiveVideoWebContext'
|
||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||
|
||||
@@ -30,7 +30,7 @@ export function VideoEmbed({
|
||||
useActiveVideoWeb()
|
||||
const [onScreen, setOnScreen] = useState(false)
|
||||
const [isFullscreen] = useFullscreen()
|
||||
const lastKnownTime = useRef<number | undefined>(undefined)
|
||||
const lastKnownTime = useRef<number | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return
|
||||
@@ -0,0 +1,332 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {
|
||||
type $Typed,
|
||||
type AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AtUri,
|
||||
moderatePost,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {unstableCacheProfileView} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {Link} from '#/view/com/util/Link'
|
||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {
|
||||
type Embed as TEmbed,
|
||||
type EmbedType,
|
||||
parseEmbed,
|
||||
} from '#/types/bsky/post'
|
||||
import {ExternalEmbed} from './ExternalEmbed'
|
||||
import {ModeratedFeedEmbed} from './FeedEmbed'
|
||||
import {ImageEmbed} from './ImageEmbed'
|
||||
import {ModeratedListEmbed} from './ListEmbed'
|
||||
import {PostPlaceholder as PostPlaceholderText} from './PostPlaceholder'
|
||||
import {
|
||||
type CommonProps,
|
||||
type EmbedProps,
|
||||
PostEmbedViewContext,
|
||||
QuoteEmbedViewContext,
|
||||
} from './types'
|
||||
import {VideoEmbed} from './VideoEmbed'
|
||||
|
||||
export {PostEmbedViewContext, QuoteEmbedViewContext} from './types'
|
||||
|
||||
export function Embed({embed: rawEmbed, ...rest}: EmbedProps) {
|
||||
const embed = parseEmbed(rawEmbed)
|
||||
|
||||
switch (embed.type) {
|
||||
case 'images':
|
||||
case 'link':
|
||||
case 'video': {
|
||||
return <MediaEmbed embed={embed} {...rest} />
|
||||
}
|
||||
case 'feed':
|
||||
case 'list':
|
||||
case 'starter_pack':
|
||||
case 'labeler':
|
||||
case 'post':
|
||||
case 'post_not_found':
|
||||
case 'post_blocked':
|
||||
case 'post_detached': {
|
||||
return <RecordEmbed embed={embed} {...rest} />
|
||||
}
|
||||
case 'post_with_media': {
|
||||
return (
|
||||
<View style={rest.style}>
|
||||
<MediaEmbed embed={embed.media} {...rest} />
|
||||
<RecordEmbed embed={embed.view} {...rest} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function MediaEmbed({
|
||||
embed,
|
||||
...rest
|
||||
}: CommonProps & {
|
||||
embed: TEmbed
|
||||
}) {
|
||||
switch (embed.type) {
|
||||
case 'images': {
|
||||
return (
|
||||
<ContentHider modui={rest.moderation?.ui('contentMedia')}>
|
||||
<ImageEmbed embed={embed} {...rest} />
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
case 'link': {
|
||||
return (
|
||||
<ContentHider modui={rest.moderation?.ui('contentMedia')}>
|
||||
<ExternalEmbed
|
||||
link={embed.view.external}
|
||||
onOpen={rest.onOpen}
|
||||
style={[a.mt_sm, rest.style]}
|
||||
/>
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
case 'video': {
|
||||
return (
|
||||
<ContentHider modui={rest.moderation?.ui('contentMedia')}>
|
||||
<VideoEmbed embed={embed.view} />
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function RecordEmbed({
|
||||
embed,
|
||||
...rest
|
||||
}: CommonProps & {
|
||||
embed: TEmbed
|
||||
}) {
|
||||
switch (embed.type) {
|
||||
case 'feed': {
|
||||
return (
|
||||
<View style={a.mt_sm}>
|
||||
<ModeratedFeedEmbed embed={embed} {...rest} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
case 'list': {
|
||||
return (
|
||||
<View style={a.mt_sm}>
|
||||
<ModeratedListEmbed embed={embed} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
case 'starter_pack': {
|
||||
return (
|
||||
<View style={a.mt_sm}>
|
||||
<StarterPackCard starterPack={embed.view} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
case 'labeler': {
|
||||
// not implemented
|
||||
return null
|
||||
}
|
||||
case 'post': {
|
||||
if (rest.isWithinQuote && !rest.allowNestedQuotes) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<QuoteEmbed
|
||||
{...rest}
|
||||
embed={embed}
|
||||
viewContext={
|
||||
rest.viewContext === PostEmbedViewContext.Feed
|
||||
? QuoteEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
: undefined
|
||||
}
|
||||
isWithinQuote={rest.isWithinQuote}
|
||||
allowNestedQuotes={rest.allowNestedQuotes}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'post_not_found': {
|
||||
return (
|
||||
<PostPlaceholderText>
|
||||
<Trans>Deleted</Trans>
|
||||
</PostPlaceholderText>
|
||||
)
|
||||
}
|
||||
case 'post_blocked': {
|
||||
return (
|
||||
<PostPlaceholderText>
|
||||
<Trans>Blocked</Trans>
|
||||
</PostPlaceholderText>
|
||||
)
|
||||
}
|
||||
case 'post_detached': {
|
||||
return <PostDetachedEmbed embed={embed} />
|
||||
}
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function PostDetachedEmbed({
|
||||
embed,
|
||||
}: {
|
||||
embed: EmbedType<'post_detached'>
|
||||
}) {
|
||||
const {currentAccount} = useSession()
|
||||
const isViewerOwner = currentAccount?.did
|
||||
? embed.view.uri.includes(currentAccount.did)
|
||||
: false
|
||||
|
||||
return (
|
||||
<PostPlaceholderText>
|
||||
{isViewerOwner ? (
|
||||
<Trans>Removed by you</Trans>
|
||||
) : (
|
||||
<Trans>Removed by author</Trans>
|
||||
)}
|
||||
</PostPlaceholderText>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Nests parent `Embed` component and therefore must live in this file to avoid
|
||||
* circular imports.
|
||||
*/
|
||||
export function QuoteEmbed({
|
||||
embed,
|
||||
onOpen,
|
||||
style,
|
||||
isWithinQuote: parentIsWithinQuote,
|
||||
allowNestedQuotes: parentAllowNestedQuotes,
|
||||
}: Omit<CommonProps, 'viewContext'> & {
|
||||
embed: EmbedType<'post'>
|
||||
viewContext?: QuoteEmbedViewContext
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const quote = React.useMemo<$Typed<AppBskyFeedDefs.PostView>>(
|
||||
() => ({
|
||||
...embed.view,
|
||||
$type: 'app.bsky.feed.defs#postView',
|
||||
record: embed.view.value,
|
||||
embed: embed.view.embeds?.[0],
|
||||
}),
|
||||
[embed],
|
||||
)
|
||||
const moderation = React.useMemo(() => {
|
||||
return moderationOpts ? moderatePost(quote, moderationOpts) : undefined
|
||||
}, [quote, moderationOpts])
|
||||
|
||||
const t = useTheme()
|
||||
const queryClient = useQueryClient()
|
||||
const pal = usePalette('default')
|
||||
const itemUrip = new AtUri(quote.uri)
|
||||
const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey)
|
||||
const itemTitle = `Post by ${quote.author.handle}`
|
||||
|
||||
const richText = React.useMemo(() => {
|
||||
if (
|
||||
!bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
quote.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
)
|
||||
return undefined
|
||||
const {text, facets} = quote.record
|
||||
return text.trim()
|
||||
? new RichTextAPI({text: text, facets: facets})
|
||||
: undefined
|
||||
}, [quote.record])
|
||||
|
||||
const onBeforePress = React.useCallback(() => {
|
||||
unstableCacheProfileView(queryClient, quote.author)
|
||||
onOpen?.()
|
||||
}, [queryClient, quote.author, onOpen])
|
||||
|
||||
const [hover, setHover] = React.useState(false)
|
||||
return (
|
||||
<View
|
||||
onPointerEnter={() => {
|
||||
setHover(true)
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
setHover(false)
|
||||
}}>
|
||||
<ContentHider
|
||||
modui={moderation?.ui('contentList')}
|
||||
style={[
|
||||
a.rounded_md,
|
||||
a.p_md,
|
||||
a.mt_sm,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
style,
|
||||
]}
|
||||
childContainerStyle={[a.pt_sm]}>
|
||||
<SubtleWebHover hover={hover} />
|
||||
<Link
|
||||
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
||||
href={itemHref}
|
||||
title={itemTitle}
|
||||
onBeforePress={onBeforePress}>
|
||||
<View pointerEvents="none">
|
||||
<PostMeta
|
||||
author={quote.author}
|
||||
moderation={moderation}
|
||||
showAvatar
|
||||
postHref={itemHref}
|
||||
timestamp={quote.indexedAt}
|
||||
/>
|
||||
</View>
|
||||
{moderation ? (
|
||||
<PostAlerts
|
||||
modui={moderation.ui('contentView')}
|
||||
style={[a.py_xs]}
|
||||
/>
|
||||
) : null}
|
||||
{richText ? (
|
||||
<RichText
|
||||
value={richText}
|
||||
style={a.text_md}
|
||||
numberOfLines={20}
|
||||
disableLinks
|
||||
/>
|
||||
) : null}
|
||||
{quote.embed && (
|
||||
<Embed
|
||||
embed={quote.embed}
|
||||
moderation={moderation}
|
||||
isWithinQuote={parentIsWithinQuote ?? true}
|
||||
// already within quote? override nested
|
||||
allowNestedQuotes={
|
||||
parentIsWithinQuote ? false : parentAllowNestedQuotes
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</ContentHider>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {type StyleProp, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyFeedDefs, type ModerationDecision} from '@atproto/api'
|
||||
|
||||
export enum PostEmbedViewContext {
|
||||
ThreadHighlighted = 'ThreadHighlighted',
|
||||
Feed = 'Feed',
|
||||
FeedEmbedRecordWithMedia = 'FeedEmbedRecordWithMedia',
|
||||
}
|
||||
|
||||
export enum QuoteEmbedViewContext {
|
||||
FeedEmbedRecordWithMedia = PostEmbedViewContext.FeedEmbedRecordWithMedia,
|
||||
}
|
||||
|
||||
export type CommonProps = {
|
||||
moderation?: ModerationDecision
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
viewContext?: PostEmbedViewContext
|
||||
isWithinQuote?: boolean
|
||||
allowNestedQuotes?: boolean
|
||||
}
|
||||
|
||||
export type EmbedProps = CommonProps & {
|
||||
embed?: AppBskyFeedDefs.PostView['embed']
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type React from 'react'
|
||||
|
||||
export type ProfileHoverCardProps = {
|
||||
children: React.ReactElement<any>
|
||||
children: React.ReactElement
|
||||
did: string
|
||||
inline?: boolean
|
||||
disable?: boolean
|
||||
|
||||
@@ -301,8 +301,8 @@ let Header = ({
|
||||
interestsDisplayNames,
|
||||
}: {
|
||||
guide: Follow10ProgressGuide
|
||||
inputRef: React.RefObject<TextInput | null>
|
||||
listRef: React.RefObject<ListMethods | null>
|
||||
inputRef: React.RefObject<TextInput>
|
||||
listRef: React.RefObject<ListMethods>
|
||||
onSelectTab: (v: string) => void
|
||||
searchText: string
|
||||
setHeaderHeight: (v: number) => void
|
||||
@@ -653,7 +653,7 @@ function SearchInput({
|
||||
}: {
|
||||
onChangeText: (text: string) => void
|
||||
onEscape: () => void
|
||||
inputRef: React.RefObject<TextInput | null>
|
||||
inputRef: React.RefObject<TextInput>
|
||||
defaultValue: string
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
@@ -14,7 +14,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Portal} from '#/components/Portal'
|
||||
import {AnimatedCheck, type AnimatedCheckRef} from '../anim/AnimatedCheck'
|
||||
import {AnimatedCheck, AnimatedCheckRef} from '../anim/AnimatedCheck'
|
||||
import {Text} from '../Typography'
|
||||
|
||||
export interface ProgressGuideToastRef {
|
||||
@@ -39,7 +39,7 @@ export const ProgressGuideToast = React.forwardRef<
|
||||
const translateY = useSharedValue(0)
|
||||
const opacity = useSharedValue(0)
|
||||
const animatedCheckRef = React.useRef<AnimatedCheckRef | null>(null)
|
||||
const timeoutRef = React.useRef<NodeJS.Timeout | undefined>(undefined)
|
||||
const timeoutRef = React.useRef<NodeJS.Timeout | undefined>()
|
||||
const winDim = useWindowDimensions()
|
||||
|
||||
/**
|
||||
|
||||
@@ -160,7 +160,7 @@ export type ContentProps<T> = {
|
||||
item: T,
|
||||
index: number,
|
||||
selectedValue?: string | null,
|
||||
) => React.ReactElement<any>
|
||||
) => React.ReactElement
|
||||
/*
|
||||
* Extracts the value from an item. Defaults to `item => item.value`
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import {type ReactNode} from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {
|
||||
atoms as a,
|
||||
flatten,
|
||||
type TextStyleProp,
|
||||
useAlf,
|
||||
useTheme,
|
||||
type ViewStyleProp,
|
||||
} from '#/alf'
|
||||
import {normalizeTextStyles} from '#/alf/typography'
|
||||
|
||||
type SkeletonProps = {
|
||||
blend?: boolean
|
||||
}
|
||||
|
||||
export function Text({blend, style}: TextStyleProp & SkeletonProps) {
|
||||
const {fonts, flags, theme: t} = useAlf()
|
||||
const {width, ...flattened} = flatten(style)
|
||||
const {lineHeight = 14, ...rest} = normalizeTextStyles(
|
||||
[a.text_sm, a.leading_snug, flattened],
|
||||
{
|
||||
fontScale: fonts.scaleMultiplier,
|
||||
fontFamily: fonts.family,
|
||||
flags,
|
||||
},
|
||||
)
|
||||
return (
|
||||
<View
|
||||
style={[a.flex_1, {maxWidth: width, paddingVertical: lineHeight * 0.15}]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
height: lineHeight * 0.7,
|
||||
opacity: blend ? 0.6 : 1,
|
||||
},
|
||||
rest,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Circle({
|
||||
children,
|
||||
size,
|
||||
blend,
|
||||
style,
|
||||
}: ViewStyleProp & {children?: ReactNode; size: number} & SkeletonProps) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
opacity: blend ? 0.6 : 1,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Pill({
|
||||
size,
|
||||
blend,
|
||||
style,
|
||||
}: ViewStyleProp & {size: number} & SkeletonProps) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
width: size * 1.618,
|
||||
height: size,
|
||||
opacity: blend ? 0.6 : 1,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Col({
|
||||
children,
|
||||
style,
|
||||
}: ViewStyleProp & {children?: React.ReactNode}) {
|
||||
return <View style={[a.flex_1, style]}>{children}</View>
|
||||
}
|
||||
|
||||
export function Row({
|
||||
children,
|
||||
style,
|
||||
}: ViewStyleProp & {children?: React.ReactNode}) {
|
||||
return <View style={[a.flex_row, style]}>{children}</View>
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {type TextInput, View} from 'react-native'
|
||||
import {TextInput, View} from 'react-native'
|
||||
import {useWindowDimensions} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
@@ -15,13 +15,13 @@ import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {
|
||||
type Gif,
|
||||
Gif,
|
||||
useFeaturedGifsQuery,
|
||||
useGifSearchQuery,
|
||||
} from '#/state/queries/tenor'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {type ListMethods} from '#/view/com/util/List'
|
||||
import {ListMethods} from '#/view/com/util/List'
|
||||
import {atoms as a, ios, native, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -36,7 +36,7 @@ export function GifSelectDialog({
|
||||
onClose,
|
||||
onSelectGif: onSelectGifProp,
|
||||
}: {
|
||||
controlRef: React.RefObject<{open: () => void} | null>
|
||||
controlRef: React.RefObject<{open: () => void}>
|
||||
onClose?: () => void
|
||||
onSelectGif: (gif: Gif) => void
|
||||
}) {
|
||||
|
||||
@@ -397,6 +397,7 @@ function DefaultProfileCard({
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
disabledPreview
|
||||
/>
|
||||
<View style={[a.flex_1]}>
|
||||
<ProfileCard.Name
|
||||
@@ -483,7 +484,7 @@ function SearchInput({
|
||||
value: string
|
||||
onChangeText: (text: string) => void
|
||||
onEscape: () => void
|
||||
inputRef: React.RefObject<TextInput | null>
|
||||
inputRef: React.RefObject<TextInput>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {View} from 'react-native'
|
||||
import {ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Pressable, View} from 'react-native'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {useConvoActive} from '#/state/messages/convo'
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react'
|
||||
import {useWindowDimensions, View} from 'react-native'
|
||||
import {AppBskyEmbedRecord} from '@atproto/api'
|
||||
import {type $Typed, type AppBskyEmbedRecord} from '@atproto/api'
|
||||
|
||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
||||
import {atoms as a, native, tokens, useTheme, web} from '#/alf'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed'
|
||||
import {Embed} from '#/components/Post/Embed'
|
||||
import {MessageContextProvider} from './MessageContext'
|
||||
|
||||
let MessageItemEmbed = ({
|
||||
embed,
|
||||
}: {
|
||||
embed: AppBskyEmbedRecord.View
|
||||
embed: $Typed<AppBskyEmbedRecord.View>
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const screen = useWindowDimensions()
|
||||
@@ -32,7 +33,7 @@ let MessageItemEmbed = ({
|
||||
}),
|
||||
]}>
|
||||
<View style={{marginTop: tokens.space.sm * -1}}>
|
||||
<PostEmbeds
|
||||
<Embed
|
||||
embed={embed}
|
||||
allowNestedQuotes
|
||||
viewContext={PostEmbedViewContext.Feed}
|
||||
|
||||
@@ -21,14 +21,11 @@ export function InputGroup(props: React.PropsWithChildren<{}>) {
|
||||
/>
|
||||
) : null}
|
||||
{React.cloneElement(child, {
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
style: [
|
||||
// @ts-expect-error
|
||||
...(Array.isArray(child.props?.style)
|
||||
? // @ts-expect-error
|
||||
child.props.style
|
||||
: // @ts-expect-error
|
||||
[child.props.style || {}]),
|
||||
? child.props.style
|
||||
: [child.props.style || {}]),
|
||||
{
|
||||
borderTopLeftRadius: i > 0 ? 0 : undefined,
|
||||
borderTopRightRadius: i > 0 ? 0 : undefined,
|
||||
|
||||
@@ -26,7 +26,7 @@ import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const Context = React.createContext<{
|
||||
inputRef: React.RefObject<TextInput | null> | null
|
||||
inputRef: React.RefObject<TextInput> | null
|
||||
isInvalid: boolean
|
||||
hovered: boolean
|
||||
onHoverIn: () => void
|
||||
@@ -140,7 +140,7 @@ export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {
|
||||
value?: string
|
||||
onChangeText?: (value: string) => void
|
||||
isInvalid?: boolean
|
||||
inputRef?: React.RefObject<TextInput | null> | React.ForwardedRef<TextInput>
|
||||
inputRef?: React.RefObject<TextInput> | React.ForwardedRef<TextInput>
|
||||
}
|
||||
|
||||
export function createInput(Component: typeof TextInput) {
|
||||
@@ -196,7 +196,7 @@ export function createInput(Component: typeof TextInput) {
|
||||
minWidth: 0,
|
||||
},
|
||||
ios({paddingTop: 12, paddingBottom: 13}),
|
||||
android(a.py_sm),
|
||||
android(a.py_md),
|
||||
// fix for autofill styles covering border
|
||||
web({
|
||||
paddingTop: 10,
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
type AccessibilityProps,
|
||||
type TextStyle,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {AccessibilityProps, TextStyle, View, ViewStyle} from 'react-native'
|
||||
|
||||
import {atoms as a, native, useTheme} from '#/alf'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
@@ -12,7 +7,7 @@ import {Text} from '#/components/Typography'
|
||||
|
||||
type ItemProps = Omit<Toggle.ItemProps, 'style' | 'role' | 'children'> &
|
||||
AccessibilityProps & {
|
||||
children: React.ReactElement<any>
|
||||
children: React.ReactElement
|
||||
testID?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {formatDistance, Locale} from 'date-fns'
|
||||
import {formatDistance, type Locale} from 'date-fns'
|
||||
import {
|
||||
ca,
|
||||
cy,
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
zhTW,
|
||||
} from 'date-fns/locale'
|
||||
|
||||
import {AppLanguage} from '#/locale/languages'
|
||||
import {type AppLanguage} from '#/locale/languages'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ function fullscreenSubscribe(onChange: () => void) {
|
||||
return () => document.removeEventListener('fullscreenchange', onChange)
|
||||
}
|
||||
|
||||
export function useFullscreen(ref?: React.RefObject<HTMLElement | null>) {
|
||||
export function useFullscreen(ref?: React.RefObject<HTMLElement>) {
|
||||
if (!isWeb) throw new Error("'useFullscreen' is a web-only hook")
|
||||
const isFullscreen = useSyncExternalStore(fullscreenSubscribe, () =>
|
||||
Boolean(document.fullscreenElement),
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const ArrowTopCircle_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.63 3.225a1 1 0 0 1 1.337.068l3 3 .068.076a1 1 0 0 1-1.406 1.406l-.076-.068L13 10.414V16a1 1 0 1 1-2 0v-5.586l-1.293 1.293a1 1 0 1 1-1.414-1.414l3-3 .076-.068Z',
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const CirclePlus_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm0 3a1 1 0 0 1 1 1v3h3l.102.005a1 1 0 0 1 0 1.99L16 13h-3v3a1 1 0 1 1-2 0v-3H8a1 1 0 0 1 0-2h3V8a1 1 0 0 1 1-1Z',
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const Tree_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M6 2a2.998 2.998 0 0 1 1 5.825V8a2 2 0 0 0 2 2h1.174c.412-1.165 1.52-2 2.826-2h5a3 3 0 1 1 0 6h-5a2.998 2.998 0 0 1-2.826-2H9a3.98 3.98 0 0 1-2-.537V16a2 2 0 0 0 2 2h1.174c.412-1.165 1.52-2 2.826-2h5a3 3 0 1 1 0 6h-5a2.998 2.998 0 0 1-2.826-2H9a4 4 0 0 1-4-4V7.825A2.998 2.998 0 0 1 6 2Zm7 16a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5Zm0-8a1 1 0 1 0 0 2h5a1 1 0 1 0 0-2h-5ZM6 4a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z',
|
||||
})
|
||||
@@ -1,6 +1,16 @@
|
||||
import React, {ComponentProps} from 'react'
|
||||
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyActorDefs, ModerationCause, ModerationUI} from '@atproto/api'
|
||||
import React, {type ComponentProps} from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
type StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
type ModerationCause,
|
||||
type ModerationUI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import {timeout} from '#/lib/async/timeout'
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
|
||||
export async function retry<P>(
|
||||
retries: number,
|
||||
cond: (err: any) => boolean,
|
||||
fn: () => Promise<P>,
|
||||
shouldRetry: (err: any) => boolean,
|
||||
action: () => Promise<P>,
|
||||
delay?: number,
|
||||
): Promise<P> {
|
||||
let lastErr
|
||||
while (retries > 0) {
|
||||
try {
|
||||
return await fn()
|
||||
return await action()
|
||||
} catch (e: any) {
|
||||
lastErr = e
|
||||
if (cond(e)) {
|
||||
if (shouldRetry(e)) {
|
||||
if (delay) {
|
||||
await timeout(delay)
|
||||
}
|
||||
retries--
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {useRef} from 'react'
|
||||
import * as React from 'react'
|
||||
import {Animated} from 'react-native'
|
||||
|
||||
export function useAnimatedValue(initialValue: number) {
|
||||
const lazyRef = useRef<Animated.Value>(undefined)
|
||||
const lazyRef = React.useRef<Animated.Value>()
|
||||
|
||||
if (lazyRef.current === undefined) {
|
||||
lazyRef.current = new Animated.Value(initialValue)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import {useCallback} from 'react'
|
||||
|
||||
export enum OnceKey {
|
||||
PreferencesThread = 'preferences:thread',
|
||||
}
|
||||
|
||||
const called: Record<OnceKey, boolean> = {
|
||||
[OnceKey.PreferencesThread]: false,
|
||||
}
|
||||
|
||||
export function useCallOnce(key: OnceKey) {
|
||||
return useCallback(
|
||||
(cb: () => void) => {
|
||||
if (called[key] === true) return
|
||||
called[key] = true
|
||||
cb()
|
||||
},
|
||||
[key],
|
||||
)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import {useState} from 'react'
|
||||
import {AnimatedRef, measure, MeasuredDimensions} from 'react-native-reanimated'
|
||||
|
||||
export type HandleRef = {
|
||||
(node: any): void
|
||||
current: null | number
|
||||
}
|
||||
|
||||
// This is a lighterweight alternative to `useAnimatedRef()` for imperative UI thread actions.
|
||||
// Render it like <View ref={ref} />, then pass `ref.current` to `measureHandle()` and such.
|
||||
export function useHandleRef(): HandleRef {
|
||||
return useState(() => {
|
||||
const ref = (node: any) => {
|
||||
if (node) {
|
||||
ref.current =
|
||||
node._nativeTag ??
|
||||
node.__nativeTag ??
|
||||
node.canonical?.nativeTag ??
|
||||
null
|
||||
} else {
|
||||
ref.current = null
|
||||
}
|
||||
}
|
||||
ref.current = null
|
||||
return ref
|
||||
})[0] as HandleRef
|
||||
}
|
||||
|
||||
// When using this version, you need to read ref.current on the JS thread, and pass it to UI.
|
||||
export function measureHandle(
|
||||
current: number | null,
|
||||
): MeasuredDimensions | null {
|
||||
'worklet'
|
||||
if (current !== null) {
|
||||
return measure((() => current) as AnimatedRef<any>)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import {createContext, useCallback, useContext, useState} from 'react'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
type HideBottomBarBorderSetter = () => () => void
|
||||
|
||||
const HideBottomBarBorderContext = createContext<boolean>(false)
|
||||
const HideBottomBarBorderSetterContext =
|
||||
createContext<HideBottomBarBorderSetter | null>(null)
|
||||
|
||||
export function useHideBottomBarBorderSetter() {
|
||||
const hideBottomBarBorder = useContext(HideBottomBarBorderSetterContext)
|
||||
if (!hideBottomBarBorder) {
|
||||
throw new Error(
|
||||
'useHideBottomBarBorderSetter must be used within a HideBottomBarBorderProvider',
|
||||
)
|
||||
}
|
||||
return hideBottomBarBorder
|
||||
}
|
||||
|
||||
export function useHideBottomBarBorderForScreen() {
|
||||
const hideBorder = useHideBottomBarBorderSetter()
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const cleanup = hideBorder()
|
||||
return () => cleanup()
|
||||
}, [hideBorder]),
|
||||
)
|
||||
}
|
||||
|
||||
export function useHideBottomBarBorder() {
|
||||
return useContext(HideBottomBarBorderContext)
|
||||
}
|
||||
|
||||
export function Provider({children}: {children: React.ReactNode}) {
|
||||
const [refCount, setRefCount] = useState(0)
|
||||
|
||||
const setter = useCallback(() => {
|
||||
setRefCount(prev => prev + 1)
|
||||
return () => setRefCount(prev => prev - 1)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<HideBottomBarBorderSetterContext.Provider value={setter}>
|
||||
<HideBottomBarBorderContext.Provider value={refCount > 0}>
|
||||
{children}
|
||||
</HideBottomBarBorderContext.Provider>
|
||||
</HideBottomBarBorderSetterContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {Alert, AppState, type AppStateStatus} from 'react-native'
|
||||
import {Alert, AppState, AppStateStatus} from 'react-native'
|
||||
import {nativeBuildVersion} from 'expo-application'
|
||||
import {
|
||||
checkForUpdateAsync,
|
||||
@@ -35,7 +35,7 @@ export function useOTAUpdates() {
|
||||
const appState = React.useRef<AppStateStatus>('active')
|
||||
const lastMinimize = React.useRef(0)
|
||||
const ranInitialCheck = React.useRef(false)
|
||||
const timeout = React.useRef<NodeJS.Timeout>(undefined)
|
||||
const timeout = React.useRef<NodeJS.Timeout>()
|
||||
const {isUpdatePending} = useUpdates()
|
||||
|
||||
const setCheckTimeout = React.useCallback(() => {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react'
|
||||
|
||||
/**
|
||||
* Helper hook to run persistent timers on views
|
||||
*/
|
||||
export function useTimer(time: number, handler: () => void) {
|
||||
const timer = React.useRef<undefined | NodeJS.Timeout>(undefined)
|
||||
|
||||
// function to restart the timer
|
||||
const reset = React.useCallback(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
}
|
||||
timer.current = setTimeout(handler, time)
|
||||
}, [time, timer, handler])
|
||||
|
||||
// function to cancel the timer
|
||||
const cancel = React.useCallback(() => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current)
|
||||
timer.current = undefined
|
||||
}
|
||||
}, [timer])
|
||||
|
||||
// start the timer immediately
|
||||
React.useEffect(() => {
|
||||
reset()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return [reset, cancel]
|
||||
}
|
||||
+37
-50
@@ -1,8 +1,9 @@
|
||||
import {Image as RNImage, Share as RNShare} from 'react-native'
|
||||
import {Image as RNImage} from 'react-native'
|
||||
import uuid from 'react-native-uuid'
|
||||
import {
|
||||
cacheDirectory,
|
||||
copyAsync,
|
||||
createDownloadResumable,
|
||||
deleteAsync,
|
||||
EncodingType,
|
||||
getInfoAsync,
|
||||
@@ -14,7 +15,6 @@ import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
import {Buffer} from 'buffer'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
import {POST_IMG_MAX} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
@@ -68,28 +68,13 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
||||
return
|
||||
}
|
||||
|
||||
let downloadRes
|
||||
const path = createPath(appendExt)
|
||||
|
||||
try {
|
||||
const downloadResPromise = RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
appendExt,
|
||||
}).fetch('GET', opts.uri)
|
||||
const to1 = setTimeout(() => downloadResPromise.cancel(), opts.timeout)
|
||||
downloadRes = await downloadResPromise
|
||||
clearTimeout(to1)
|
||||
|
||||
const status = downloadRes.info().status
|
||||
if (status !== 200) {
|
||||
return
|
||||
}
|
||||
|
||||
const localUri = normalizePath(downloadRes.path(), true)
|
||||
return await doResize(localUri, opts)
|
||||
await downloadImage(opts.uri, path, opts.timeout)
|
||||
return await doResize(path, opts)
|
||||
} finally {
|
||||
// TODO Whenever we remove `rn-fetch-blob`, we will need to replace this `flush()` with a `deleteAsync()` -hailey
|
||||
if (downloadRes) {
|
||||
downloadRes.flush()
|
||||
}
|
||||
safeDeleteAsync(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,32 +83,16 @@ export async function shareImageModal({uri}: {uri: string}) {
|
||||
// TODO might need to give an error to the user in this case -prf
|
||||
return
|
||||
}
|
||||
const downloadResponse = await RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
}).fetch('GET', uri)
|
||||
|
||||
// NOTE
|
||||
// assuming PNG
|
||||
// we're currently relying on the fact our CDN only serves pngs
|
||||
// -prf
|
||||
|
||||
let imagePath = downloadResponse.path()
|
||||
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
|
||||
|
||||
// NOTE
|
||||
// for some reason expo-sharing refuses to work on iOS
|
||||
// ...and visa versa
|
||||
// -prf
|
||||
if (isIOS) {
|
||||
await RNShare.share({url: imagePath})
|
||||
} else {
|
||||
await Sharing.shareAsync(imagePath, {
|
||||
mimeType: 'image/png',
|
||||
UTI: 'image/png',
|
||||
})
|
||||
}
|
||||
|
||||
safeDeleteAsync(imagePath)
|
||||
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||
safeDeleteAsync(imageUri)
|
||||
await Sharing.shareAsync(imagePath, {
|
||||
mimeType: 'image/png',
|
||||
UTI: 'image/png',
|
||||
})
|
||||
}
|
||||
|
||||
const ALBUM_NAME = 'Bluesky'
|
||||
@@ -134,11 +103,8 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
||||
// assuming PNG
|
||||
// we're currently relying on the fact our CDN only serves pngs
|
||||
// -prf
|
||||
const downloadResponse = await RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
}).fetch('GET', uri)
|
||||
let imagePath = downloadResponse.path()
|
||||
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
|
||||
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||
|
||||
// save
|
||||
try {
|
||||
@@ -403,3 +369,24 @@ export function getResizedDimensions(originalDims: {
|
||||
height: Math.round(originalDims.height * ratio),
|
||||
}
|
||||
}
|
||||
|
||||
function createPath(ext: string) {
|
||||
// cacheDirectory will never be null on native, so the null check here is not necessary except for typescript.
|
||||
// we use a web-only function for downloadAndResize on web
|
||||
return `${cacheDirectory ?? ''}/${uuid.v4()}.${ext}`
|
||||
}
|
||||
|
||||
async function downloadImage(uri: string, path: string, timeout: number) {
|
||||
const dlResumable = createDownloadResumable(uri, path, {cache: true})
|
||||
|
||||
const to1 = setTimeout(() => dlResumable.cancelAsync(), timeout)
|
||||
|
||||
const dlRes = await dlResumable.downloadAsync()
|
||||
clearTimeout(to1)
|
||||
|
||||
if (!dlRes?.uri) {
|
||||
throw new Error('Failed to download image - dlRes is undefined')
|
||||
}
|
||||
|
||||
return normalizePath(dlRes.uri)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* returns a ref callback function that can be used to merge multiple refs into a single ref.
|
||||
*/
|
||||
export function mergeRefs<T = any>(
|
||||
refs: Array<React.MutableRefObject<T> | React.Ref<T>>,
|
||||
refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>,
|
||||
): React.RefCallback<T> {
|
||||
return value => {
|
||||
refs.forEach(ref => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Route, RouteParams} from './types'
|
||||
import {type Route, type RouteParams} from './types'
|
||||
|
||||
export class Router {
|
||||
routes: [string, Route][] = []
|
||||
@@ -45,7 +45,7 @@ function createRoute(pattern: string): Route {
|
||||
})
|
||||
const matcherRe = new RegExp(`^${matcherReInternal}([?]|$)`, 'i')
|
||||
return {
|
||||
match(path: string) {
|
||||
match(path) {
|
||||
const {pathname, searchParams} = new URL(path, 'http://throwaway.com')
|
||||
const addedParams = Object.fromEntries(searchParams.entries())
|
||||
|
||||
@@ -55,10 +55,10 @@ function createRoute(pattern: string): Route {
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
build(params: Record<string, string>) {
|
||||
build(params = {}) {
|
||||
const str = pattern.replace(
|
||||
/:([\w]+)/g,
|
||||
(_m, name) => params[name] || 'undefined',
|
||||
(_m, name) => params[encodeURIComponent(name)] || 'undefined',
|
||||
)
|
||||
|
||||
let hasQp = false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user