Merge remote-tracking branch 'origin/main' into samuel/show-more
* origin/main: Bump API SDK to fix `and/or` mute words bug (#8488) Disable default stack traces that are causing issues (#8487) Port post embeds to new arch (#7408) Nightly source-language update Revert "Instant Feed Update on Mute or Moderation Action" (#8482) rm browserslist (#8481) new arch (#8295)
This commit is contained in:
@@ -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 {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||||
import RNFetchBlob from 'rn-fetch-blob'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
downloadAndResize,
|
downloadAndResize,
|
||||||
DownloadAndResizeOpts,
|
type DownloadAndResizeOpts,
|
||||||
getResizedDimensions,
|
getResizedDimensions,
|
||||||
} from '../../src/lib/media/manip'
|
} from '../../src/lib/media/manip'
|
||||||
|
|
||||||
@@ -32,11 +31,12 @@ describe('downloadAndResize', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return resized image for valid URI and options', async () => {
|
it('should return resized image for valid URI and options', async () => {
|
||||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
const mockedFetch = createDownloadResumable as jest.Mock
|
||||||
mockedFetch.mockResolvedValueOnce({
|
mockedFetch.mockReturnValue({
|
||||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
cancelAsync: jest.fn(),
|
||||||
info: jest.fn().mockReturnValue({status: 200}),
|
downloadAsync: jest
|
||||||
flush: jest.fn(),
|
.fn()
|
||||||
|
.mockResolvedValue({uri: 'file://resized-image.jpg'}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const opts: DownloadAndResizeOpts = {
|
const opts: DownloadAndResizeOpts = {
|
||||||
@@ -50,13 +50,12 @@ describe('downloadAndResize', () => {
|
|||||||
|
|
||||||
const result = await downloadAndResize(opts)
|
const result = await downloadAndResize(opts)
|
||||||
expect(result).toEqual(mockResizedImage)
|
expect(result).toEqual(mockResizedImage)
|
||||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
expect(createDownloadResumable).toHaveBeenCalledWith(
|
||||||
fileCache: true,
|
opts.uri,
|
||||||
appendExt: 'jpeg',
|
expect.anything(),
|
||||||
})
|
{
|
||||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
cache: true,
|
||||||
'GET',
|
},
|
||||||
'https://example.com/image.jpg',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// First time it gets called is to get dimensions
|
// First time it gets called is to get dimensions
|
||||||
@@ -86,28 +85,6 @@ describe('downloadAndResize', () => {
|
|||||||
expect(result).toBeUndefined()
|
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', () => {
|
it('should not downsize whenever dimensions are below the max dimensions', () => {
|
||||||
const initialDimensionsOne = {
|
const initialDimensionsOne = {
|
||||||
width: 1200,
|
width: 1200,
|
||||||
|
|||||||
+1
-1
@@ -219,7 +219,7 @@ module.exports = function (_config) {
|
|||||||
compileSdkVersion: 35,
|
compileSdkVersion: 35,
|
||||||
targetSdkVersion: 35,
|
targetSdkVersion: 35,
|
||||||
buildToolsVersion: '35.0.0',
|
buildToolsVersion: '35.0.0',
|
||||||
newArchEnabled: false,
|
newArchEnabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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 createShortLink} from './createShortLink.js'
|
||||||
import {default as health} from './health.js'
|
import {default as health} from './health.js'
|
||||||
import {default as redirect} from './redirect.js'
|
import {default as redirect} from './redirect.js'
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import assert from 'node:assert'
|
|||||||
|
|
||||||
import {DAY, SECOND} from '@atproto/common'
|
import {DAY, SECOND} from '@atproto/common'
|
||||||
import escapeHTML from 'escape-html'
|
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'
|
import {handler} from './util.js'
|
||||||
|
|
||||||
const INTERNAL_IP_REGEX = new RegExp(
|
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'
|
import {handler} from './util.js'
|
||||||
|
|
||||||
export default function (ctx: AppContext, app: Express) {
|
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', () => ({
|
jest.mock('expo-file-system', () => ({
|
||||||
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
|
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
|
||||||
deleteAsync: jest.fn(),
|
deleteAsync: jest.fn(),
|
||||||
|
createDownloadResumable: jest.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('expo-image-manipulator', () => ({
|
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
|
return () => null
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|||||||
+40
-39
@@ -69,12 +69,12 @@
|
|||||||
"icons:optimize": "svgo -f ./assets/icons"
|
"icons:optimize": "svgo -f ./assets/icons"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.15.14",
|
"@atproto/api": "^0.15.15",
|
||||||
"@bitdrift/react-native": "^0.6.8",
|
"@bitdrift/react-native": "^0.6.8",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@expo/html-elements": "^0.12.4",
|
"@expo/html-elements": "^0.12.5",
|
||||||
"@expo/webpack-config": "^19.0.1",
|
"@expo/webpack-config": "^19.0.1",
|
||||||
"@floating-ui/dom": "^1.6.3",
|
"@floating-ui/dom": "^1.6.3",
|
||||||
"@floating-ui/react-dom": "^2.0.8",
|
"@floating-ui/react-dom": "^2.0.8",
|
||||||
@@ -85,12 +85,12 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
"@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",
|
"@ipld/dag-cbor": "^9.2.0",
|
||||||
"@lingui/react": "^4.14.1",
|
"@lingui/react": "^4.14.1",
|
||||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
|
||||||
"@miblanchard/react-native-slider": "^2.3.1",
|
"@miblanchard/react-native-slider": "^2.6.0",
|
||||||
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
"@mozzius/expo-dynamic-app-icon": "1.5.0",
|
||||||
"@react-native-async-storage/async-storage": "2.1.2",
|
"@react-native-async-storage/async-storage": "2.1.2",
|
||||||
"@react-native-menu/menu": "^1.2.3",
|
"@react-native-menu/menu": "^1.2.3",
|
||||||
"@react-native-picker/picker": "2.11.0",
|
"@react-native-picker/picker": "2.11.0",
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
"@react-navigation/drawer": "^7.3.12",
|
"@react-navigation/drawer": "^7.3.12",
|
||||||
"@react-navigation/native": "^7.1.9",
|
"@react-navigation/native": "^7.1.9",
|
||||||
"@react-navigation/native-stack": "^7.3.13",
|
"@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/query-async-storage-persister": "^5.25.0",
|
||||||
"@tanstack/react-query": "^5.8.1",
|
"@tanstack/react-query": "^5.8.1",
|
||||||
"@tanstack/react-query-persist-client": "^5.25.0",
|
"@tanstack/react-query-persist-client": "^5.25.0",
|
||||||
@@ -130,33 +130,33 @@
|
|||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"emoji-regex": "^10.4.0",
|
"emoji-regex": "^10.4.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"expo": "^53.0.5",
|
"expo": "53.0.11",
|
||||||
"expo-application": "~6.1.4",
|
"expo-application": "~6.1.4",
|
||||||
"expo-blur": "~14.1.4",
|
"expo-blur": "~14.1.5",
|
||||||
"expo-build-properties": "~0.14.6",
|
"expo-build-properties": "~0.14.6",
|
||||||
"expo-camera": "~16.1.6",
|
"expo-camera": "~16.1.8",
|
||||||
"expo-clipboard": "~7.1.4",
|
"expo-clipboard": "~7.1.4",
|
||||||
"expo-dev-client": "~5.1.7",
|
"expo-dev-client": "~5.2.0",
|
||||||
"expo-device": "~7.1.4",
|
"expo-device": "~7.1.4",
|
||||||
"expo-file-system": "~18.1.8",
|
"expo-file-system": "~18.1.10",
|
||||||
"expo-font": "~13.3.0",
|
"expo-font": "~13.3.1",
|
||||||
"expo-haptics": "~14.1.4",
|
"expo-haptics": "~14.1.4",
|
||||||
"expo-image": "~2.1.6",
|
"expo-image": "~2.2.1",
|
||||||
"expo-image-crop-tool": "^0.1.8",
|
"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-image-picker": "~16.1.4",
|
||||||
"expo-linear-gradient": "~14.1.4",
|
"expo-linear-gradient": "~14.1.5",
|
||||||
"expo-linking": "~7.1.4",
|
"expo-linking": "~7.1.5",
|
||||||
"expo-localization": "~16.1.5",
|
"expo-localization": "~16.1.5",
|
||||||
"expo-media-library": "~17.1.6",
|
"expo-media-library": "~17.1.7",
|
||||||
"expo-notifications": "~0.31.1",
|
"expo-notifications": "~0.31.3",
|
||||||
"expo-screen-orientation": "~8.1.5",
|
"expo-screen-orientation": "~8.1.7",
|
||||||
"expo-sharing": "~13.1.5",
|
"expo-sharing": "~13.1.5",
|
||||||
"expo-splash-screen": "~0.30.8",
|
"expo-splash-screen": "~0.30.9",
|
||||||
"expo-system-ui": "~5.0.7",
|
"expo-system-ui": "~5.0.8",
|
||||||
"expo-task-manager": "~13.1.5",
|
"expo-task-manager": "~13.1.5",
|
||||||
"expo-updates": "~0.28.12",
|
"expo-updates": "~0.28.14",
|
||||||
"expo-video": "~2.1.8",
|
"expo-video": "~2.2.1",
|
||||||
"expo-web-browser": "~14.1.6",
|
"expo-web-browser": "~14.1.6",
|
||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
@@ -182,35 +182,34 @@
|
|||||||
"react-image-crop": "^11.0.7",
|
"react-image-crop": "^11.0.7",
|
||||||
"react-is": "19",
|
"react-is": "19",
|
||||||
"react-keyed-flatten-children": "^5.0.0",
|
"react-keyed-flatten-children": "^5.0.0",
|
||||||
"react-native": "0.79.2",
|
"react-native": "^0.79.3",
|
||||||
"react-native-compressor": "1.11.0",
|
"react-native-compressor": "^1.11.0",
|
||||||
"react-native-date-picker": "^5.0.12",
|
"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-edge-to-edge": "^1.6.0",
|
||||||
"react-native-gesture-handler": "2.25.0",
|
"react-native-gesture-handler": "2.25.0",
|
||||||
"react-native-get-random-values": "~1.11.0",
|
"react-native-get-random-values": "~1.11.0",
|
||||||
"react-native-ios-context-menu": "^1.15.3",
|
"react-native-ios-context-menu": "^1.15.3",
|
||||||
"react-native-keyboard-controller": "^1.17.1",
|
"react-native-keyboard-controller": "^1.17.1",
|
||||||
"react-native-mmkv": "^2.12.2",
|
"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-progress": "bluesky-social/react-native-progress",
|
||||||
"react-native-qrcode-styled": "^0.3.3",
|
"react-native-qrcode-styled": "^0.3.3",
|
||||||
"react-native-reanimated": "~3.17.5",
|
"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-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "^4.11.1",
|
"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-uitextview": "^1.4.0",
|
||||||
"react-native-url-polyfill": "^1.3.0",
|
"react-native-url-polyfill": "^1.3.0",
|
||||||
"react-native-uuid": "^2.0.3",
|
"react-native-uuid": "^2.0.3",
|
||||||
"react-native-view-shot": "^4.0.3",
|
"react-native-view-shot": "^4.0.3",
|
||||||
"react-native-web": "~0.20.0",
|
"react-native-web": "~0.20.0",
|
||||||
"react-native-web-webview": "^1.0.2",
|
"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-remove-scroll-bar": "^2.3.8",
|
||||||
"react-responsive": "^9.0.2",
|
"react-responsive": "^9.0.2",
|
||||||
"react-textarea-autosize": "^8.5.3",
|
"react-textarea-autosize": "^8.5.3",
|
||||||
"rn-fetch-blob": "^0.12.0",
|
|
||||||
"statsig-react-native-expo": "^4.6.1",
|
"statsig-react-native-expo": "^4.6.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tlds": "^1.234.0",
|
"tlds": "^1.234.0",
|
||||||
@@ -227,8 +226,9 @@
|
|||||||
"@lingui/cli": "^4.14.1",
|
"@lingui/cli": "^4.14.1",
|
||||||
"@lingui/macro": "^4.14.1",
|
"@lingui/macro": "^4.14.1",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||||
"@react-native/eslint-config": "^0.79.2",
|
"@react-native/babel-preset": "0.79.3",
|
||||||
"@react-native/typescript-config": "^0.79.2",
|
"@react-native/eslint-config": "^0.79.3",
|
||||||
|
"@react-native/typescript-config": "^0.79.3",
|
||||||
"@sentry/webpack-plugin": "^3.2.2",
|
"@sentry/webpack-plugin": "^3.2.2",
|
||||||
"@testing-library/jest-native": "^5.4.3",
|
"@testing-library/jest-native": "^5.4.3",
|
||||||
"@testing-library/react-native": "^13.2.0",
|
"@testing-library/react-native": "^13.2.0",
|
||||||
@@ -260,7 +260,7 @@
|
|||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expo": "~53.0.3",
|
"jest-expo": "~53.0.7",
|
||||||
"jest-junit": "^16.0.0",
|
"jest-junit": "^16.0.0",
|
||||||
"lint-staged": "^13.2.3",
|
"lint-staged": "^13.2.3",
|
||||||
"lockfile-lint": "^4.14.0",
|
"lockfile-lint": "^4.14.0",
|
||||||
@@ -275,11 +275,11 @@
|
|||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@expo/image-utils": "0.6.3",
|
"@expo/image-utils": "0.6.3",
|
||||||
"@react-native/babel-preset": "0.79.2",
|
"@react-native/babel-preset": "0.79.3",
|
||||||
"@react-native/normalize-colors": "0.79.2",
|
"@react-native/normalize-colors": "0.79.3",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"**/expo-constants": "17.0.3",
|
"**/expo-constants": "17.0.3",
|
||||||
"**/expo-device": "7.0.1",
|
"**/expo-device": "7.1.4",
|
||||||
"**/zod": "3.23.8",
|
"**/zod": "3.23.8",
|
||||||
"**/multiformats": "9.9.0"
|
"**/multiformats": "9.9.0"
|
||||||
},
|
},
|
||||||
@@ -359,7 +359,8 @@
|
|||||||
],
|
],
|
||||||
"allowedUrls": [
|
"allowedUrls": [
|
||||||
"https://codeload.github.com/bluesky-social/react-native-bottom-sheet/tar.gz/28a87d1bb55e10fc355fa1455545a30734995908",
|
"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,
|
"emptyHostname": false,
|
||||||
"validatePackageNames": true,
|
"validatePackageNames": true,
|
||||||
|
|||||||
+109
-2
@@ -114,7 +114,7 @@ index e916023..5049c33 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - UIScrollViewDelegate
|
#pragma mark - UIScrollViewDelegate
|
||||||
@@ -62,7 +92,6 @@
|
@@ -62,7 +92,6 @@ - (void)setSmartPunctuation:(NSString *)smartPunctuation {
|
||||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||||
{
|
{
|
||||||
RCTDirectEventBlock onScroll = self.onScroll;
|
RCTDirectEventBlock onScroll = self.onScroll;
|
||||||
@@ -122,7 +122,7 @@ index e916023..5049c33 100644
|
|||||||
if (onScroll) {
|
if (onScroll) {
|
||||||
CGPoint contentOffset = scrollView.contentOffset;
|
CGPoint contentOffset = scrollView.contentOffset;
|
||||||
CGSize contentSize = scrollView.contentSize;
|
CGSize contentSize = scrollView.contentSize;
|
||||||
@@ -71,22 +100,22 @@
|
@@ -71,22 +100,22 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||||
|
|
||||||
onScroll(@{
|
onScroll(@{
|
||||||
@"contentOffset": @{
|
@"contentOffset": @{
|
||||||
@@ -155,3 +155,110 @@ index e916023..5049c33 100644
|
|||||||
},
|
},
|
||||||
@"zoomScale": @(scrollView.zoomScale ?: 1),
|
@"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
|
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
|
index e9b330f..ec5f58c 100644
|
||||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
--- 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
|
index 53bfd04..ff1b1ed 100644
|
||||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||||
+++ b/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;
|
UIColor *_titleColor;
|
||||||
CGFloat _progressViewOffset;
|
CGFloat _progressViewOffset;
|
||||||
BOOL _hasMovedToWindow;
|
BOOL _hasMovedToWindow;
|
||||||
@@ -23,7 +52,7 @@ index 53bfd04..ff1b1ed 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)init
|
- (instancetype)init
|
||||||
@@ -58,6 +59,12 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
@@ -58,6 +59,12 @@ - (void)layoutSubviews
|
||||||
_isInitialRender = false;
|
_isInitialRender = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +65,7 @@ index 53bfd04..ff1b1ed 100644
|
|||||||
- (void)didMoveToWindow
|
- (void)didMoveToWindow
|
||||||
{
|
{
|
||||||
[super 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
|
index 40aaf9c..1c60164 100644
|
||||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||||
+++ b/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(onRefresh, RCTDirectEventBlock)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
|
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
|
||||||
@@ -105,15 +134,3 @@ index 40aaf9c..1c60164 100644
|
|||||||
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
|
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
|
||||||
{
|
{
|
||||||
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
[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)
|
|
||||||
+1
-1
@@ -59,7 +59,6 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
|||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
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 * as Toast from '#/view/com/util/Toast'
|
||||||
import {Shell} from '#/view/shell'
|
import {Shell} from '#/view/shell'
|
||||||
import {ThemeProvider as Alf} from '#/alf'
|
import {ThemeProvider as Alf} from '#/alf'
|
||||||
@@ -69,6 +68,7 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
|||||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
|
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||||
import {Splash} from '#/Splash'
|
import {Splash} from '#/Splash'
|
||||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
|
|||||||
+2
-2
@@ -48,8 +48,6 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
|||||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||||
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 * as Toast from '#/view/com/util/Toast'
|
||||||
import {ToastContainer} from '#/view/com/util/Toast.web'
|
import {ToastContainer} from '#/view/com/util/Toast.web'
|
||||||
import {Shell} from '#/view/shell/index'
|
import {Shell} from '#/view/shell/index'
|
||||||
@@ -60,6 +58,8 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
|||||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
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 {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as SystemUI from 'expo-system-ui'
|
import * as SystemUI from 'expo-system-ui'
|
||||||
|
|
||||||
import {isAndroid} from '#/platform/detection'
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {Theme} from '../types'
|
import {type Theme} from '../types'
|
||||||
|
|
||||||
export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
|
export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {Pressable} from 'react-native'
|
|||||||
import Animated, {
|
import Animated, {
|
||||||
Extrapolation,
|
Extrapolation,
|
||||||
interpolate,
|
interpolate,
|
||||||
SharedValue,
|
type SharedValue,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {ScrollView} from 'react-native-gesture-handler'
|
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 {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
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 {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
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 {useProfilesQuery} from '#/state/queries/profile'
|
||||||
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import * as userActionHistory from '#/state/userActionHistory'
|
import * as userActionHistory from '#/state/userActionHistory'
|
||||||
import {SeenPost} from '#/state/userActionHistory'
|
import {type SeenPost} from '#/state/userActionHistory'
|
||||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
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 {Button} from '#/components/Button'
|
||||||
import * as FeedCard from '#/components/FeedCard'
|
import * as FeedCard from '#/components/FeedCard'
|
||||||
import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
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 {InlineLinkText} from '#/components/Link'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {ProgressGuideList} from './ProgressGuide/List'
|
import {ProgressGuideList} from './ProgressGuide/List'
|
||||||
|
|
||||||
const MOBILE_CARD_WIDTH = 300
|
const MOBILE_CARD_WIDTH = 300
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import {createContext, useCallback, useContext} from 'react'
|
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 {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {HITSLOP_30} from '#/lib/constants'
|
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 {isIOS} from '#/platform/detection'
|
||||||
import {useSetDrawerOpen} from '#/state/shell'
|
import {useSetDrawerOpen} from '#/state/shell'
|
||||||
import {
|
import {
|
||||||
atoms as a,
|
atoms as a,
|
||||||
platform,
|
platform,
|
||||||
TextStyleProp,
|
type TextStyleProp,
|
||||||
useBreakpoints,
|
useBreakpoints,
|
||||||
useGutters,
|
useGutters,
|
||||||
useLayoutBreakpoints,
|
useLayoutBreakpoints,
|
||||||
useTheme,
|
useTheme,
|
||||||
web,
|
web,
|
||||||
} from '#/alf'
|
} 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 {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
||||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
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)
|
export const Context = React.createContext<ContextType | null>(null)
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ActivityIndicator, GestureResponderEvent, Pressable} from 'react-native'
|
import {ActivityIndicator, type GestureResponderEvent, Pressable} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {EmbedPlayerParams} from '#/lib/strings/embed-player'
|
import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
|
||||||
import {isIOS, isNative, isWeb} from '#/platform/detection'
|
import {isIOS, isNative, isWeb} from '#/platform/detection'
|
||||||
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -14,7 +14,7 @@ import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
|
|||||||
import {Fill} from '#/components/Fill'
|
import {Fill} from '#/components/Fill'
|
||||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||||
|
|
||||||
export function ExternalGifEmbed({
|
export function ExternalGif({
|
||||||
link,
|
link,
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
GestureResponderEvent,
|
type GestureResponderEvent,
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
@@ -16,21 +16,21 @@ import Animated, {
|
|||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {WebView} from 'react-native-webview'
|
import {WebView} from 'react-native-webview'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player'
|
import {type EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||||
|
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
|
import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
|
||||||
import {Fill} from '#/components/Fill'
|
import {Fill} from '#/components/Fill'
|
||||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||||
import {EventStopper} from '../EventStopper'
|
|
||||||
|
|
||||||
interface ShouldStartLoadRequest {
|
interface ShouldStartLoadRequest {
|
||||||
url: string
|
url: string
|
||||||
+4
-4
@@ -1,17 +1,17 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleProp,
|
type StyleProp,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {HITSLOP_20} from '#/lib/constants'
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
import {EmbedPlayerParams} from '#/lib/strings/embed-player'
|
import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useAutoplayDisabled} from '#/state/preferences'
|
import {useAutoplayDisabled} from '#/state/preferences'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
@@ -22,7 +22,7 @@ import * as Prompt from '#/components/Prompt'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||||
import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
|
import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
|
||||||
import {GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
|
import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
|
||||||
|
|
||||||
function PlaybackControls({
|
function PlaybackControls({
|
||||||
onPress,
|
onPress,
|
||||||
+5
-5
@@ -12,16 +12,16 @@ import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
|
|||||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
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 {atoms as a, useTheme} from '#/alf'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {ExternalGif} from './ExternalGif'
|
||||||
|
import {ExternalPlayer} from './ExternalPlayer'
|
||||||
|
import {GifEmbed} from './Gif'
|
||||||
|
|
||||||
export const ExternalLinkEmbed = ({
|
export const ExternalEmbed = ({
|
||||||
link,
|
link,
|
||||||
onOpen,
|
onOpen,
|
||||||
style,
|
style,
|
||||||
@@ -106,7 +106,7 @@ export const ExternalLinkEmbed = ({
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
{embedPlayerParams?.isGif ? (
|
{embedPlayerParams?.isGif ? (
|
||||||
<ExternalGifEmbed link={link} params={embedPlayerParams} />
|
<ExternalGif link={link} params={embedPlayerParams} />
|
||||||
) : embedPlayerParams ? (
|
) : embedPlayerParams ? (
|
||||||
<ExternalPlayer link={link} params={embedPlayerParams} />
|
<ExternalPlayer link={link} params={embedPlayerParams} />
|
||||||
) : undefined}
|
) : 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 {type EmbedType} from '#/types/bsky/post'
|
||||||
|
import {type 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
@@ -1,4 +1,4 @@
|
|||||||
import {StyleProp, ViewStyle} from 'react-native'
|
import {type StyleProp, type ViewStyle} from 'react-native'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {msg, plural} from '@lingui/macro'
|
import {msg, plural} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
+3
-3
@@ -1,13 +1,12 @@
|
|||||||
import React, {useRef} from 'react'
|
import React, {useRef} from 'react'
|
||||||
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||||
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {HITSLOP_30} from '#/lib/constants'
|
import {HITSLOP_30} from '#/lib/constants'
|
||||||
import {useAutoplayDisabled} from '#/state/preferences'
|
import {useAutoplayDisabled} from '#/state/preferences'
|
||||||
import {useVideoMuteState} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
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 {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
|
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||||
import {TimeIndicator} from './TimeIndicator'
|
import {TimeIndicator} from './TimeIndicator'
|
||||||
|
|
||||||
export const VideoEmbedInnerNative = React.forwardRef(
|
export const VideoEmbedInnerNative = React.forwardRef(
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import type React from 'react'
|
||||||
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import {type SvgProps} from 'react-native-svg'
|
||||||
import {SvgProps} from 'react-native-svg'
|
import type React from 'react'
|
||||||
|
|
||||||
|
import {PressableWithHover} from '#/view/com/util/PressableWithHover'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {PressableWithHover} from '../../../PressableWithHover'
|
|
||||||
|
|
||||||
export function ControlButton({
|
export function ControlButton({
|
||||||
active,
|
active,
|
||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import type React from 'react';
|
||||||
|
|
||||||
import {isFirefox, isTouchDevice} from '#/lib/browser'
|
import {isFirefox, isTouchDevice} from '#/lib/browser'
|
||||||
import {clamp} from '#/lib/numbers'
|
import {clamp} from '#/lib/numbers'
|
||||||
+3
-2
@@ -1,14 +1,15 @@
|
|||||||
import React, {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import type React from 'react';
|
||||||
|
|
||||||
import {isSafari, isTouchDevice} from '#/lib/browser'
|
import {isSafari, isTouchDevice} from '#/lib/browser'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
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'
|
import {ControlButton} from './ControlButton'
|
||||||
|
|
||||||
export function VolumeControl({
|
export function VolumeControl({
|
||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
import {type RefObject, useCallback, useEffect, useRef, useState} from 'react'
|
||||||
|
|
||||||
import {isSafari} from '#/lib/browser'
|
import {isSafari} from '#/lib/browser'
|
||||||
import {useVideoVolumeState} from '../../VideoVolumeContext'
|
import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||||
|
|
||||||
export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
|
export function useVideoElement(ref: RefObject<HTMLVideoElement>) {
|
||||||
const [playing, setPlaying] = useState(false)
|
const [playing, setPlaying] = useState(false)
|
||||||
const [muted, setMuted] = useState(true)
|
const [muted, setMuted] = useState(true)
|
||||||
const [currentTime, setCurrentTime] = useState(0)
|
const [currentTime, setCurrentTime] = useState(0)
|
||||||
+3
-3
@@ -1,17 +1,17 @@
|
|||||||
import React, {useCallback, useState} from 'react'
|
import React, {useCallback, useState} from 'react'
|
||||||
import {ActivityIndicator, View} from 'react-native'
|
import {ActivityIndicator, View} from 'react-native'
|
||||||
import {ImageBackground} from 'expo-image'
|
import {ImageBackground} from 'expo-image'
|
||||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||||
import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
|
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 {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||||
import {ErrorBoundary} from '../ErrorBoundary'
|
import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative'
|
||||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
+8
-7
@@ -1,20 +1,21 @@
|
|||||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import type React from 'react';
|
||||||
|
|
||||||
import {isFirefox} from '#/lib/browser'
|
import {isFirefox} from '#/lib/browser'
|
||||||
|
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||||
import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
|
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 {
|
import {
|
||||||
HLSUnsupportedError,
|
HLSUnsupportedError,
|
||||||
VideoEmbedInnerWeb,
|
VideoEmbedInnerWeb,
|
||||||
VideoNotFoundError,
|
VideoNotFoundError,
|
||||||
} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb'
|
} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb'
|
||||||
import {atoms as a} from '#/alf'
|
|
||||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
|
||||||
import {useFullscreen} from '#/components/hooks/useFullscreen'
|
|
||||||
import {ErrorBoundary} from '../ErrorBoundary'
|
|
||||||
import {useActiveVideoWeb} from './ActiveVideoWebContext'
|
import {useActiveVideoWeb} from './ActiveVideoWebContext'
|
||||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||||
|
|
||||||
@@ -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']
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {ChatBskyConvoDefs} from '@atproto/api'
|
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
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 {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'
|
import {MessageContextProvider} from './MessageContext'
|
||||||
|
|
||||||
let MessageItemEmbed = ({
|
let MessageItemEmbed = ({
|
||||||
embed,
|
embed,
|
||||||
}: {
|
}: {
|
||||||
embed: AppBskyEmbedRecord.View
|
embed: $Typed<AppBskyEmbedRecord.View>
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const screen = useWindowDimensions()
|
const screen = useWindowDimensions()
|
||||||
@@ -32,7 +33,7 @@ let MessageItemEmbed = ({
|
|||||||
}),
|
}),
|
||||||
]}>
|
]}>
|
||||||
<View style={{marginTop: tokens.space.sm * -1}}>
|
<View style={{marginTop: tokens.space.sm * -1}}>
|
||||||
<PostEmbeds
|
<Embed
|
||||||
embed={embed}
|
embed={embed}
|
||||||
allowNestedQuotes
|
allowNestedQuotes
|
||||||
viewContext={PostEmbedViewContext.Feed}
|
viewContext={PostEmbedViewContext.Feed}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
AccessibilityProps,
|
type AccessibilityProps,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TextInput,
|
TextInput,
|
||||||
TextInputProps,
|
type TextInputProps,
|
||||||
TextStyle,
|
type TextStyle,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
|
||||||
import {HITSLOP_20} from '#/lib/constants'
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
@@ -16,13 +16,13 @@ import {
|
|||||||
applyFonts,
|
applyFonts,
|
||||||
atoms as a,
|
atoms as a,
|
||||||
ios,
|
ios,
|
||||||
TextStyleProp,
|
type TextStyleProp,
|
||||||
useAlf,
|
useAlf,
|
||||||
useTheme,
|
useTheme,
|
||||||
web,
|
web,
|
||||||
} from '#/alf'
|
} from '#/alf'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
const Context = React.createContext<{
|
const Context = React.createContext<{
|
||||||
@@ -196,7 +196,7 @@ export function createInput(Component: typeof TextInput) {
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
ios({paddingTop: 12, paddingBottom: 13}),
|
ios({paddingTop: 12, paddingBottom: 13}),
|
||||||
android(a.py_sm),
|
android(a.py_md),
|
||||||
// fix for autofill styles covering border
|
// fix for autofill styles covering border
|
||||||
web({
|
web({
|
||||||
paddingTop: 10,
|
paddingTop: 10,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {formatDistance, Locale} from 'date-fns'
|
import {formatDistance, type Locale} from 'date-fns'
|
||||||
import {
|
import {
|
||||||
ca,
|
ca,
|
||||||
cy,
|
cy,
|
||||||
@@ -47,7 +47,7 @@ import {
|
|||||||
zhTW,
|
zhTW,
|
||||||
} from 'date-fns/locale'
|
} from 'date-fns/locale'
|
||||||
|
|
||||||
import {AppLanguage} from '#/locale/languages'
|
import {type AppLanguage} from '#/locale/languages'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import React, {ComponentProps} from 'react'
|
import React, {type ComponentProps} from 'react'
|
||||||
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
import {
|
||||||
import {AppBskyActorDefs, ModerationCause, ModerationUI} from '@atproto/api'
|
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 {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+33
-46
@@ -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 uuid from 'react-native-uuid'
|
||||||
import {
|
import {
|
||||||
cacheDirectory,
|
cacheDirectory,
|
||||||
copyAsync,
|
copyAsync,
|
||||||
|
createDownloadResumable,
|
||||||
deleteAsync,
|
deleteAsync,
|
||||||
EncodingType,
|
EncodingType,
|
||||||
getInfoAsync,
|
getInfoAsync,
|
||||||
@@ -14,7 +15,6 @@ import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
|||||||
import * as MediaLibrary from 'expo-media-library'
|
import * as MediaLibrary from 'expo-media-library'
|
||||||
import * as Sharing from 'expo-sharing'
|
import * as Sharing from 'expo-sharing'
|
||||||
import {Buffer} from 'buffer'
|
import {Buffer} from 'buffer'
|
||||||
import RNFetchBlob from 'rn-fetch-blob'
|
|
||||||
|
|
||||||
import {POST_IMG_MAX} from '#/lib/constants'
|
import {POST_IMG_MAX} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
@@ -68,28 +68,13 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let downloadRes
|
const path = createPath(appendExt)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const downloadResPromise = RNFetchBlob.config({
|
await downloadImage(opts.uri, path, opts.timeout)
|
||||||
fileCache: true,
|
return await doResize(path, opts)
|
||||||
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)
|
|
||||||
} finally {
|
} finally {
|
||||||
// TODO Whenever we remove `rn-fetch-blob`, we will need to replace this `flush()` with a `deleteAsync()` -hailey
|
safeDeleteAsync(path)
|
||||||
if (downloadRes) {
|
|
||||||
downloadRes.flush()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,34 +83,18 @@ export async function shareImageModal({uri}: {uri: string}) {
|
|||||||
// TODO might need to give an error to the user in this case -prf
|
// TODO might need to give an error to the user in this case -prf
|
||||||
return
|
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
|
// we're currently relying on the fact our CDN only serves pngs
|
||||||
// -prf
|
// -prf
|
||||||
|
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||||
let imagePath = downloadResponse.path()
|
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||||
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
|
safeDeleteAsync(imageUri)
|
||||||
|
|
||||||
// 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, {
|
await Sharing.shareAsync(imagePath, {
|
||||||
mimeType: 'image/png',
|
mimeType: 'image/png',
|
||||||
UTI: 'image/png',
|
UTI: 'image/png',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
safeDeleteAsync(imagePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALBUM_NAME = 'Bluesky'
|
const ALBUM_NAME = 'Bluesky'
|
||||||
|
|
||||||
export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
||||||
@@ -134,11 +103,8 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
|||||||
// assuming PNG
|
// assuming PNG
|
||||||
// we're currently relying on the fact our CDN only serves pngs
|
// we're currently relying on the fact our CDN only serves pngs
|
||||||
// -prf
|
// -prf
|
||||||
const downloadResponse = await RNFetchBlob.config({
|
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||||
fileCache: true,
|
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||||
}).fetch('GET', uri)
|
|
||||||
let imagePath = downloadResponse.path()
|
|
||||||
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
|
|
||||||
|
|
||||||
// save
|
// save
|
||||||
try {
|
try {
|
||||||
@@ -403,3 +369,24 @@ export function getResizedDimensions(originalDims: {
|
|||||||
height: Math.round(originalDims.height * ratio),
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
|
import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
|
||||||
import * as bcp47Match from 'bcp-47-match'
|
import * as bcp47Match from 'bcp-47-match'
|
||||||
import lande from 'lande'
|
import lande from 'lande'
|
||||||
|
|
||||||
|
|||||||
@@ -4469,11 +4469,11 @@ msgstr ""
|
|||||||
msgid "List by {0}"
|
msgid "List by {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:156
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:160
|
||||||
msgid "List by <0/>"
|
msgid "List by <0/>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:158
|
||||||
msgid "List by you"
|
msgid "List by you"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -4740,12 +4740,12 @@ msgstr ""
|
|||||||
msgid "Moderation list by {0}"
|
msgid "Moderation list by {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:169
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:173
|
||||||
msgid "Moderation list by <0/>"
|
msgid "Moderation list by <0/>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/modals/UserAddRemoveLists.tsx:220
|
#: src/view/com/modals/UserAddRemoveLists.tsx:220
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:167
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:171
|
||||||
msgid "Moderation list by you"
|
msgid "Moderation list by you"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -5905,17 +5905,17 @@ msgstr ""
|
|||||||
msgid "Porn"
|
msgid "Porn"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/composer/Composer.tsx:1030
|
|
||||||
msgctxt "action"
|
|
||||||
msgid "Post"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/screens/PostThread/index.tsx:490
|
#: src/screens/PostThread/index.tsx:490
|
||||||
#: src/view/com/post-thread/PostThread.tsx:561
|
#: src/view/com/post-thread/PostThread.tsx:561
|
||||||
msgctxt "description"
|
msgctxt "description"
|
||||||
msgid "Post"
|
msgid "Post"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/composer/Composer.tsx:1030
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Post"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/composer/Composer.tsx:1028
|
#: src/view/com/composer/Composer.tsx:1028
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Post All"
|
msgid "Post All"
|
||||||
@@ -7681,12 +7681,12 @@ msgstr ""
|
|||||||
msgid "Starter pack by {0}"
|
msgid "Starter pack by {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:182
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:186
|
||||||
msgid "Starter pack by <0/>"
|
msgid "Starter pack by <0/>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/StarterPack/StarterPackCard.tsx:89
|
#: src/components/StarterPack/StarterPackCard.tsx:89
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:180
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:184
|
||||||
msgid "Starter pack by you"
|
msgid "Starter pack by you"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -9126,7 +9126,7 @@ msgstr ""
|
|||||||
msgid "View profile"
|
msgid "View profile"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:119
|
#: src/view/com/profile/ProfileSubpageHeader.tsx:123
|
||||||
msgid "View the avatar"
|
msgid "View the avatar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -9171,8 +9171,8 @@ msgstr ""
|
|||||||
msgid "View your verifications"
|
msgid "View your verifications"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/util/images/AutoSizedImage.tsx:199
|
#: src/view/com/util/images/AutoSizedImage.tsx:205
|
||||||
#: src/view/com/util/images/AutoSizedImage.tsx:221
|
#: src/view/com/util/images/AutoSizedImage.tsx:227
|
||||||
msgid "Views full image"
|
msgid "Views full image"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -38,4 +38,12 @@ init({
|
|||||||
*/
|
*/
|
||||||
`Network request failed`,
|
`Network request failed`,
|
||||||
],
|
],
|
||||||
|
/**
|
||||||
|
* Does not affect traces of error events or other logs, just disables
|
||||||
|
* automatically attaching stack traces to events. This helps us group events
|
||||||
|
* and prevents explosions of separate issues.
|
||||||
|
*
|
||||||
|
* @see https://docs.sentry.io/platforms/react-native/configuration/options/#attach-stacktrace
|
||||||
|
*/
|
||||||
|
attachStacktrace: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import {
|
|||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
LayoutAnimation,
|
LayoutAnimation,
|
||||||
TextInput,
|
type TextInput,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {
|
import {
|
||||||
ComAtprotoServerCreateSession,
|
ComAtprotoServerCreateSession,
|
||||||
ComAtprotoServerDescribeServer,
|
type ComAtprotoServerDescribeServer,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {Image as ExpoImage} from 'expo-image'
|
import {Image as ExpoImage} from 'expo-image'
|
||||||
import {
|
import {
|
||||||
ImagePickerOptions,
|
type ImagePickerOptions,
|
||||||
launchImageLibraryAsync,
|
launchImageLibraryAsync,
|
||||||
MediaTypeOptions,
|
MediaTypeOptions,
|
||||||
} from 'expo-image-picker'
|
} from 'expo-image-picker'
|
||||||
@@ -27,7 +27,7 @@ import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreato
|
|||||||
import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems'
|
import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems'
|
||||||
import {
|
import {
|
||||||
PlaceholderCanvas,
|
PlaceholderCanvas,
|
||||||
PlaceholderCanvasRef,
|
type PlaceholderCanvasRef,
|
||||||
} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas'
|
} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
@@ -38,7 +38,7 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components
|
|||||||
import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
|
||||||
import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive'
|
import {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {AvatarColor, avatarColors, Emoji, emojiItems} from './types'
|
import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types'
|
||||||
|
|
||||||
export interface Avatar {
|
export interface Avatar {
|
||||||
image?: {
|
image?: {
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import {type PostSource} from '#/state/unstable-post-source'
|
|||||||
import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
|
import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
|
||||||
import {Link} from '#/view/com/util/Link'
|
import {Link} from '#/view/com/util/Link'
|
||||||
import {formatCount} from '#/view/com/util/numeric/format'
|
import {formatCount} from '#/view/com/util/numeric/format'
|
||||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {
|
import {
|
||||||
LINEAR_AVI_WIDTH,
|
LINEAR_AVI_WIDTH,
|
||||||
@@ -53,6 +52,7 @@ import {ContentHider} from '#/components/moderation/ContentHider'
|
|||||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
|
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||||
import {PostControls} from '#/components/PostControls'
|
import {PostControls} from '#/components/PostControls'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
@@ -388,7 +388,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
{post.embed && (
|
{post.embed && (
|
||||||
<View style={[a.py_xs]}>
|
<View style={[a.py_xs]}>
|
||||||
<PostEmbeds
|
<Embed
|
||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
viewContext={PostEmbedViewContext.ThreadHighlighted}
|
viewContext={PostEmbedViewContext.ThreadHighlighted}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {type ThreadItem} from '#/state/queries/usePostThread/types'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {type OnPostSuccessData} from '#/state/shell/composer'
|
import {type OnPostSuccessData} from '#/state/shell/composer'
|
||||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
|
||||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +36,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
|||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {PostHider} from '#/components/moderation/PostHider'
|
import {PostHider} from '#/components/moderation/PostHider'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
|
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||||
import {PostControls} from '#/components/PostControls'
|
import {PostControls} from '#/components/PostControls'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {ShowMore} from '#/components/ShowMore'
|
import {ShowMore} from '#/components/ShowMore'
|
||||||
@@ -316,7 +316,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
{post.embed && (
|
{post.embed && (
|
||||||
<View style={[a.pb_xs]}>
|
<View style={[a.pb_xs]}>
|
||||||
<PostEmbeds
|
<Embed
|
||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
viewContext={PostEmbedViewContext.Feed}
|
viewContext={PostEmbedViewContext.Feed}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {type ThreadItem} from '#/state/queries/usePostThread/types'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {type OnPostSuccessData} from '#/state/shell/composer'
|
import {type OnPostSuccessData} from '#/state/shell/composer'
|
||||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
|
||||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||||
import {
|
import {
|
||||||
OUTER_SPACE,
|
OUTER_SPACE,
|
||||||
@@ -36,6 +35,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
|||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {PostHider} from '#/components/moderation/PostHider'
|
import {PostHider} from '#/components/moderation/PostHider'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
|
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||||
import {PostControls} from '#/components/PostControls'
|
import {PostControls} from '#/components/PostControls'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {ShowMore} from '#/components/ShowMore'
|
import {ShowMore} from '#/components/ShowMore'
|
||||||
@@ -363,7 +363,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
{post.embed && (
|
{post.embed && (
|
||||||
<View style={[a.pb_xs]}>
|
<View style={[a.pb_xs]}>
|
||||||
<PostEmbeds
|
<Embed
|
||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
viewContext={PostEmbedViewContext.Feed}
|
viewContext={PostEmbedViewContext.Feed}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
import {type AppBskyActorDefs} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
|
import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
import {Shadow} from '#/state/cache/types'
|
import {type Shadow} from '#/state/cache/types'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {NewskieDialog} from '#/components/NewskieDialog'
|
import {NewskieDialog} from '#/components/NewskieDialog'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, {memo, useEffect} from 'react'
|
import {memo, useCallback, useEffect, useMemo} from 'react'
|
||||||
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||||
import {
|
import Animated, {
|
||||||
|
measure,
|
||||||
type MeasuredDimensions,
|
type MeasuredDimensions,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
runOnUI,
|
runOnUI,
|
||||||
|
useAnimatedRef,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api'
|
import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api'
|
||||||
@@ -14,7 +16,6 @@ import {useNavigation} from '@react-navigation/native'
|
|||||||
import {useActorStatus} from '#/lib/actor-status'
|
import {useActorStatus} from '#/lib/actor-status'
|
||||||
import {BACK_HITSLOP} from '#/lib/constants'
|
import {BACK_HITSLOP} from '#/lib/constants'
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
@@ -59,9 +60,9 @@ let ProfileHeaderShell = ({
|
|||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const liveStatusControl = useDialogControl()
|
const liveStatusControl = useDialogControl()
|
||||||
|
|
||||||
const aviRef = useHandleRef()
|
const aviRef = useAnimatedRef()
|
||||||
|
|
||||||
const onPressBack = React.useCallback(() => {
|
const onPressBack = useCallback(() => {
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
} else {
|
} else {
|
||||||
@@ -69,7 +70,7 @@ let ProfileHeaderShell = ({
|
|||||||
}
|
}
|
||||||
}, [navigation])
|
}, [navigation])
|
||||||
|
|
||||||
const _openLightbox = React.useCallback(
|
const _openLightbox = useCallback(
|
||||||
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
||||||
openLightbox({
|
openLightbox({
|
||||||
images: [
|
images: [
|
||||||
@@ -92,7 +93,7 @@ let ProfileHeaderShell = ({
|
|||||||
[openLightbox],
|
[openLightbox],
|
||||||
)
|
)
|
||||||
|
|
||||||
const isMe = React.useMemo(
|
const isMe = useMemo(
|
||||||
() => currentAccount?.did === profile.did,
|
() => currentAccount?.did === profile.did,
|
||||||
[currentAccount, profile],
|
[currentAccount, profile],
|
||||||
)
|
)
|
||||||
@@ -109,7 +110,7 @@ let ProfileHeaderShell = ({
|
|||||||
}
|
}
|
||||||
}, [live.isActive, profile.did])
|
}, [live.isActive, profile.did])
|
||||||
|
|
||||||
const onPressAvi = React.useCallback(() => {
|
const onPressAvi = useCallback(() => {
|
||||||
if (live.isActive) {
|
if (live.isActive) {
|
||||||
playHaptic('Light')
|
playHaptic('Light')
|
||||||
logger.metric(
|
logger.metric(
|
||||||
@@ -122,10 +123,9 @@ let ProfileHeaderShell = ({
|
|||||||
const modui = moderation.ui('avatar')
|
const modui = moderation.ui('avatar')
|
||||||
const avatar = profile.avatar
|
const avatar = profile.avatar
|
||||||
if (avatar && !(modui.blur && modui.noOverride)) {
|
if (avatar && !(modui.blur && modui.noOverride)) {
|
||||||
const aviHandle = aviRef.current
|
|
||||||
runOnUI(() => {
|
runOnUI(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const rect = measureHandle(aviHandle)
|
const rect = measure(aviRef)
|
||||||
runOnJS(_openLightbox)(avatar, rect)
|
runOnJS(_openLightbox)(avatar, rect)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,7 @@ let ProfileHeaderShell = ({
|
|||||||
styles.avi,
|
styles.avi,
|
||||||
profile.associated?.labeler && styles.aviLabeler,
|
profile.associated?.labeler && styles.aviLabeler,
|
||||||
]}>
|
]}>
|
||||||
<View ref={aviRef} collapsable={false}>
|
<Animated.View ref={aviRef} collapsable={false}>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||||
size={live.isActive ? 88 : 90}
|
size={live.isActive ? 88 : 90}
|
||||||
@@ -231,7 +231,7 @@ let ProfileHeaderShell = ({
|
|||||||
moderation={moderation.ui('avatar')}
|
moderation={moderation.ui('avatar')}
|
||||||
/>
|
/>
|
||||||
{live.isActive && <LiveIndicator size="large" />}
|
{live.isActive && <LiveIndicator size="large" />}
|
||||||
</View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
</GrowableAvatar>
|
</GrowableAvatar>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {ReactElement} from 'react'
|
import {type ReactElement} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {ComAtprotoServerDescribeServer} from '@atproto/api'
|
import {type ComAtprotoServerDescribeServer} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
AppBskyGraphDefs,
|
AppBskyGraphDefs,
|
||||||
AppBskyGraphStarterpack,
|
AppBskyGraphStarterpack,
|
||||||
AtUri,
|
AtUri,
|
||||||
ModerationOpts,
|
type ModerationOpts,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
import {type GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||||
import {msg, plural} from '@lingui/macro'
|
import {msg, plural} from '@lingui/macro'
|
||||||
|
|
||||||
import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
|
import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {Modal, View} from 'react-native'
|
|||||||
import {SystemBars} from 'react-native-edge-to-edge'
|
import {SystemBars} from 'react-native-edge-to-edge'
|
||||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
|
import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {ComAtprotoAdminDefs, ComAtprotoModerationDefs} from '@atproto/api'
|
import {type ComAtprotoAdminDefs, ComAtprotoModerationDefs} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import {View} from 'react-native'
|
|||||||
import {
|
import {
|
||||||
Gesture,
|
Gesture,
|
||||||
GestureDetector,
|
GestureDetector,
|
||||||
NativeGesture,
|
type NativeGesture,
|
||||||
} from 'react-native-gesture-handler'
|
} from 'react-native-gesture-handler'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
interpolate,
|
interpolate,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
runOnUI,
|
runOnUI,
|
||||||
SharedValue,
|
type SharedValue,
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -20,11 +20,11 @@ import {
|
|||||||
useSafeAreaInsets,
|
useSafeAreaInsets,
|
||||||
} from 'react-native-safe-area-context'
|
} from 'react-native-safe-area-context'
|
||||||
import {useEventListener} from 'expo'
|
import {useEventListener} from 'expo'
|
||||||
import {VideoPlayer} from 'expo-video'
|
import {type VideoPlayer} from 'expo-video'
|
||||||
|
|
||||||
import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils'
|
|
||||||
import {tokens} from '#/alf'
|
import {tokens} from '#/alf'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
import {formatTime} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
// magic number that is roughly the min height of the write reply button
|
// magic number that is roughly the min height of the write reply button
|
||||||
|
|||||||
Vendored
+2
-10
@@ -15,7 +15,6 @@ import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/q
|
|||||||
import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread'
|
import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread'
|
||||||
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
|
import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
|
||||||
import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
|
import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
|
||||||
import {useProfileShadow} from './profile-shadow'
|
|
||||||
import {castAsShadow, type Shadow} from './types'
|
import {castAsShadow, type Shadow} from './types'
|
||||||
export type {Shadow} from './types'
|
export type {Shadow} from './types'
|
||||||
|
|
||||||
@@ -45,10 +44,6 @@ export function usePostShadow(
|
|||||||
setShadow(shadows.get(post))
|
setShadow(shadows.get(post))
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorShadow = useProfileShadow(post.author)
|
|
||||||
const wasMuted = !!authorShadow.viewer?.muted
|
|
||||||
const wasBlocked = !!authorShadow.viewer?.blocking
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onUpdate() {
|
function onUpdate() {
|
||||||
setShadow(shadows.get(post))
|
setShadow(shadows.get(post))
|
||||||
@@ -60,18 +55,15 @@ export function usePostShadow(
|
|||||||
}, [post, setShadow])
|
}, [post, setShadow])
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
if (wasMuted || wasBlocked) {
|
|
||||||
return POST_TOMBSTONE
|
|
||||||
}
|
|
||||||
if (shadow) {
|
if (shadow) {
|
||||||
return mergeShadow(post, shadow)
|
return mergeShadow(post, shadow)
|
||||||
} else {
|
} else {
|
||||||
return castAsShadow(post)
|
return castAsShadow(post)
|
||||||
}
|
}
|
||||||
}, [post, shadow, wasMuted, wasBlocked])
|
}, [post, shadow])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeShadow(
|
function mergeShadow(
|
||||||
post: AppBskyFeedDefs.PostView,
|
post: AppBskyFeedDefs.PostView,
|
||||||
shadow: Partial<PostShadow>,
|
shadow: Partial<PostShadow>,
|
||||||
): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE {
|
): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {BskyAgent, ChatBskyConvoGetLog} from '@atproto/api'
|
import {type BskyAgent, type ChatBskyConvoGetLog} from '@atproto/api'
|
||||||
import EventEmitter from 'eventemitter3'
|
import EventEmitter from 'eventemitter3'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
@@ -9,11 +9,11 @@ import {
|
|||||||
DEFAULT_POLL_INTERVAL,
|
DEFAULT_POLL_INTERVAL,
|
||||||
} from '#/state/messages/events/const'
|
} from '#/state/messages/events/const'
|
||||||
import {
|
import {
|
||||||
MessagesEventBusDispatch,
|
type MessagesEventBusDispatch,
|
||||||
MessagesEventBusDispatchEvent,
|
MessagesEventBusDispatchEvent,
|
||||||
MessagesEventBusErrorCode,
|
MessagesEventBusErrorCode,
|
||||||
MessagesEventBusEvent,
|
type MessagesEventBusEvent,
|
||||||
MessagesEventBusParams,
|
type MessagesEventBusParams,
|
||||||
MessagesEventBusStatus,
|
MessagesEventBusStatus,
|
||||||
} from '#/state/messages/events/types'
|
} from '#/state/messages/events/types'
|
||||||
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
$Typed,
|
type $Typed,
|
||||||
AppBskyEmbedRecord,
|
AppBskyEmbedRecord,
|
||||||
AppBskyEmbedRecordWithMedia,
|
AppBskyEmbedRecordWithMedia,
|
||||||
AppBskyFeedDefs,
|
type AppBskyFeedDefs,
|
||||||
AppBskyFeedPostgate,
|
type AppBskyFeedPostgate,
|
||||||
AtUri,
|
AtUri,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
|
||||||
@@ -113,6 +113,7 @@ export function createEmbedViewRecordFromPost(
|
|||||||
likeCount: post.likeCount,
|
likeCount: post.likeCount,
|
||||||
quoteCount: post.quoteCount,
|
quoteCount: post.quoteCount,
|
||||||
indexedAt: post.indexedAt,
|
indexedAt: post.indexedAt,
|
||||||
|
embeds: post.embed ? [post.embed] : [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -499,10 +499,9 @@ function useProfileBlockMutation() {
|
|||||||
{subject: did, createdAt: new Date().toISOString()},
|
{subject: did, createdAt: new Date().toISOString()},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onSuccess(data, {did}) {
|
onSuccess(_, {did}) {
|
||||||
queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()})
|
queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()})
|
||||||
resetProfilePostsQueries(queryClient, did, 1000)
|
resetProfilePostsQueries(queryClient, did, 1000)
|
||||||
updateProfileShadow(queryClient, did, {blockingUri: data.uri})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -524,7 +523,6 @@ function useProfileUnblockMutation() {
|
|||||||
},
|
},
|
||||||
onSuccess(_, {did}) {
|
onSuccess(_, {did}) {
|
||||||
resetProfilePostsQueries(queryClient, did, 1000)
|
resetProfilePostsQueries(queryClient, did, 1000)
|
||||||
updateProfileShadow(queryClient, did, {blockingUri: undefined})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {LogEvents} from '#/lib/statsig/statsig'
|
import {type LogEvents} from '#/lib/statsig/statsig'
|
||||||
import {PersistedAccount} from '#/state/persisted'
|
import {type PersistedAccount} from '#/state/persisted'
|
||||||
|
|
||||||
export type SessionAccount = PersistedAccount
|
export type SessionAccount = PersistedAccount
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {AppBskyFeedThreadgate} from '@atproto/api'
|
import {type AppBskyFeedThreadgate} from '@atproto/api'
|
||||||
|
|
||||||
type StateContext = {
|
type StateContext = {
|
||||||
uris: Set<string>
|
uris: Set<string>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
|||||||
import {mimeToExt} from '#/lib/media/video/util'
|
import {mimeToExt} from '#/lib/media/video/util'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {colors, s} from '#/lib/styles'
|
import {colors} from '#/lib/styles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
|
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
|
||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
@@ -97,6 +97,7 @@ import {
|
|||||||
ExternalEmbedGif,
|
ExternalEmbedGif,
|
||||||
ExternalEmbedLink,
|
ExternalEmbedLink,
|
||||||
} from '#/view/com/composer/ExternalEmbed'
|
} from '#/view/com/composer/ExternalEmbed'
|
||||||
|
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
|
||||||
import {GifAltTextDialog} from '#/view/com/composer/GifAltText'
|
import {GifAltTextDialog} from '#/view/com/composer/GifAltText'
|
||||||
import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
|
import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
|
||||||
import {Gallery} from '#/view/com/composer/photos/Gallery'
|
import {Gallery} from '#/view/com/composer/photos/Gallery'
|
||||||
@@ -116,7 +117,6 @@ import {SelectVideoBtn} from '#/view/com/composer/videos/SelectVideoBtn'
|
|||||||
import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog'
|
import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog'
|
||||||
import {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
|
import {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
|
||||||
import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress'
|
import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress'
|
||||||
import {LazyQuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed'
|
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
@@ -125,6 +125,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
|||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
|
import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text as NewText} from '#/components/Typography'
|
import {Text as NewText} from '#/components/Typography'
|
||||||
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
|
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
|
||||||
@@ -1149,13 +1150,17 @@ function ComposerEmbeds({
|
|||||||
)}
|
)}
|
||||||
</LayoutAnimationConfig>
|
</LayoutAnimationConfig>
|
||||||
{embed.quote?.uri ? (
|
{embed.quote?.uri ? (
|
||||||
<View style={!video ? [a.mt_md] : []}>
|
<View
|
||||||
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
|
style={[a.pb_sm, video ? [a.pt_md] : [a.pt_xl], isWeb && [a.pb_md]]}>
|
||||||
|
<View style={[a.relative]}>
|
||||||
<View style={{pointerEvents: 'none'}}>
|
<View style={{pointerEvents: 'none'}}>
|
||||||
<LazyQuoteEmbed uri={embed.quote.uri} />
|
<LazyQuoteEmbed uri={embed.quote.uri} />
|
||||||
</View>
|
</View>
|
||||||
{canRemoveQuote && (
|
{canRemoveQuote && (
|
||||||
<QuoteX onRemove={() => dispatch({type: 'embed_remove_quote'})} />
|
<ExternalEmbedRemoveBtn
|
||||||
|
onRemove={() => dispatch({type: 'embed_remove_quote'})}
|
||||||
|
style={{top: 16}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {type ComposerOptsPostRef} from '#/state/shell/composer'
|
import {type ComposerOptsPostRef} from '#/state/shell/composer'
|
||||||
import {MaybeQuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed'
|
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
|
import {QuoteEmbed} from '#/components/Post/Embed'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {useSimpleVerificationState} from '#/components/verification'
|
import {useSimpleVerificationState} from '#/components/verification'
|
||||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||||
|
import {parseEmbed} from '#/types/bsky/post'
|
||||||
|
|
||||||
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
|
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -51,6 +52,12 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}, [embed])
|
}, [embed])
|
||||||
|
const parsedQuoteEmbed = quoteEmbed
|
||||||
|
? parseEmbed({
|
||||||
|
$type: 'app.bsky.embed.record#view',
|
||||||
|
...quoteEmbed,
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
|
||||||
const images = useMemo(() => {
|
const images = useMemo(() => {
|
||||||
if (AppBskyEmbedImages.isView(embed)) {
|
if (AppBskyEmbedImages.isView(embed)) {
|
||||||
@@ -124,7 +131,9 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
|
|||||||
<ComposerReplyToImages images={images} showFull={showFull} />
|
<ComposerReplyToImages images={images} showFull={showFull} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
{showFull && quoteEmbed && <MaybeQuoteEmbed embed={quoteEmbed} />}
|
{showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && (
|
||||||
|
<QuoteEmbed embed={parsedQuoteEmbed} />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {
|
import {
|
||||||
useResolveGifQuery,
|
useResolveGifQuery,
|
||||||
useResolveLinkQuery,
|
useResolveLinkQuery,
|
||||||
} from '#/state/queries/resolve-link'
|
} from '#/state/queries/resolve-link'
|
||||||
import {Gif} from '#/state/queries/tenor'
|
import {type Gif} from '#/state/queries/tenor'
|
||||||
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
|
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
|
||||||
import {ExternalLinkEmbed} from '#/view/com/util/post-embeds/ExternalLinkEmbed'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed'
|
||||||
|
import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed'
|
||||||
|
import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed'
|
||||||
import {Embed as StarterPackEmbed} from '#/components/StarterPack/StarterPackCard'
|
import {Embed as StarterPackEmbed} from '#/components/StarterPack/StarterPackCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {MaybeFeedCard, MaybeListCard} from '../util/post-embeds'
|
|
||||||
|
|
||||||
export const ExternalEmbedGif = ({
|
export const ExternalEmbedGif = ({
|
||||||
onRemove,
|
onRemove,
|
||||||
@@ -44,7 +45,7 @@ export const ExternalEmbedGif = ({
|
|||||||
<View style={[a.overflow_hidden, t.atoms.border_contrast_medium]}>
|
<View style={[a.overflow_hidden, t.atoms.border_contrast_medium]}>
|
||||||
{linkInfo ? (
|
{linkInfo ? (
|
||||||
<View style={{pointerEvents: 'auto'}}>
|
<View style={{pointerEvents: 'auto'}}>
|
||||||
<ExternalLinkEmbed link={linkInfo} hideAlt />
|
<ExternalEmbed link={linkInfo} hideAlt />
|
||||||
</View>
|
</View>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<Container style={[a.align_start, a.p_md, a.gap_xs]}>
|
<Container style={[a.align_start, a.p_md, a.gap_xs]}>
|
||||||
@@ -80,7 +81,7 @@ export const ExternalEmbedLink = ({
|
|||||||
if (data) {
|
if (data) {
|
||||||
if (data.type === 'external') {
|
if (data.type === 'external') {
|
||||||
return (
|
return (
|
||||||
<ExternalLinkEmbed
|
<ExternalEmbed
|
||||||
link={{
|
link={{
|
||||||
title: data.title || uri,
|
title: data.title || uri,
|
||||||
uri,
|
uri,
|
||||||
@@ -91,9 +92,29 @@ export const ExternalEmbedLink = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (data.kind === 'feed') {
|
} else if (data.kind === 'feed') {
|
||||||
return <MaybeFeedCard view={data.view} />
|
return (
|
||||||
|
<ModeratedFeedEmbed
|
||||||
|
embed={{
|
||||||
|
type: 'feed',
|
||||||
|
view: {
|
||||||
|
$type: 'app.bsky.feed.defs#generatorView',
|
||||||
|
...data.view,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
} else if (data.kind === 'list') {
|
} else if (data.kind === 'list') {
|
||||||
return <MaybeListCard view={data.view} />
|
return (
|
||||||
|
<ModeratedListEmbed
|
||||||
|
embed={{
|
||||||
|
type: 'list',
|
||||||
|
view: {
|
||||||
|
$type: 'app.bsky.graph.defs#listView',
|
||||||
|
...data.view,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
} else if (data.kind === 'starter-pack') {
|
} else if (data.kind === 'starter-pack') {
|
||||||
return <StarterPackEmbed starterPack={data.view} />
|
return <StarterPackEmbed starterPack={data.view} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,27 @@ import {View} from 'react-native'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
|
|
||||||
export function ExternalEmbedRemoveBtn({onRemove}: {onRemove: () => void}) {
|
export function ExternalEmbedRemoveBtn({
|
||||||
|
onRemove,
|
||||||
|
style,
|
||||||
|
}: {onRemove: () => void} & ViewStyleProp) {
|
||||||
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.absolute, {top: 8, right: 8}, a.z_50]}>
|
<View style={[a.absolute, {top: 8, right: 8}, a.z_50, style]}>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Remove attachment`)}
|
label={_(msg`Remove attachment`)}
|
||||||
onPress={onRemove}
|
onPress={onRemove}
|
||||||
size="small"
|
size="small"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
shape="round">
|
shape="round"
|
||||||
|
style={[t.atoms.shadow_sm]}>
|
||||||
<ButtonIcon icon={X} size="sm" />
|
<ButtonIcon icon={X} size="sm" />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -6,23 +6,23 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants'
|
import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants'
|
||||||
import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
|
import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
|
||||||
import {
|
import {
|
||||||
EmbedPlayerParams,
|
type EmbedPlayerParams,
|
||||||
parseEmbedPlayerFromUrl,
|
parseEmbedPlayerFromUrl,
|
||||||
} from '#/lib/strings/embed-player'
|
} from '#/lib/strings/embed-player'
|
||||||
import {isAndroid} from '#/platform/detection'
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {useResolveGifQuery} from '#/state/queries/resolve-link'
|
import {useResolveGifQuery} from '#/state/queries/resolve-link'
|
||||||
import {Gif} from '#/state/queries/tenor'
|
import {type Gif} from '#/state/queries/tenor'
|
||||||
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
|
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {DialogControlProps} from '#/components/Dialog'
|
import {type DialogControlProps} from '#/components/Dialog'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
|
import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {GifEmbed} from '../util/post-embeds/GifEmbed'
|
|
||||||
import {AltTextReminder} from './photos/Gallery'
|
import {AltTextReminder} from './photos/Gallery'
|
||||||
|
|
||||||
export function GifAltTextDialog({
|
export function GifAltTextDialog({
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ADULT_CONTENT_LABELS,
|
ADULT_CONTENT_LABELS,
|
||||||
AdultSelfLabel,
|
type AdultSelfLabel,
|
||||||
OTHER_SELF_LABELS,
|
OTHER_SELF_LABELS,
|
||||||
OtherSelfLabel,
|
type OtherSelfLabel,
|
||||||
SelfLabel,
|
type SelfLabel,
|
||||||
} from '#/lib/moderation'
|
} from '#/lib/moderation'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ImageStyle, useWindowDimensions, View} from 'react-native'
|
import {type ImageStyle, useWindowDimensions, View} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {msg, Plural, Trans} from '@lingui/macro'
|
import {msg, Plural, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -7,12 +7,12 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {MAX_ALT_TEXT} from '#/lib/constants'
|
import {MAX_ALT_TEXT} from '#/lib/constants'
|
||||||
import {enforceLen} from '#/lib/strings/helpers'
|
import {enforceLen} from '#/lib/strings/helpers'
|
||||||
import {isAndroid, isWeb} from '#/platform/detection'
|
import {isAndroid, isWeb} from '#/platform/detection'
|
||||||
import {ComposerImage} from '#/state/gallery'
|
import {type ComposerImage} from '#/state/gallery'
|
||||||
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
|
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {DialogControlProps} from '#/components/Dialog'
|
import {type DialogControlProps} from '#/components/Dialog'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {useCameraPermission} from '#/lib/hooks/usePermissions'
|
|||||||
import {openCamera} from '#/lib/media/picker'
|
import {openCamera} from '#/lib/media/picker'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isMobileWeb, isNative} from '#/platform/detection'
|
import {isMobileWeb, isNative} from '#/platform/detection'
|
||||||
import {ComposerImage, createComposerImage} from '#/state/gallery'
|
import {type ComposerImage, createComposerImage} from '#/state/gallery'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera'
|
import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
ComponentProps,
|
type ComponentProps,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -7,16 +7,16 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
NativeSyntheticEvent,
|
type NativeSyntheticEvent,
|
||||||
Text as RNText,
|
Text as RNText,
|
||||||
TextInput as RNTextInput,
|
type TextInput as RNTextInput,
|
||||||
TextInputSelectionChangeEventData,
|
type TextInputSelectionChangeEventData,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||||
import PasteInput, {
|
import PasteInput, {
|
||||||
PastedFile,
|
type PastedFile,
|
||||||
PasteInputRef,
|
type PasteInputRef, // @ts-expect-error no types when installing from github
|
||||||
} from '@mattermost/react-native-paste-input'
|
} from '@mattermost/react-native-paste-input'
|
||||||
|
|
||||||
import {POST_IMG_MAX} from '#/lib/constants'
|
import {POST_IMG_MAX} from '#/lib/constants'
|
||||||
@@ -27,7 +27,7 @@ import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip'
|
|||||||
import {useTheme} from '#/lib/ThemeContext'
|
import {useTheme} from '#/lib/ThemeContext'
|
||||||
import {isAndroid, isNative} from '#/platform/detection'
|
import {isAndroid, isNative} from '#/platform/detection'
|
||||||
import {
|
import {
|
||||||
LinkFacetMatch,
|
type LinkFacetMatch,
|
||||||
suggestLinkCardUri,
|
suggestLinkCardUri,
|
||||||
} from '#/view/com/composer/text-input/text-input-util'
|
} from '#/view/com/composer/text-input/text-input-util'
|
||||||
import {atoms as a, useAlf} from '#/alf'
|
import {atoms as a, useAlf} from '#/alf'
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import React, {forwardRef, useCallback, useContext} from 'react'
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
||||||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||||
import PagerView, {
|
import PagerView, {
|
||||||
PagerViewOnPageScrollEventData,
|
type PagerViewOnPageScrollEventData,
|
||||||
PagerViewOnPageSelectedEvent,
|
type PagerViewOnPageSelectedEvent,
|
||||||
PagerViewOnPageSelectedEventData,
|
type PagerViewOnPageSelectedEventData,
|
||||||
PageScrollStateChangedNativeEventData,
|
type PageScrollStateChangedNativeEventData,
|
||||||
} from 'react-native-pager-view'
|
} from 'react-native-pager-view'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
SharedValue,
|
type SharedValue,
|
||||||
useEvent,
|
useEvent,
|
||||||
useHandler,
|
useHandler,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -36,8 +42,12 @@ export interface RenderTabBarFnProps {
|
|||||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
ref?: React.Ref<PagerRef>
|
||||||
initialPage?: number
|
initialPage?: number
|
||||||
renderTabBar: RenderTabBarFn
|
renderTabBar: RenderTabBarFn
|
||||||
|
// tab pressed, yet to scroll to page
|
||||||
|
onTabPressed?: (index: number) => void
|
||||||
|
// scroll settled
|
||||||
onPageSelected?: (index: number) => void
|
onPageSelected?: (index: number) => void
|
||||||
onPageScrollStateChanged?: (
|
onPageScrollStateChanged?: (
|
||||||
scrollState: 'idle' | 'dragging' | 'settling',
|
scrollState: 'idle' | 'dragging' | 'settling',
|
||||||
@@ -47,22 +57,20 @@ interface Props {
|
|||||||
|
|
||||||
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
|
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
|
||||||
|
|
||||||
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
export function Pager({
|
||||||
function PagerImpl(
|
ref,
|
||||||
{
|
|
||||||
children,
|
children,
|
||||||
initialPage = 0,
|
initialPage = 0,
|
||||||
renderTabBar,
|
renderTabBar,
|
||||||
onPageScrollStateChanged: parentOnPageScrollStateChanged,
|
|
||||||
onPageSelected: parentOnPageSelected,
|
onPageSelected: parentOnPageSelected,
|
||||||
|
onTabPressed: parentOnTabPressed,
|
||||||
|
onPageScrollStateChanged: parentOnPageScrollStateChanged,
|
||||||
testID,
|
testID,
|
||||||
}: React.PropsWithChildren<Props>,
|
}: React.PropsWithChildren<Props>) {
|
||||||
ref,
|
const [selectedPage, setSelectedPage] = useState(initialPage)
|
||||||
) {
|
const pagerView = useRef<PagerView>(null)
|
||||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
|
||||||
const pagerView = React.useRef<PagerView>(null)
|
|
||||||
|
|
||||||
const [isIdle, setIsIdle] = React.useState(true)
|
const [isIdle, setIsIdle] = useState(true)
|
||||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@@ -74,13 +82,13 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
|||||||
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
|
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
setPage: (index: number) => {
|
setPage: (index: number) => {
|
||||||
pagerView.current?.setPage(index)
|
pagerView.current?.setPage(index)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const onPageSelectedJSThread = React.useCallback(
|
const onPageSelectedJSThread = useCallback(
|
||||||
(nextPosition: number) => {
|
(nextPosition: number) => {
|
||||||
setSelectedPage(nextPosition)
|
setSelectedPage(nextPosition)
|
||||||
parentOnPageSelected?.(nextPosition)
|
parentOnPageSelected?.(nextPosition)
|
||||||
@@ -88,11 +96,12 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
|||||||
[setSelectedPage, parentOnPageSelected],
|
[setSelectedPage, parentOnPageSelected],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onTabBarSelect = React.useCallback(
|
const onTabBarSelect = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
|
parentOnTabPressed?.(index)
|
||||||
pagerView.current?.setPage(index)
|
pagerView.current?.setPage(index)
|
||||||
},
|
},
|
||||||
[pagerView],
|
[pagerView, parentOnTabPressed],
|
||||||
)
|
)
|
||||||
|
|
||||||
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
|
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
|
||||||
@@ -153,8 +162,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
|||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
|
||||||
function usePagerHandlers(
|
function usePagerHandlers(
|
||||||
handlers: {
|
handlers: {
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
import React from 'react'
|
import {
|
||||||
|
Children,
|
||||||
|
useCallback,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {flushSync} from 'react-dom'
|
import {flushSync} from 'react-dom'
|
||||||
|
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
|
||||||
|
export interface PagerRef {
|
||||||
|
setPage: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
export interface RenderTabBarFnProps {
|
export interface RenderTabBarFnProps {
|
||||||
selectedPage: number
|
selectedPage: number
|
||||||
@@ -12,30 +23,30 @@ export interface RenderTabBarFnProps {
|
|||||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
ref?: React.Ref<PagerRef>
|
||||||
initialPage?: number
|
initialPage?: number
|
||||||
renderTabBar: RenderTabBarFn
|
renderTabBar: RenderTabBarFn
|
||||||
onPageSelected?: (index: number) => void
|
onPageSelected?: (index: number) => void
|
||||||
}
|
}
|
||||||
export const Pager = React.forwardRef(function PagerImpl(
|
|
||||||
{
|
export function Pager({
|
||||||
|
ref,
|
||||||
children,
|
children,
|
||||||
initialPage = 0,
|
initialPage = 0,
|
||||||
renderTabBar,
|
renderTabBar,
|
||||||
onPageSelected,
|
onPageSelected,
|
||||||
}: React.PropsWithChildren<Props>,
|
}: React.PropsWithChildren<Props>) {
|
||||||
ref,
|
const [selectedPage, setSelectedPage] = useState(initialPage)
|
||||||
) {
|
const scrollYs = useRef<Array<number | null>>([])
|
||||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
const anchorRef = useRef(null)
|
||||||
const scrollYs = React.useRef<Array<number | null>>([])
|
|
||||||
const anchorRef = React.useRef(null)
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
setPage: (index: number) => {
|
setPage: (index: number) => {
|
||||||
onTabBarSelect(index)
|
onTabBarSelect(index)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const onTabBarSelect = React.useCallback(
|
const onTabBarSelect = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const scrollY = window.scrollY
|
const scrollY = window.scrollY
|
||||||
// We want to determine if the tabbar is already "sticking" at the top (in which
|
// We want to determine if the tabbar is already "sticking" at the top (in which
|
||||||
@@ -75,11 +86,13 @@ export const Pager = React.forwardRef(function PagerImpl(
|
|||||||
tabBarAnchor: <View ref={anchorRef} />,
|
tabBarAnchor: <View ref={anchorRef} />,
|
||||||
onSelect: e => onTabBarSelect(e),
|
onSelect: e => onTabBarSelect(e),
|
||||||
})}
|
})}
|
||||||
{React.Children.map(children, (child, i) => (
|
{Children.map(children, (child, i) => (
|
||||||
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
|
<View
|
||||||
|
style={selectedPage === i ? a.flex_1 : a.hidden}
|
||||||
|
key={`page-${i}`}>
|
||||||
{child}
|
{child}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import * as React from 'react'
|
import {memo, useCallback, useEffect, useRef, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
LayoutChangeEvent,
|
type LayoutChangeEvent,
|
||||||
NativeScrollEvent,
|
type NativeScrollEvent,
|
||||||
ScrollView,
|
type ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
AnimatedRef,
|
type AnimatedRef,
|
||||||
runOnJS,
|
|
||||||
runOnUI,
|
runOnUI,
|
||||||
scrollTo,
|
scrollTo,
|
||||||
SharedValue,
|
type SharedValue,
|
||||||
useAnimatedRef,
|
useAnimatedRef,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -20,9 +19,13 @@ import Animated, {
|
|||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
|
import {
|
||||||
|
Pager,
|
||||||
|
type PagerRef,
|
||||||
|
type RenderTabBarFnProps,
|
||||||
|
} from '#/view/com/pager/Pager'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {ListMethods} from '../util/List'
|
import {type ListMethods} from '../util/List'
|
||||||
import {PagerHeaderProvider} from './PagerHeaderContext'
|
import {PagerHeaderProvider} from './PagerHeaderContext'
|
||||||
import {TabBar} from './TabBar'
|
import {TabBar} from './TabBar'
|
||||||
|
|
||||||
@@ -33,6 +36,7 @@ export interface PagerWithHeaderChildParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PagerWithHeaderProps {
|
export interface PagerWithHeaderProps {
|
||||||
|
ref?: React.Ref<PagerRef>
|
||||||
testID?: string
|
testID?: string
|
||||||
children:
|
children:
|
||||||
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
||||||
@@ -49,9 +53,8 @@ export interface PagerWithHeaderProps {
|
|||||||
onCurrentPageSelected?: (index: number) => void
|
onCurrentPageSelected?: (index: number) => void
|
||||||
allowHeaderOverScroll?: boolean
|
allowHeaderOverScroll?: boolean
|
||||||
}
|
}
|
||||||
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
export function PagerWithHeader({
|
||||||
function PageWithHeaderImpl(
|
ref,
|
||||||
{
|
|
||||||
children,
|
children,
|
||||||
testID,
|
testID,
|
||||||
items,
|
items,
|
||||||
@@ -61,12 +64,10 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
onPageSelected,
|
onPageSelected,
|
||||||
onCurrentPageSelected,
|
onCurrentPageSelected,
|
||||||
allowHeaderOverScroll,
|
allowHeaderOverScroll,
|
||||||
}: PagerWithHeaderProps,
|
}: PagerWithHeaderProps) {
|
||||||
ref,
|
const [currentPage, setCurrentPage] = useState(0)
|
||||||
) {
|
const [tabBarHeight, setTabBarHeight] = useState(0)
|
||||||
const [currentPage, setCurrentPage] = React.useState(0)
|
const [headerOnlyHeight, setHeaderOnlyHeight] = useState(0)
|
||||||
const [tabBarHeight, setTabBarHeight] = React.useState(0)
|
|
||||||
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
|
|
||||||
const scrollY = useSharedValue(0)
|
const scrollY = useSharedValue(0)
|
||||||
const headerHeight = headerOnlyHeight + tabBarHeight
|
const headerHeight = headerOnlyHeight + tabBarHeight
|
||||||
|
|
||||||
@@ -85,12 +86,10 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderTabBar = React.useCallback(
|
const renderTabBar = useCallback(
|
||||||
(props: RenderTabBarFnProps) => {
|
(props: RenderTabBarFnProps) => {
|
||||||
return (
|
return (
|
||||||
<PagerHeaderProvider
|
<PagerHeaderProvider scrollY={scrollY} headerHeight={headerOnlyHeight}>
|
||||||
scrollY={scrollY}
|
|
||||||
headerHeight={headerOnlyHeight}>
|
|
||||||
<PagerTabBar
|
<PagerTabBar
|
||||||
headerOnlyHeight={headerOnlyHeight}
|
headerOnlyHeight={headerOnlyHeight}
|
||||||
items={items}
|
items={items}
|
||||||
@@ -126,7 +125,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
|
const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
|
||||||
const registerRef = React.useCallback(
|
const registerRef = useCallback(
|
||||||
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
|
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
|
||||||
scrollRefs.modify(refs => {
|
scrollRefs.modify(refs => {
|
||||||
'worklet'
|
'worklet'
|
||||||
@@ -138,8 +137,10 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const lastForcedScrollY = useSharedValue(0)
|
const lastForcedScrollY = useSharedValue(0)
|
||||||
const adjustScrollForOtherPages = () => {
|
const adjustScrollForOtherPages = useCallback(
|
||||||
|
(scrollState: 'idle' | 'dragging' | 'settling') => {
|
||||||
'worklet'
|
'worklet'
|
||||||
|
if (scrollState !== 'dragging') return
|
||||||
const currentScrollY = scrollY.get()
|
const currentScrollY = scrollY.get()
|
||||||
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
|
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
|
||||||
if (lastForcedScrollY.get() !== forcedScrollY) {
|
if (lastForcedScrollY.get() !== forcedScrollY) {
|
||||||
@@ -152,21 +153,11 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[currentPage, headerOnlyHeight, lastForcedScrollY, scrollRefs, scrollY],
|
||||||
const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
|
|
||||||
null,
|
|
||||||
)
|
)
|
||||||
const queueThrottledOnScroll = useNonReactiveCallback(() => {
|
|
||||||
if (!throttleTimeout.current) {
|
|
||||||
throttleTimeout.current = setTimeout(() => {
|
|
||||||
throttleTimeout.current = null
|
|
||||||
runOnUI(adjustScrollForOtherPages)()
|
|
||||||
}, 80 /* Sync often enough you're unlikely to catch it unsynced */)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const onScrollWorklet = React.useCallback(
|
const onScrollWorklet = useCallback(
|
||||||
(e: NativeScrollEvent) => {
|
(e: NativeScrollEvent) => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const nextScrollY = e.contentOffset.y
|
const nextScrollY = e.contentOffset.y
|
||||||
@@ -177,13 +168,12 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
|
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
|
||||||
if (!isPossiblyInvalid) {
|
if (!isPossiblyInvalid) {
|
||||||
scrollY.set(nextScrollY)
|
scrollY.set(nextScrollY)
|
||||||
runOnJS(queueThrottledOnScroll)()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[scrollY, queueThrottledOnScroll, headerHeight],
|
[scrollY, headerHeight],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPageSelectedInner = React.useCallback(
|
const onPageSelectedInner = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
setCurrentPage(index)
|
setCurrentPage(index)
|
||||||
onPageSelected?.(index)
|
onPageSelected?.(index)
|
||||||
@@ -191,13 +181,19 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
[onPageSelected, setCurrentPage],
|
[onPageSelected, setCurrentPage],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onTabPressed = useCallback(() => {
|
||||||
|
runOnUI(adjustScrollForOtherPages)('dragging')
|
||||||
|
}, [adjustScrollForOtherPages])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pager
|
<Pager
|
||||||
ref={ref}
|
ref={ref}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
initialPage={initialPage}
|
initialPage={initialPage}
|
||||||
|
onTabPressed={onTabPressed}
|
||||||
onPageSelected={onPageSelectedInner}
|
onPageSelected={onPageSelectedInner}
|
||||||
renderTabBar={renderTabBar}>
|
renderTabBar={renderTabBar}
|
||||||
|
onPageScrollStateChanged={adjustScrollForOtherPages}>
|
||||||
{toArray(children)
|
{toArray(children)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((child, i) => {
|
.map((child, i) => {
|
||||||
@@ -219,8 +215,7 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
})}
|
})}
|
||||||
</Pager>
|
</Pager>
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
|
||||||
let PagerTabBar = ({
|
let PagerTabBar = ({
|
||||||
currentPage,
|
currentPage,
|
||||||
@@ -258,7 +253,7 @@ let PagerTabBar = ({
|
|||||||
dragState: SharedValue<'idle' | 'dragging' | 'settling'>
|
dragState: SharedValue<'idle' | 'dragging' | 'settling'>
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0)
|
const [minimumHeaderHeight, setMinimumHeaderHeight] = useState(0)
|
||||||
const headerTransform = useAnimatedStyle(() => {
|
const headerTransform = useAnimatedStyle(() => {
|
||||||
const translateY =
|
const translateY =
|
||||||
Math.min(
|
Math.min(
|
||||||
@@ -275,7 +270,7 @@ let PagerTabBar = ({
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const headerRef = React.useRef(null)
|
const headerRef = useRef(null)
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
pointerEvents={isIOS ? 'auto' : 'box-none'}
|
pointerEvents={isIOS ? 'auto' : 'box-none'}
|
||||||
@@ -327,7 +322,7 @@ let PagerTabBar = ({
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PagerTabBar = React.memo(PagerTabBar)
|
PagerTabBar = memo(PagerTabBar)
|
||||||
|
|
||||||
function PagerItem({
|
function PagerItem({
|
||||||
headerHeight,
|
headerHeight,
|
||||||
@@ -348,7 +343,7 @@ function PagerItem({
|
|||||||
}) {
|
}) {
|
||||||
const scrollElRef = useAnimatedRef()
|
const scrollElRef = useAnimatedRef()
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
registerRef(scrollElRef, index)
|
registerRef(scrollElRef, index)
|
||||||
return () => {
|
return () => {
|
||||||
registerRef(null, index)
|
registerRef(null, index)
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
|
|||||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||||
import {Link} from '#/view/com/util/Link'
|
import {Link} from '#/view/com/util/Link'
|
||||||
import {formatCount} from '#/view/com/util/numeric/format'
|
import {formatCount} from '#/view/com/util/numeric/format'
|
||||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
|
||||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -62,6 +61,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
|||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {PostHider} from '#/components/moderation/PostHider'
|
import {PostHider} from '#/components/moderation/PostHider'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
|
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||||
import {PostControls} from '#/components/PostControls'
|
import {PostControls} from '#/components/PostControls'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
@@ -466,7 +466,7 @@ let PostThreadItemLoaded = ({
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
{post.embed && (
|
{post.embed && (
|
||||||
<View style={[a.py_xs]}>
|
<View style={[a.py_xs]}>
|
||||||
<PostEmbeds
|
<Embed
|
||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
viewContext={PostEmbedViewContext.ThreadHighlighted}
|
viewContext={PostEmbedViewContext.ThreadHighlighted}
|
||||||
@@ -693,7 +693,7 @@ let PostThreadItemLoaded = ({
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
{post.embed && (
|
{post.embed && (
|
||||||
<View style={[a.pb_xs]}>
|
<View style={[a.pb_xs]}>
|
||||||
<PostEmbeds
|
<Embed
|
||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
viewContext={PostEmbedViewContext.Feed}
|
viewContext={PostEmbedViewContext.Feed}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
|||||||
import {precacheProfile} from '#/state/queries/profile'
|
import {precacheProfile} from '#/state/queries/profile'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {Link} from '#/view/com/util/Link'
|
import {Link} from '#/view/com/util/Link'
|
||||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
|
||||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
@@ -36,6 +35,7 @@ import {atoms as a} from '#/alf'
|
|||||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
|
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||||
import {PostControls} from '#/components/PostControls'
|
import {PostControls} from '#/components/PostControls'
|
||||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
@@ -242,7 +242,7 @@ function PostInner({
|
|||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{post.embed ? (
|
{post.embed ? (
|
||||||
<PostEmbeds
|
<Embed
|
||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
viewContext={PostEmbedViewContext.Feed}
|
viewContext={PostEmbedViewContext.Feed}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import {
|
|||||||
} from '#/state/unstable-post-source'
|
} from '#/state/unstable-post-source'
|
||||||
import {FeedNameText} from '#/view/com/util/FeedInfoText'
|
import {FeedNameText} from '#/view/com/util/FeedInfoText'
|
||||||
import {Link, TextLinkOnWebOnly} from '#/view/com/util/Link'
|
import {Link, TextLinkOnWebOnly} from '#/view/com/util/Link'
|
||||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
|
||||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
@@ -53,6 +52,8 @@ import {ContentHider} from '#/components/moderation/ContentHider'
|
|||||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||||
import {type AppModerationCause} from '#/components/Pills'
|
import {type AppModerationCause} from '#/components/Pills'
|
||||||
|
import {Embed} from '#/components/Post/Embed'
|
||||||
|
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||||
import {PostControls} from '#/components/PostControls'
|
import {PostControls} from '#/components/PostControls'
|
||||||
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
|
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
|
||||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||||
@@ -562,7 +563,7 @@ let PostContent = ({
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
{postEmbed ? (
|
{postEmbed ? (
|
||||||
<View style={[a.pb_xs]}>
|
<View style={[a.pb_xs]}>
|
||||||
<PostEmbeds
|
<Embed
|
||||||
embed={postEmbed}
|
embed={postEmbed}
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
onOpen={onOpenEmbed}
|
onOpen={onOpenEmbed}
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
import Animated, {
|
||||||
import {AppBskyGraphDefs} from '@atproto/api'
|
measure,
|
||||||
|
type MeasuredDimensions,
|
||||||
|
runOnJS,
|
||||||
|
runOnUI,
|
||||||
|
useAnimatedRef,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {type AppBskyGraphDefs} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {emitSoftReset} from '#/state/events'
|
import {emitSoftReset} from '#/state/events'
|
||||||
import {useLightboxControls} from '#/state/lightbox'
|
import {useLightboxControls} from '#/state/lightbox'
|
||||||
import {TextLink} from '#/view/com/util/Link'
|
import {TextLink} from '#/view/com/util/Link'
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {UserAvatar, UserAvatarType} from '#/view/com/util/UserAvatar'
|
import {UserAvatar, type UserAvatarType} from '#/view/com/util/UserAvatar'
|
||||||
import {StarterPack} from '#/components/icons/StarterPack'
|
import {StarterPack} from '#/components/icons/StarterPack'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
|
||||||
@@ -52,7 +57,7 @@ export function ProfileSubpageHeader({
|
|||||||
const {openLightbox} = useLightboxControls()
|
const {openLightbox} = useLightboxControls()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const canGoBack = navigation.canGoBack()
|
const canGoBack = navigation.canGoBack()
|
||||||
const aviRef = useHandleRef()
|
const aviRef = useAnimatedRef()
|
||||||
|
|
||||||
const _openLightbox = React.useCallback(
|
const _openLightbox = React.useCallback(
|
||||||
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
||||||
@@ -81,10 +86,9 @@ export function ProfileSubpageHeader({
|
|||||||
if (
|
if (
|
||||||
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
||||||
) {
|
) {
|
||||||
const aviHandle = aviRef.current
|
|
||||||
runOnUI(() => {
|
runOnUI(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const rect = measureHandle(aviHandle)
|
const rect = measure(aviRef)
|
||||||
runOnJS(_openLightbox)(avatar, rect)
|
runOnJS(_openLightbox)(avatar, rect)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
@@ -111,7 +115,7 @@ export function ProfileSubpageHeader({
|
|||||||
paddingBottom: 14,
|
paddingBottom: 14,
|
||||||
paddingHorizontal: isMobile ? 12 : 14,
|
paddingHorizontal: isMobile ? 12 : 14,
|
||||||
}}>
|
}}>
|
||||||
<View ref={aviRef} collapsable={false}>
|
<Animated.View ref={aviRef} collapsable={false}>
|
||||||
<Pressable
|
<Pressable
|
||||||
testID="headerAviButton"
|
testID="headerAviButton"
|
||||||
onPress={onPressAvi}
|
onPress={onPressAvi}
|
||||||
@@ -125,7 +129,7 @@ export function ProfileSubpageHeader({
|
|||||||
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</Animated.View>
|
||||||
<View style={{flex: 1, gap: 4}}>
|
<View style={{flex: 1, gap: 4}}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoadingPlaceholder
|
<LoadingPlaceholder
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
type FontAwesomeIconStyle,
|
type FontAwesomeIconStyle,
|
||||||
type Props as FontAwesomeProps,
|
type Props as FontAwesomeProps,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import type React from 'react'
|
|
||||||
|
|
||||||
const DURATION = 3500
|
const DURATION = 3500
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,12 @@
|
|||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
FlatList,
|
type FlatList,
|
||||||
FlatListProps,
|
type FlatListProps,
|
||||||
ScrollViewProps,
|
type ScrollViewProps,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
ViewProps,
|
type ViewProps,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import Animated from 'react-native-reanimated'
|
import Animated from 'react-native-reanimated'
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user