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 RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
import {
|
||||
downloadAndResize,
|
||||
DownloadAndResizeOpts,
|
||||
type DownloadAndResizeOpts,
|
||||
getResizedDimensions,
|
||||
} from '../../src/lib/media/manip'
|
||||
|
||||
@@ -32,11 +31,12 @@ describe('downloadAndResize', () => {
|
||||
})
|
||||
|
||||
it('should return resized image for valid URI and options', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
||||
info: jest.fn().mockReturnValue({status: 200}),
|
||||
flush: jest.fn(),
|
||||
const mockedFetch = createDownloadResumable as jest.Mock
|
||||
mockedFetch.mockReturnValue({
|
||||
cancelAsync: jest.fn(),
|
||||
downloadAsync: jest
|
||||
.fn()
|
||||
.mockResolvedValue({uri: 'file://resized-image.jpg'}),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
@@ -50,13 +50,12 @@ describe('downloadAndResize', () => {
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(result).toEqual(mockResizedImage)
|
||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
||||
fileCache: true,
|
||||
appendExt: 'jpeg',
|
||||
})
|
||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'https://example.com/image.jpg',
|
||||
expect(createDownloadResumable).toHaveBeenCalledWith(
|
||||
opts.uri,
|
||||
expect.anything(),
|
||||
{
|
||||
cache: true,
|
||||
},
|
||||
)
|
||||
|
||||
// First time it gets called is to get dimensions
|
||||
@@ -86,28 +85,6 @@ describe('downloadAndResize', () => {
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for non-200 response', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
||||
info: jest.fn().mockReturnValue({status: 400}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
uri: 'https://example.com/image',
|
||||
width: 100,
|
||||
height: 100,
|
||||
maxSize: 500000,
|
||||
mode: 'cover',
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(errorSpy).not.toHaveBeenCalled()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not downsize whenever dimensions are below the max dimensions', () => {
|
||||
const initialDimensionsOne = {
|
||||
width: 1200,
|
||||
|
||||
+1
-1
@@ -219,7 +219,7 @@ module.exports = function (_config) {
|
||||
compileSdkVersion: 35,
|
||||
targetSdkVersion: 35,
|
||||
buildToolsVersion: '35.0.0',
|
||||
newArchEnabled: false,
|
||||
newArchEnabled: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Express} from 'express'
|
||||
import {type Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {default as createShortLink} from './createShortLink.js'
|
||||
import {default as health} from './health.js'
|
||||
import {default as redirect} from './redirect.js'
|
||||
|
||||
@@ -2,9 +2,9 @@ import assert from 'node:assert'
|
||||
|
||||
import {DAY, SECOND} from '@atproto/common'
|
||||
import escapeHTML from 'escape-html'
|
||||
import {Express} from 'express'
|
||||
import {type Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {handler} from './util.js'
|
||||
|
||||
const INTERNAL_IP_REGEX = new RegExp(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Express} from 'express'
|
||||
import {type Express} from 'express'
|
||||
|
||||
import {AppContext} from '../context.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {handler} from './util.js'
|
||||
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
|
||||
+2
-7
@@ -33,15 +33,10 @@ jest.mock('react-native-safe-area-context', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('rn-fetch-blob', () => ({
|
||||
config: jest.fn().mockReturnThis(),
|
||||
cancel: jest.fn(),
|
||||
fetch: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('expo-file-system', () => ({
|
||||
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
|
||||
deleteAsync: jest.fn(),
|
||||
createDownloadResumable: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('expo-image-manipulator', () => ({
|
||||
@@ -101,7 +96,7 @@ jest.mock('expo-modules-core', () => ({
|
||||
}
|
||||
}
|
||||
}),
|
||||
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
|
||||
requireNativeViewManager: jest.fn().mockImplementation(_ => {
|
||||
return () => null
|
||||
}),
|
||||
}))
|
||||
|
||||
+40
-39
@@ -69,12 +69,12 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.15.14",
|
||||
"@atproto/api": "^0.15.15",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@expo/html-elements": "^0.12.4",
|
||||
"@expo/html-elements": "^0.12.5",
|
||||
"@expo/webpack-config": "^19.0.1",
|
||||
"@floating-ui/dom": "^1.6.3",
|
||||
"@floating-ui/react-dom": "^2.0.8",
|
||||
@@ -85,12 +85,12 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||
"@haileyok/bluesky-video": "0.2.6",
|
||||
"@haileyok/bluesky-video": "0.3.1",
|
||||
"@ipld/dag-cbor": "^9.2.0",
|
||||
"@lingui/react": "^4.14.1",
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
||||
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
|
||||
"@miblanchard/react-native-slider": "^2.6.0",
|
||||
"@mozzius/expo-dynamic-app-icon": "1.5.0",
|
||||
"@react-native-async-storage/async-storage": "2.1.2",
|
||||
"@react-native-menu/menu": "^1.2.3",
|
||||
"@react-native-picker/picker": "2.11.0",
|
||||
@@ -98,7 +98,7 @@
|
||||
"@react-navigation/drawer": "^7.3.12",
|
||||
"@react-navigation/native": "^7.1.9",
|
||||
"@react-navigation/native-stack": "^7.3.13",
|
||||
"@sentry/react-native": "~6.10.0",
|
||||
"@sentry/react-native": "~6.14.0",
|
||||
"@tanstack/query-async-storage-persister": "^5.25.0",
|
||||
"@tanstack/react-query": "^5.8.1",
|
||||
"@tanstack/react-query-persist-client": "^5.25.0",
|
||||
@@ -130,33 +130,33 @@
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "^53.0.5",
|
||||
"expo": "53.0.11",
|
||||
"expo-application": "~6.1.4",
|
||||
"expo-blur": "~14.1.4",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-build-properties": "~0.14.6",
|
||||
"expo-camera": "~16.1.6",
|
||||
"expo-camera": "~16.1.8",
|
||||
"expo-clipboard": "~7.1.4",
|
||||
"expo-dev-client": "~5.1.7",
|
||||
"expo-dev-client": "~5.2.0",
|
||||
"expo-device": "~7.1.4",
|
||||
"expo-file-system": "~18.1.8",
|
||||
"expo-font": "~13.3.0",
|
||||
"expo-file-system": "~18.1.10",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-haptics": "~14.1.4",
|
||||
"expo-image": "~2.1.6",
|
||||
"expo-image": "~2.2.1",
|
||||
"expo-image-crop-tool": "^0.1.8",
|
||||
"expo-image-manipulator": "~13.1.5",
|
||||
"expo-image-manipulator": "~13.1.7",
|
||||
"expo-image-picker": "~16.1.4",
|
||||
"expo-linear-gradient": "~14.1.4",
|
||||
"expo-linking": "~7.1.4",
|
||||
"expo-linear-gradient": "~14.1.5",
|
||||
"expo-linking": "~7.1.5",
|
||||
"expo-localization": "~16.1.5",
|
||||
"expo-media-library": "~17.1.6",
|
||||
"expo-notifications": "~0.31.1",
|
||||
"expo-screen-orientation": "~8.1.5",
|
||||
"expo-media-library": "~17.1.7",
|
||||
"expo-notifications": "~0.31.3",
|
||||
"expo-screen-orientation": "~8.1.7",
|
||||
"expo-sharing": "~13.1.5",
|
||||
"expo-splash-screen": "~0.30.8",
|
||||
"expo-system-ui": "~5.0.7",
|
||||
"expo-splash-screen": "~0.30.9",
|
||||
"expo-system-ui": "~5.0.8",
|
||||
"expo-task-manager": "~13.1.5",
|
||||
"expo-updates": "~0.28.12",
|
||||
"expo-video": "~2.1.8",
|
||||
"expo-updates": "~0.28.14",
|
||||
"expo-video": "~2.2.1",
|
||||
"expo-web-browser": "~14.1.6",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
@@ -182,35 +182,34 @@
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-is": "19",
|
||||
"react-keyed-flatten-children": "^5.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-compressor": "1.11.0",
|
||||
"react-native": "^0.79.3",
|
||||
"react-native-compressor": "^1.11.0",
|
||||
"react-native-date-picker": "^5.0.12",
|
||||
"react-native-drawer-layout": "^4.1.6",
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-gesture-handler": "2.25.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.17.1",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.7.1",
|
||||
"react-native-pager-view": "^6.7.1",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
"react-native-reanimated": "~3.17.5",
|
||||
"react-native-root-siblings": "^4.1.1",
|
||||
"react-native-root-siblings": "^5.0.1",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "^4.11.1",
|
||||
"react-native-svg": "15.11.2",
|
||||
"react-native-svg": "15.12.0",
|
||||
"react-native-uitextview": "^1.4.0",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-view-shot": "^4.0.3",
|
||||
"react-native-web": "~0.20.0",
|
||||
"react-native-web-webview": "^1.0.2",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-webview": "^13.13.5",
|
||||
"react-remove-scroll-bar": "^2.3.8",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-textarea-autosize": "^8.5.3",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"statsig-react-native-expo": "^4.6.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
@@ -227,8 +226,9 @@
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@react-native/eslint-config": "^0.79.2",
|
||||
"@react-native/typescript-config": "^0.79.2",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/eslint-config": "^0.79.3",
|
||||
"@react-native/typescript-config": "^0.79.3",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.2.0",
|
||||
@@ -260,7 +260,7 @@
|
||||
"husky": "^8.0.3",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~53.0.3",
|
||||
"jest-expo": "~53.0.7",
|
||||
"jest-junit": "^16.0.0",
|
||||
"lint-staged": "^13.2.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
@@ -275,11 +275,11 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@expo/image-utils": "0.6.3",
|
||||
"@react-native/babel-preset": "0.79.2",
|
||||
"@react-native/normalize-colors": "0.79.2",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/normalize-colors": "0.79.3",
|
||||
"@types/react": "^18",
|
||||
"**/expo-constants": "17.0.3",
|
||||
"**/expo-device": "7.0.1",
|
||||
"**/expo-device": "7.1.4",
|
||||
"**/zod": "3.23.8",
|
||||
"**/multiformats": "9.9.0"
|
||||
},
|
||||
@@ -359,7 +359,8 @@
|
||||
],
|
||||
"allowedUrls": [
|
||||
"https://codeload.github.com/bluesky-social/react-native-bottom-sheet/tar.gz/28a87d1bb55e10fc355fa1455545a30734995908",
|
||||
"https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4"
|
||||
"https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4",
|
||||
"https://codeload.github.com/mattermost/react-native-paste-input/tar.gz/f260447edc645a817ab1ba7b46d8341d84dba8e9"
|
||||
],
|
||||
"emptyHostname": false,
|
||||
"validatePackageNames": true,
|
||||
|
||||
+109
-2
@@ -114,7 +114,7 @@ index e916023..5049c33 100644
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
@@ -62,7 +92,6 @@
|
||||
@@ -62,7 +92,6 @@ - (void)setSmartPunctuation:(NSString *)smartPunctuation {
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
RCTDirectEventBlock onScroll = self.onScroll;
|
||||
@@ -122,7 +122,7 @@ index e916023..5049c33 100644
|
||||
if (onScroll) {
|
||||
CGPoint contentOffset = scrollView.contentOffset;
|
||||
CGSize contentSize = scrollView.contentSize;
|
||||
@@ -71,22 +100,22 @@
|
||||
@@ -71,22 +100,22 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
|
||||
onScroll(@{
|
||||
@"contentOffset": @{
|
||||
@@ -155,3 +155,110 @@ index e916023..5049c33 100644
|
||||
},
|
||||
@"zoomScale": @(scrollView.zoomScale ?: 1),
|
||||
});
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||
index dd50053..2ed7017 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||
@@ -122,8 +122,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
|
||||
const auto &newTextInputProps = static_cast<const PasteTextInputProps &>(*props);
|
||||
|
||||
// Traits:
|
||||
- if (newTextInputProps.traits.multiline != oldTextInputProps.traits.multiline) {
|
||||
- [self _setMultiline:newTextInputProps.traits.multiline];
|
||||
+ if (newTextInputProps.multiline != oldTextInputProps.multiline) {
|
||||
+ [self _setMultiline:newTextInputProps.multiline];
|
||||
}
|
||||
|
||||
if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) {
|
||||
@@ -421,7 +421,7 @@ - (void)textInputDidChangeSelection
|
||||
return;
|
||||
}
|
||||
const auto &props = static_cast<const PasteTextInputProps &>(*_props);
|
||||
- if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
|
||||
+ if (props.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
|
||||
[self textInputDidChange];
|
||||
_ignoreNextTextInputCall = YES;
|
||||
}
|
||||
@@ -708,11 +708,11 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe
|
||||
- (SubmitBehavior)getSubmitBehavior
|
||||
{
|
||||
const auto &props = static_cast<const PasteTextInputProps &>(*_props);
|
||||
- const SubmitBehavior submitBehaviorDefaultable = props.traits.submitBehavior;
|
||||
+ const SubmitBehavior submitBehaviorDefaultable = props.submitBehavior;
|
||||
|
||||
// We should always have a non-default `submitBehavior`, but in case we don't, set it based on multiline.
|
||||
if (submitBehaviorDefaultable == SubmitBehavior::Default) {
|
||||
- return props.traits.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
|
||||
+ return props.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
|
||||
}
|
||||
|
||||
return submitBehaviorDefaultable;
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||
index 29e094f..7ef519a 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||
@@ -22,8 +22,7 @@ PasteTextInputProps::PasteTextInputProps(
|
||||
const PropsParserContext &context,
|
||||
const PasteTextInputProps &sourceProps,
|
||||
const RawProps& rawProps)
|
||||
- : ViewProps(context, sourceProps, rawProps),
|
||||
- BaseTextProps(context, sourceProps, rawProps),
|
||||
+ : BaseTextInputProps(context, sourceProps, rawProps),
|
||||
traits(convertRawProp(context, rawProps, sourceProps.traits, {})),
|
||||
smartPunctuation(convertRawProp(context, rawProps, "smartPunctuation", sourceProps.smartPunctuation, {})),
|
||||
disableCopyPaste(convertRawProp(context, rawProps, "disableCopyPaste", sourceProps.disableCopyPaste, {false})),
|
||||
@@ -133,7 +132,7 @@ TextAttributes PasteTextInputProps::getEffectiveTextAttributes(Float fontSizeMul
|
||||
ParagraphAttributes PasteTextInputProps::getEffectiveParagraphAttributes() const {
|
||||
auto result = paragraphAttributes;
|
||||
|
||||
- if (!traits.multiline) {
|
||||
+ if (!multiline) {
|
||||
result.maximumNumberOfLines = 1;
|
||||
}
|
||||
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||
index 723d00c..31cfe66 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <react/renderer/components/iostextinput/conversions.h>
|
||||
#include <react/renderer/components/iostextinput/primitives.h>
|
||||
#include <react/renderer/components/text/BaseTextProps.h>
|
||||
+#include <react/renderer/components/textinput/BaseTextInputProps.h>
|
||||
#include <react/renderer/components/view/ViewProps.h>
|
||||
#include <react/renderer/core/Props.h>
|
||||
#include <react/renderer/core/PropsParserContext.h>
|
||||
@@ -25,7 +26,7 @@
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
-class PasteTextInputProps final : public ViewProps, public BaseTextProps {
|
||||
+class PasteTextInputProps final : public BaseTextInputProps {
|
||||
public:
|
||||
PasteTextInputProps() = default;
|
||||
PasteTextInputProps(const PropsParserContext& context, const PasteTextInputProps& sourceProps, const RawProps& rawProps);
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||
index 31e07e3..7f0ebfb 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||
@@ -91,20 +91,11 @@ void PasteTextInputShadowNode::updateStateIfNeeded(
|
||||
const auto& state = getStateData();
|
||||
|
||||
react_native_assert(textLayoutManager_);
|
||||
- react_native_assert(
|
||||
- (!state.layoutManager || state.layoutManager == textLayoutManager_) &&
|
||||
- "`StateData` refers to a different `TextLayoutManager`");
|
||||
-
|
||||
- if (state.reactTreeAttributedString == reactTreeAttributedString &&
|
||||
- state.layoutManager == textLayoutManager_) {
|
||||
- return;
|
||||
- }
|
||||
|
||||
auto newState = TextInputState{};
|
||||
newState.attributedStringBox = AttributedStringBox{reactTreeAttributedString};
|
||||
newState.paragraphAttributes = getConcreteProps().paragraphAttributes;
|
||||
newState.reactTreeAttributedString = reactTreeAttributedString;
|
||||
- newState.layoutManager = textLayoutManager_;
|
||||
newState.mostRecentEventCount = getConcreteProps().mostRecentEventCount;
|
||||
setStateData(std::move(newState));
|
||||
}
|
||||
@@ -1,3 +1,32 @@
|
||||
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||
index 914a249..0deac55 100644
|
||||
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||
@@ -19,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
*/
|
||||
@interface RCTPullToRefreshViewComponentView : RCTViewComponentView <RCTCustomPullToRefreshViewProtocol>
|
||||
|
||||
+- (void)beginRefreshingProgrammatically;
|
||||
+
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
index d029337..0f63ea3 100644
|
||||
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
@@ -1003,6 +1003,11 @@ - (void)_adjustForMaintainVisibleContentPosition
|
||||
}
|
||||
}
|
||||
|
||||
++ (BOOL)shouldBeRecycled
|
||||
+{
|
||||
+ return NO;
|
||||
+}
|
||||
+
|
||||
@end
|
||||
|
||||
Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
|
||||
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||
index e9b330f..ec5f58c 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||
@@ -15,7 +44,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont
|
||||
index 53bfd04..ff1b1ed 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||
@@ -23,6 +23,7 @@
|
||||
@@ -23,6 +23,7 @@ @implementation RCTRefreshControl {
|
||||
UIColor *_titleColor;
|
||||
CGFloat _progressViewOffset;
|
||||
BOOL _hasMovedToWindow;
|
||||
@@ -23,7 +52,7 @@ index 53bfd04..ff1b1ed 100644
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
@@ -58,6 +59,12 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
@@ -58,6 +59,12 @@ - (void)layoutSubviews
|
||||
_isInitialRender = false;
|
||||
}
|
||||
|
||||
@@ -36,7 +65,7 @@ index 53bfd04..ff1b1ed 100644
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
[super didMoveToWindow];
|
||||
@@ -221,4 +228,50 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
||||
@@ -221,4 +228,50 @@ - (void)refreshControlValueChanged
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +120,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont
|
||||
index 40aaf9c..1c60164 100644
|
||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||
@@ -22,11 +22,12 @@ RCT_EXPORT_MODULE()
|
||||
@@ -22,11 +22,12 @@ - (UIView *)view
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
|
||||
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
|
||||
@@ -105,15 +134,3 @@ index 40aaf9c..1c60164 100644
|
||||
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
|
||||
{
|
||||
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
index cd1e7eb..c1d0172 100644
|
||||
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
||||
@@ -83,6 +83,7 @@ RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval)
|
||||
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
||||
+RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(verticalScrollIndicatorInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
|
||||
+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 HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Shell} from '#/view/shell'
|
||||
import {ThemeProvider as Alf} from '#/alf'
|
||||
@@ -69,6 +68,7 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {Splash} from '#/Splash'
|
||||
import {BottomSheetProvider} from '../modules/bottom-sheet'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
|
||||
+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 StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
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 {ToastContainer} from '#/view/com/util/Toast.web'
|
||||
import {Shell} from '#/view/shell/index'
|
||||
@@ -60,6 +58,8 @@ import {NuxDialogs} from '#/components/dialogs/nuxs'
|
||||
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
|
||||
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
import {Theme} from '../types'
|
||||
import {type Theme} from '../types'
|
||||
|
||||
export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
|
||||
if (isAndroid) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {Pressable} from 'react-native'
|
||||
import Animated, {
|
||||
Extrapolation,
|
||||
interpolate,
|
||||
SharedValue,
|
||||
type SharedValue,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {SeenPost} from '#/state/userActionHistory'
|
||||
import {type SeenPost} from '#/state/userActionHistory'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
||||
import {
|
||||
atoms as a,
|
||||
useBreakpoints,
|
||||
useTheme,
|
||||
type ViewStyleProp,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import * as FeedCard from '#/components/FeedCard'
|
||||
import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
||||
@@ -27,7 +33,7 @@ import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/P
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {ProgressGuideList} from './ProgressGuide/List'
|
||||
|
||||
const MOBILE_CARD_WIDTH = 300
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import {createContext, useCallback, useContext} from 'react'
|
||||
import {GestureResponderEvent, Keyboard, View} from 'react-native'
|
||||
import {type GestureResponderEvent, Keyboard, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
import {
|
||||
atoms as a,
|
||||
platform,
|
||||
TextStyleProp,
|
||||
type TextStyleProp,
|
||||
useBreakpoints,
|
||||
useGutters,
|
||||
useLayoutBreakpoints,
|
||||
useTheme,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
|
||||
import {Button, ButtonIcon, type ButtonProps} from '#/components/Button'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
|
||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import type {ContextType, ItemContextType} from '#/components/Menu/types'
|
||||
import {type ContextType, type ItemContextType} from '#/components/Menu/types'
|
||||
|
||||
export const Context = React.createContext<ContextType | null>(null)
|
||||
|
||||
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
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 {AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
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 {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -14,7 +14,7 @@ import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
|
||||
import {Fill} from '#/components/Fill'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
|
||||
export function ExternalGifEmbed({
|
||||
export function ExternalGif({
|
||||
link,
|
||||
params,
|
||||
}: {
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
GestureResponderEvent,
|
||||
type GestureResponderEvent,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
useWindowDimensions,
|
||||
@@ -16,21 +16,21 @@ import Animated, {
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {WebView} from 'react-native-webview'
|
||||
import {Image} from 'expo-image'
|
||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {type EmbedPlayerParams, getPlayerAspect} from '#/lib/strings/embed-player'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
|
||||
import {Fill} from '#/components/Fill'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
|
||||
interface ShouldStartLoadRequest {
|
||||
url: string
|
||||
+4
-4
@@ -1,17 +1,17 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
type StyleProp,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
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 {useAutoplayDisabled} from '#/state/preferences'
|
||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||
@@ -22,7 +22,7 @@ import * as Prompt from '#/components/Prompt'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
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({
|
||||
onPress,
|
||||
+5
-5
@@ -12,16 +12,16 @@ import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
|
||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||
import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed'
|
||||
import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed'
|
||||
import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||
import {Link} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ExternalGif} from './ExternalGif'
|
||||
import {ExternalPlayer} from './ExternalPlayer'
|
||||
import {GifEmbed} from './Gif'
|
||||
|
||||
export const ExternalLinkEmbed = ({
|
||||
export const ExternalEmbed = ({
|
||||
link,
|
||||
onOpen,
|
||||
style,
|
||||
@@ -106,7 +106,7 @@ export const ExternalLinkEmbed = ({
|
||||
) : undefined}
|
||||
|
||||
{embedPlayerParams?.isGif ? (
|
||||
<ExternalGifEmbed link={link} params={embedPlayerParams} />
|
||||
<ExternalGif link={link} params={embedPlayerParams} />
|
||||
) : embedPlayerParams ? (
|
||||
<ExternalPlayer link={link} params={embedPlayerParams} />
|
||||
) : undefined}
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet} from 'react-native'
|
||||
import {moderateFeedGenerator} from '@atproto/api'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {type EmbedType} from '#/types/bsky/post'
|
||||
import {type CommonProps} from './types'
|
||||
|
||||
export function FeedEmbed({
|
||||
embed,
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'feed'>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<FeedSourceCard
|
||||
feedUri={embed.view.uri}
|
||||
style={[pal.view, pal.border, styles.customFeedOuter]}
|
||||
showLikes
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ModeratedFeedEmbed({
|
||||
embed,
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'feed'>
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const moderation = React.useMemo(() => {
|
||||
return moderationOpts
|
||||
? moderateFeedGenerator(embed.view, moderationOpts)
|
||||
: undefined
|
||||
}, [embed.view, moderationOpts])
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentList')}>
|
||||
<FeedEmbed embed={embed} />
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
customFeedOuter: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
import {InteractionManager, View} from 'react-native'
|
||||
import {
|
||||
type AnimatedRef,
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
} from 'react-native-reanimated'
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
import {useLightboxControls} from '#/state/lightbox'
|
||||
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
||||
import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||
import {type EmbedType} from '#/types/bsky/post'
|
||||
import {type CommonProps} from './types'
|
||||
|
||||
export function ImageEmbed({
|
||||
embed,
|
||||
...rest
|
||||
}: CommonProps & {
|
||||
embed: EmbedType<'images'>
|
||||
}) {
|
||||
const {openLightbox} = useLightboxControls()
|
||||
const {images} = embed.view
|
||||
|
||||
if (images.length > 0) {
|
||||
const items = images.map(img => ({
|
||||
uri: img.fullsize,
|
||||
thumbUri: img.thumb,
|
||||
alt: img.alt,
|
||||
dimensions: img.aspectRatio ?? null,
|
||||
}))
|
||||
const _openLightbox = (
|
||||
index: number,
|
||||
thumbRects: (MeasuredDimensions | null)[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => {
|
||||
openLightbox({
|
||||
images: items.map((item, i) => ({
|
||||
...item,
|
||||
thumbRect: thumbRects[i] ?? null,
|
||||
thumbDimensions: fetchedDims[i] ?? null,
|
||||
type: 'image',
|
||||
})),
|
||||
index,
|
||||
})
|
||||
}
|
||||
const onPress = (
|
||||
index: number,
|
||||
refs: AnimatedRef<any>[],
|
||||
fetchedDims: (Dimensions | null)[],
|
||||
) => {
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
const rects: (MeasuredDimensions | null)[] = []
|
||||
for (const r of refs) {
|
||||
rects.push(measure(r))
|
||||
}
|
||||
runOnJS(_openLightbox)(index, rects, fetchedDims)
|
||||
})()
|
||||
}
|
||||
const onPressIn = (_: number) => {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
Image.prefetch(items.map(i => i.uri))
|
||||
})
|
||||
}
|
||||
|
||||
if (images.length === 1) {
|
||||
const image = images[0]
|
||||
return (
|
||||
<View style={[a.mt_sm, rest.style]}>
|
||||
<AutoSizedImage
|
||||
crop={
|
||||
rest.viewContext === PostEmbedViewContext.ThreadHighlighted
|
||||
? 'none'
|
||||
: rest.viewContext ===
|
||||
PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
? 'square'
|
||||
: 'constrained'
|
||||
}
|
||||
image={image}
|
||||
onPress={(containerRef, dims) => onPress(0, [containerRef], [dims])}
|
||||
onPressIn={() => onPressIn(0)}
|
||||
hideBadge={
|
||||
rest.viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[a.mt_sm, rest.style]}>
|
||||
<ImageLayoutGrid
|
||||
images={images}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
viewContext={rest.viewContext}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import {useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util'
|
||||
import {useResolveLinkQuery} from '#/state/queries/resolve-link'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {QuoteEmbed} from '#/components/Post/Embed'
|
||||
|
||||
export function LazyQuoteEmbed({uri}: {uri: string}) {
|
||||
const t = useTheme()
|
||||
const {data} = useResolveLinkQuery(uri)
|
||||
|
||||
const view = useMemo(() => {
|
||||
if (!data || data.type !== 'record' || data.kind !== 'post') return
|
||||
return createEmbedViewRecordFromPost(data.view)
|
||||
}, [data])
|
||||
|
||||
return view ? (
|
||||
<QuoteEmbed
|
||||
embed={{
|
||||
type: 'post',
|
||||
view,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_md,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
height: 68,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {moderateUserList} from '@atproto/api'
|
||||
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as ListCard from '#/components/ListCard'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {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 {msg, plural} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
+3
-3
@@ -1,13 +1,12 @@
|
||||
import React, {useRef} from 'react'
|
||||
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {BlueskyVideoView} from '@haileyok/bluesky-video'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {useAutoplayDisabled} from '#/state/preferences'
|
||||
import {useVideoMuteState} from '#/view/com/util/post-embeds/VideoVolumeContext'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
@@ -15,6 +14,7 @@ import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Paus
|
||||
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {TimeIndicator} from './TimeIndicator'
|
||||
|
||||
export const VideoEmbedInnerNative = React.forwardRef(
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import {SvgProps} from 'react-native-svg'
|
||||
import {type 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 {PressableWithHover} from '../../../PressableWithHover'
|
||||
|
||||
export function ControlButton({
|
||||
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 {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react';
|
||||
|
||||
import {isFirefox, isTouchDevice} from '#/lib/browser'
|
||||
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 Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react';
|
||||
|
||||
import {isSafari, isTouchDevice} from '#/lib/browser'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {useVideoVolumeState} from '../../VideoVolumeContext'
|
||||
import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
import {ControlButton} from './ControlButton'
|
||||
|
||||
export function VolumeControl({
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {type RefObject, useCallback, useEffect, useRef, useState} from 'react'
|
||||
|
||||
import {isSafari} from '#/lib/browser'
|
||||
import {useVideoVolumeState} from '../../VideoVolumeContext'
|
||||
import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
|
||||
|
||||
export function useVideoElement(ref: React.RefObject<HTMLVideoElement>) {
|
||||
export function useVideoElement(ref: RefObject<HTMLVideoElement>) {
|
||||
const [playing, setPlaying] = useState(false)
|
||||
const [muted, setMuted] = useState(true)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
+3
-3
@@ -1,17 +1,17 @@
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {ActivityIndicator, View} from 'react-native'
|
||||
import {ImageBackground} from 'expo-image'
|
||||
import {AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
|
||||
import {ErrorBoundary} from '../ErrorBoundary'
|
||||
import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative'
|
||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||
|
||||
interface Props {
|
||||
+8
-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 {AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {type AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react';
|
||||
|
||||
import {isFirefox} from '#/lib/browser'
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {useFullscreen} from '#/components/hooks/useFullscreen'
|
||||
import {
|
||||
HLSUnsupportedError,
|
||||
VideoEmbedInnerWeb,
|
||||
VideoNotFoundError,
|
||||
} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {useIsWithinMessage} from '#/components/dms/MessageContext'
|
||||
import {useFullscreen} from '#/components/hooks/useFullscreen'
|
||||
import {ErrorBoundary} from '../ErrorBoundary'
|
||||
} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb'
|
||||
import {useActiveVideoWeb} from './ActiveVideoWebContext'
|
||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||
|
||||
@@ -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 {ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {type ChatBskyConvoDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react'
|
||||
import {useWindowDimensions, View} from 'react-native'
|
||||
import {AppBskyEmbedRecord} from '@atproto/api'
|
||||
import {type $Typed, type AppBskyEmbedRecord} from '@atproto/api'
|
||||
|
||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
||||
import {atoms as a, native, tokens, useTheme, web} from '#/alf'
|
||||
import {PostEmbedViewContext} from '#/components/Post/Embed'
|
||||
import {Embed} from '#/components/Post/Embed'
|
||||
import {MessageContextProvider} from './MessageContext'
|
||||
|
||||
let MessageItemEmbed = ({
|
||||
embed,
|
||||
}: {
|
||||
embed: AppBskyEmbedRecord.View
|
||||
embed: $Typed<AppBskyEmbedRecord.View>
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const screen = useWindowDimensions()
|
||||
@@ -32,7 +33,7 @@ let MessageItemEmbed = ({
|
||||
}),
|
||||
]}>
|
||||
<View style={{marginTop: tokens.space.sm * -1}}>
|
||||
<PostEmbeds
|
||||
<Embed
|
||||
embed={embed}
|
||||
allowNestedQuotes
|
||||
viewContext={PostEmbedViewContext.Feed}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
AccessibilityProps,
|
||||
type AccessibilityProps,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TextInputProps,
|
||||
TextStyle,
|
||||
type TextInputProps,
|
||||
type TextStyle,
|
||||
View,
|
||||
ViewStyle,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
|
||||
import {HITSLOP_20} from '#/lib/constants'
|
||||
@@ -16,13 +16,13 @@ import {
|
||||
applyFonts,
|
||||
atoms as a,
|
||||
ios,
|
||||
TextStyleProp,
|
||||
type TextStyleProp,
|
||||
useAlf,
|
||||
useTheme,
|
||||
web,
|
||||
} from '#/alf'
|
||||
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'
|
||||
|
||||
const Context = React.createContext<{
|
||||
@@ -196,7 +196,7 @@ export function createInput(Component: typeof TextInput) {
|
||||
minWidth: 0,
|
||||
},
|
||||
ios({paddingTop: 12, paddingBottom: 13}),
|
||||
android(a.py_sm),
|
||||
android(a.py_md),
|
||||
// fix for autofill styles covering border
|
||||
web({
|
||||
paddingTop: 10,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {formatDistance, Locale} from 'date-fns'
|
||||
import {formatDistance, type Locale} from 'date-fns'
|
||||
import {
|
||||
ca,
|
||||
cy,
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
zhTW,
|
||||
} from 'date-fns/locale'
|
||||
|
||||
import {AppLanguage} from '#/locale/languages'
|
||||
import {type AppLanguage} from '#/locale/languages'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import React, {ComponentProps} from 'react'
|
||||
import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {AppBskyActorDefs, ModerationCause, ModerationUI} from '@atproto/api'
|
||||
import React, {type ComponentProps} from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
type StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
type ModerationCause,
|
||||
type ModerationUI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
@@ -1,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
|
||||
}
|
||||
}
|
||||
+37
-50
@@ -1,8 +1,9 @@
|
||||
import {Image as RNImage, Share as RNShare} from 'react-native'
|
||||
import {Image as RNImage} from 'react-native'
|
||||
import uuid from 'react-native-uuid'
|
||||
import {
|
||||
cacheDirectory,
|
||||
copyAsync,
|
||||
createDownloadResumable,
|
||||
deleteAsync,
|
||||
EncodingType,
|
||||
getInfoAsync,
|
||||
@@ -14,7 +15,6 @@ import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
import {Buffer} from 'buffer'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
import {POST_IMG_MAX} from '#/lib/constants'
|
||||
import {logger} from '#/logger'
|
||||
@@ -68,28 +68,13 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
||||
return
|
||||
}
|
||||
|
||||
let downloadRes
|
||||
const path = createPath(appendExt)
|
||||
|
||||
try {
|
||||
const downloadResPromise = RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
appendExt,
|
||||
}).fetch('GET', opts.uri)
|
||||
const to1 = setTimeout(() => downloadResPromise.cancel(), opts.timeout)
|
||||
downloadRes = await downloadResPromise
|
||||
clearTimeout(to1)
|
||||
|
||||
const status = downloadRes.info().status
|
||||
if (status !== 200) {
|
||||
return
|
||||
}
|
||||
|
||||
const localUri = normalizePath(downloadRes.path(), true)
|
||||
return await doResize(localUri, opts)
|
||||
await downloadImage(opts.uri, path, opts.timeout)
|
||||
return await doResize(path, opts)
|
||||
} finally {
|
||||
// TODO Whenever we remove `rn-fetch-blob`, we will need to replace this `flush()` with a `deleteAsync()` -hailey
|
||||
if (downloadRes) {
|
||||
downloadRes.flush()
|
||||
}
|
||||
safeDeleteAsync(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,32 +83,16 @@ export async function shareImageModal({uri}: {uri: string}) {
|
||||
// TODO might need to give an error to the user in this case -prf
|
||||
return
|
||||
}
|
||||
const downloadResponse = await RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
}).fetch('GET', uri)
|
||||
|
||||
// NOTE
|
||||
// assuming PNG
|
||||
// we're currently relying on the fact our CDN only serves pngs
|
||||
// -prf
|
||||
|
||||
let imagePath = downloadResponse.path()
|
||||
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
|
||||
|
||||
// NOTE
|
||||
// for some reason expo-sharing refuses to work on iOS
|
||||
// ...and visa versa
|
||||
// -prf
|
||||
if (isIOS) {
|
||||
await RNShare.share({url: imagePath})
|
||||
} else {
|
||||
await Sharing.shareAsync(imagePath, {
|
||||
mimeType: 'image/png',
|
||||
UTI: 'image/png',
|
||||
})
|
||||
}
|
||||
|
||||
safeDeleteAsync(imagePath)
|
||||
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||
safeDeleteAsync(imageUri)
|
||||
await Sharing.shareAsync(imagePath, {
|
||||
mimeType: 'image/png',
|
||||
UTI: 'image/png',
|
||||
})
|
||||
}
|
||||
|
||||
const ALBUM_NAME = 'Bluesky'
|
||||
@@ -134,11 +103,8 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
||||
// assuming PNG
|
||||
// we're currently relying on the fact our CDN only serves pngs
|
||||
// -prf
|
||||
const downloadResponse = await RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
}).fetch('GET', uri)
|
||||
let imagePath = downloadResponse.path()
|
||||
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
|
||||
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||
|
||||
// save
|
||||
try {
|
||||
@@ -403,3 +369,24 @@ export function getResizedDimensions(originalDims: {
|
||||
height: Math.round(originalDims.height * ratio),
|
||||
}
|
||||
}
|
||||
|
||||
function createPath(ext: string) {
|
||||
// cacheDirectory will never be null on native, so the null check here is not necessary except for typescript.
|
||||
// we use a web-only function for downloadAndResize on web
|
||||
return `${cacheDirectory ?? ''}/${uuid.v4()}.${ext}`
|
||||
}
|
||||
|
||||
async function downloadImage(uri: string, path: string, timeout: number) {
|
||||
const dlResumable = createDownloadResumable(uri, path, {cache: true})
|
||||
|
||||
const to1 = setTimeout(() => dlResumable.cancelAsync(), timeout)
|
||||
|
||||
const dlRes = await dlResumable.downloadAsync()
|
||||
clearTimeout(to1)
|
||||
|
||||
if (!dlRes?.uri) {
|
||||
throw new Error('Failed to download image - dlRes is undefined')
|
||||
}
|
||||
|
||||
return normalizePath(dlRes.uri)
|
||||
}
|
||||
|
||||
@@ -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 lande from 'lande'
|
||||
|
||||
|
||||
@@ -4469,11 +4469,11 @@ msgstr ""
|
||||
msgid "List by {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:156
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:160
|
||||
msgid "List by <0/>"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:154
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:158
|
||||
msgid "List by you"
|
||||
msgstr ""
|
||||
|
||||
@@ -4740,12 +4740,12 @@ msgstr ""
|
||||
msgid "Moderation list by {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:169
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:173
|
||||
msgid "Moderation list by <0/>"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
@@ -5905,17 +5905,17 @@ msgstr ""
|
||||
msgid "Porn"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1030
|
||||
msgctxt "action"
|
||||
msgid "Post"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/PostThread/index.tsx:490
|
||||
#: src/view/com/post-thread/PostThread.tsx:561
|
||||
msgctxt "description"
|
||||
msgid "Post"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1030
|
||||
msgctxt "action"
|
||||
msgid "Post"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1028
|
||||
msgctxt "action"
|
||||
msgid "Post All"
|
||||
@@ -7681,12 +7681,12 @@ msgstr ""
|
||||
msgid "Starter pack by {0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:182
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:186
|
||||
msgid "Starter pack by <0/>"
|
||||
msgstr ""
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
|
||||
@@ -9126,7 +9126,7 @@ msgstr ""
|
||||
msgid "View profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:119
|
||||
#: src/view/com/profile/ProfileSubpageHeader.tsx:123
|
||||
msgid "View the avatar"
|
||||
msgstr ""
|
||||
|
||||
@@ -9171,8 +9171,8 @@ msgstr ""
|
||||
msgid "View your verifications"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:199
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:221
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:205
|
||||
#: src/view/com/util/images/AutoSizedImage.tsx:227
|
||||
msgid "Views full image"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -38,4 +38,12 @@ init({
|
||||
*/
|
||||
`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,
|
||||
Keyboard,
|
||||
LayoutAnimation,
|
||||
TextInput,
|
||||
type TextInput,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {
|
||||
ComAtprotoServerCreateSession,
|
||||
ComAtprotoServerDescribeServer,
|
||||
type ComAtprotoServerDescribeServer,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image as ExpoImage} from 'expo-image'
|
||||
import {
|
||||
ImagePickerOptions,
|
||||
type ImagePickerOptions,
|
||||
launchImageLibraryAsync,
|
||||
MediaTypeOptions,
|
||||
} from 'expo-image-picker'
|
||||
@@ -27,7 +27,7 @@ import {AvatarCreatorCircle} from '#/screens/Onboarding/StepProfile/AvatarCreato
|
||||
import {AvatarCreatorItems} from '#/screens/Onboarding/StepProfile/AvatarCreatorItems'
|
||||
import {
|
||||
PlaceholderCanvas,
|
||||
PlaceholderCanvasRef,
|
||||
type PlaceholderCanvasRef,
|
||||
} from '#/screens/Onboarding/StepProfile/PlaceholderCanvas'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
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 {StreamingLive_Stroke2_Corner0_Rounded as StreamingLive} from '#/components/icons/StreamingLive'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {AvatarColor, avatarColors, Emoji, emojiItems} from './types'
|
||||
import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types'
|
||||
|
||||
export interface Avatar {
|
||||
image?: {
|
||||
|
||||
@@ -36,7 +36,6 @@ import {type PostSource} from '#/state/unstable-post-source'
|
||||
import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
|
||||
import {Link} from '#/view/com/util/Link'
|
||||
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 {
|
||||
LINEAR_AVI_WIDTH,
|
||||
@@ -53,6 +52,7 @@ import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||
import {PostControls} from '#/components/PostControls'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
@@ -388,7 +388,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
|
||||
) : undefined}
|
||||
{post.embed && (
|
||||
<View style={[a.py_xs]}>
|
||||
<PostEmbeds
|
||||
<Embed
|
||||
embed={post.embed}
|
||||
moderation={moderation}
|
||||
viewContext={PostEmbedViewContext.ThreadHighlighted}
|
||||
|
||||
@@ -22,7 +22,6 @@ import {type ThreadItem} from '#/state/queries/usePostThread/types'
|
||||
import {useSession} from '#/state/session'
|
||||
import {type OnPostSuccessData} from '#/state/shell/composer'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {
|
||||
@@ -37,6 +36,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {PostHider} from '#/components/moderation/PostHider'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||
import {PostControls} from '#/components/PostControls'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {ShowMore} from '#/components/ShowMore'
|
||||
@@ -316,7 +316,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
|
||||
) : undefined}
|
||||
{post.embed && (
|
||||
<View style={[a.pb_xs]}>
|
||||
<PostEmbeds
|
||||
<Embed
|
||||
embed={post.embed}
|
||||
moderation={moderation}
|
||||
viewContext={PostEmbedViewContext.Feed}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {type ThreadItem} from '#/state/queries/usePostThread/types'
|
||||
import {useSession} from '#/state/session'
|
||||
import {type OnPostSuccessData} from '#/state/shell/composer'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||
import {
|
||||
OUTER_SPACE,
|
||||
@@ -36,6 +35,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {PostHider} from '#/components/moderation/PostHider'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||
import {PostControls} from '#/components/PostControls'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {ShowMore} from '#/components/ShowMore'
|
||||
@@ -363,7 +363,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
|
||||
) : undefined}
|
||||
{post.embed && (
|
||||
<View style={[a.pb_xs]}>
|
||||
<PostEmbeds
|
||||
<Embed
|
||||
embed={post.embed}
|
||||
moderation={moderation}
|
||||
viewContext={PostEmbedViewContext.Feed}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
|
||||
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 {NewskieDialog} from '#/components/NewskieDialog'
|
||||
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 {
|
||||
import Animated, {
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
useAnimatedRef,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
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 {BACK_HITSLOP} from '#/lib/constants'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {logger} from '#/logger'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
@@ -59,9 +60,9 @@ let ProfileHeaderShell = ({
|
||||
const playHaptic = useHaptics()
|
||||
const liveStatusControl = useDialogControl()
|
||||
|
||||
const aviRef = useHandleRef()
|
||||
const aviRef = useAnimatedRef()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
const onPressBack = useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
@@ -69,7 +70,7 @@ let ProfileHeaderShell = ({
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
const _openLightbox = React.useCallback(
|
||||
const _openLightbox = useCallback(
|
||||
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
||||
openLightbox({
|
||||
images: [
|
||||
@@ -92,7 +93,7 @@ let ProfileHeaderShell = ({
|
||||
[openLightbox],
|
||||
)
|
||||
|
||||
const isMe = React.useMemo(
|
||||
const isMe = useMemo(
|
||||
() => currentAccount?.did === profile.did,
|
||||
[currentAccount, profile],
|
||||
)
|
||||
@@ -109,7 +110,7 @@ let ProfileHeaderShell = ({
|
||||
}
|
||||
}, [live.isActive, profile.did])
|
||||
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
const onPressAvi = useCallback(() => {
|
||||
if (live.isActive) {
|
||||
playHaptic('Light')
|
||||
logger.metric(
|
||||
@@ -122,10 +123,9 @@ let ProfileHeaderShell = ({
|
||||
const modui = moderation.ui('avatar')
|
||||
const avatar = profile.avatar
|
||||
if (avatar && !(modui.blur && modui.noOverride)) {
|
||||
const aviHandle = aviRef.current
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
const rect = measureHandle(aviHandle)
|
||||
const rect = measure(aviRef)
|
||||
runOnJS(_openLightbox)(avatar, rect)
|
||||
})()
|
||||
}
|
||||
@@ -223,7 +223,7 @@ let ProfileHeaderShell = ({
|
||||
styles.avi,
|
||||
profile.associated?.labeler && styles.aviLabeler,
|
||||
]}>
|
||||
<View ref={aviRef} collapsable={false}>
|
||||
<Animated.View ref={aviRef} collapsable={false}>
|
||||
<UserAvatar
|
||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
size={live.isActive ? 88 : 90}
|
||||
@@ -231,7 +231,7 @@ let ProfileHeaderShell = ({
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
{live.isActive && <LiveIndicator size="large" />}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</GrowableAvatar>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ReactElement} from 'react'
|
||||
import {type ReactElement} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {ComAtprotoServerDescribeServer} from '@atproto/api'
|
||||
import {type ComAtprotoServerDescribeServer} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
AppBskyGraphDefs,
|
||||
AppBskyGraphStarterpack,
|
||||
AtUri,
|
||||
ModerationOpts,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||
import {type GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
import {msg, plural} from '@lingui/macro'
|
||||
|
||||
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 {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
|
||||
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 {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
@@ -3,13 +3,13 @@ import {View} from 'react-native'
|
||||
import {
|
||||
Gesture,
|
||||
GestureDetector,
|
||||
NativeGesture,
|
||||
type NativeGesture,
|
||||
} from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
interpolate,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
SharedValue,
|
||||
type SharedValue,
|
||||
useAnimatedReaction,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
useSafeAreaInsets,
|
||||
} from 'react-native-safe-area-context'
|
||||
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 {atoms as a} from '#/alf'
|
||||
import {formatTime} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/utils'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
// 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 findAllPostsInSearchQueryData} from '#/state/queries/search-posts'
|
||||
import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache'
|
||||
import {useProfileShadow} from './profile-shadow'
|
||||
import {castAsShadow, type Shadow} from './types'
|
||||
export type {Shadow} from './types'
|
||||
|
||||
@@ -45,10 +44,6 @@ export function usePostShadow(
|
||||
setShadow(shadows.get(post))
|
||||
}
|
||||
|
||||
const authorShadow = useProfileShadow(post.author)
|
||||
const wasMuted = !!authorShadow.viewer?.muted
|
||||
const wasBlocked = !!authorShadow.viewer?.blocking
|
||||
|
||||
useEffect(() => {
|
||||
function onUpdate() {
|
||||
setShadow(shadows.get(post))
|
||||
@@ -60,18 +55,15 @@ export function usePostShadow(
|
||||
}, [post, setShadow])
|
||||
|
||||
return useMemo(() => {
|
||||
if (wasMuted || wasBlocked) {
|
||||
return POST_TOMBSTONE
|
||||
}
|
||||
if (shadow) {
|
||||
return mergeShadow(post, shadow)
|
||||
} else {
|
||||
return castAsShadow(post)
|
||||
}
|
||||
}, [post, shadow, wasMuted, wasBlocked])
|
||||
}, [post, shadow])
|
||||
}
|
||||
|
||||
export function mergeShadow(
|
||||
function mergeShadow(
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
shadow: Partial<PostShadow>,
|
||||
): 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 {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
DEFAULT_POLL_INTERVAL,
|
||||
} from '#/state/messages/events/const'
|
||||
import {
|
||||
MessagesEventBusDispatch,
|
||||
type MessagesEventBusDispatch,
|
||||
MessagesEventBusDispatchEvent,
|
||||
MessagesEventBusErrorCode,
|
||||
MessagesEventBusEvent,
|
||||
MessagesEventBusParams,
|
||||
type MessagesEventBusEvent,
|
||||
type MessagesEventBusParams,
|
||||
MessagesEventBusStatus,
|
||||
} from '#/state/messages/events/types'
|
||||
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
$Typed,
|
||||
type $Typed,
|
||||
AppBskyEmbedRecord,
|
||||
AppBskyEmbedRecordWithMedia,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPostgate,
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPostgate,
|
||||
AtUri,
|
||||
} from '@atproto/api'
|
||||
|
||||
@@ -113,6 +113,7 @@ export function createEmbedViewRecordFromPost(
|
||||
likeCount: post.likeCount,
|
||||
quoteCount: post.quoteCount,
|
||||
indexedAt: post.indexedAt,
|
||||
embeds: post.embed ? [post.embed] : [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -499,10 +499,9 @@ function useProfileBlockMutation() {
|
||||
{subject: did, createdAt: new Date().toISOString()},
|
||||
)
|
||||
},
|
||||
onSuccess(data, {did}) {
|
||||
onSuccess(_, {did}) {
|
||||
queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()})
|
||||
resetProfilePostsQueries(queryClient, did, 1000)
|
||||
updateProfileShadow(queryClient, did, {blockingUri: data.uri})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -524,7 +523,6 @@ function useProfileUnblockMutation() {
|
||||
},
|
||||
onSuccess(_, {did}) {
|
||||
resetProfilePostsQueries(queryClient, did, 1000)
|
||||
updateProfileShadow(queryClient, did, {blockingUri: undefined})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {LogEvents} from '#/lib/statsig/statsig'
|
||||
import {PersistedAccount} from '#/state/persisted'
|
||||
import {type LogEvents} from '#/lib/statsig/statsig'
|
||||
import {type PersistedAccount} from '#/state/persisted'
|
||||
|
||||
export type SessionAccount = PersistedAccount
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {AppBskyFeedThreadgate} from '@atproto/api'
|
||||
import {type AppBskyFeedThreadgate} from '@atproto/api'
|
||||
|
||||
type StateContext = {
|
||||
uris: Set<string>
|
||||
|
||||
@@ -72,7 +72,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
import {mimeToExt} from '#/lib/media/video/util'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {colors, s} from '#/lib/styles'
|
||||
import {colors} from '#/lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
ExternalEmbedGif,
|
||||
ExternalEmbedLink,
|
||||
} from '#/view/com/composer/ExternalEmbed'
|
||||
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
|
||||
import {GifAltTextDialog} from '#/view/com/composer/GifAltText'
|
||||
import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
|
||||
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 {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
|
||||
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 * as Toast from '#/view/com/util/Toast'
|
||||
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 {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||
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 {Text as NewText} from '#/components/Typography'
|
||||
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
|
||||
@@ -1149,13 +1150,17 @@ function ComposerEmbeds({
|
||||
)}
|
||||
</LayoutAnimationConfig>
|
||||
{embed.quote?.uri ? (
|
||||
<View style={!video ? [a.mt_md] : []}>
|
||||
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
|
||||
<View
|
||||
style={[a.pb_sm, video ? [a.pt_md] : [a.pt_xl], isWeb && [a.pb_md]]}>
|
||||
<View style={[a.relative]}>
|
||||
<View style={{pointerEvents: 'none'}}>
|
||||
<LazyQuoteEmbed uri={embed.quote.uri} />
|
||||
</View>
|
||||
{canRemoveQuote && (
|
||||
<QuoteX onRemove={() => dispatch({type: 'embed_remove_quote'})} />
|
||||
<ExternalEmbedRemoveBtn
|
||||
onRemove={() => dispatch({type: 'embed_remove_quote'})}
|
||||
style={{top: 16}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -13,12 +13,13 @@ import {useLingui} from '@lingui/react'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {type ComposerOptsPostRef} from '#/state/shell/composer'
|
||||
import {MaybeQuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {QuoteEmbed} from '#/components/Post/Embed'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useSimpleVerificationState} from '#/components/verification'
|
||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||
import {parseEmbed} from '#/types/bsky/post'
|
||||
|
||||
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
|
||||
const t = useTheme()
|
||||
@@ -51,6 +52,12 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
|
||||
}
|
||||
return null
|
||||
}, [embed])
|
||||
const parsedQuoteEmbed = quoteEmbed
|
||||
? parseEmbed({
|
||||
$type: 'app.bsky.embed.record#view',
|
||||
...quoteEmbed,
|
||||
})
|
||||
: null
|
||||
|
||||
const images = useMemo(() => {
|
||||
if (AppBskyEmbedImages.isView(embed)) {
|
||||
@@ -124,7 +131,9 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
|
||||
<ComposerReplyToImages images={images} showFull={showFull} />
|
||||
)}
|
||||
</View>
|
||||
{showFull && quoteEmbed && <MaybeQuoteEmbed embed={quoteEmbed} />}
|
||||
{showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && (
|
||||
<QuoteEmbed embed={parsedQuoteEmbed} />
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
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 {
|
||||
useResolveGifQuery,
|
||||
useResolveLinkQuery,
|
||||
} 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 {ExternalLinkEmbed} from '#/view/com/util/post-embeds/ExternalLinkEmbed'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
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 {Text} from '#/components/Typography'
|
||||
import {MaybeFeedCard, MaybeListCard} from '../util/post-embeds'
|
||||
|
||||
export const ExternalEmbedGif = ({
|
||||
onRemove,
|
||||
@@ -44,7 +45,7 @@ export const ExternalEmbedGif = ({
|
||||
<View style={[a.overflow_hidden, t.atoms.border_contrast_medium]}>
|
||||
{linkInfo ? (
|
||||
<View style={{pointerEvents: 'auto'}}>
|
||||
<ExternalLinkEmbed link={linkInfo} hideAlt />
|
||||
<ExternalEmbed link={linkInfo} hideAlt />
|
||||
</View>
|
||||
) : error ? (
|
||||
<Container style={[a.align_start, a.p_md, a.gap_xs]}>
|
||||
@@ -80,7 +81,7 @@ export const ExternalEmbedLink = ({
|
||||
if (data) {
|
||||
if (data.type === 'external') {
|
||||
return (
|
||||
<ExternalLinkEmbed
|
||||
<ExternalEmbed
|
||||
link={{
|
||||
title: data.title || uri,
|
||||
uri,
|
||||
@@ -91,9 +92,29 @@ export const ExternalEmbedLink = ({
|
||||
/>
|
||||
)
|
||||
} 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') {
|
||||
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') {
|
||||
return <StarterPackEmbed starterPack={data.view} />
|
||||
}
|
||||
|
||||
@@ -2,22 +2,27 @@ import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
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 {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()
|
||||
|
||||
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
|
||||
label={_(msg`Remove attachment`)}
|
||||
onPress={onRemove}
|
||||
size="small"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
shape="round">
|
||||
shape="round"
|
||||
style={[t.atoms.shadow_sm]}>
|
||||
<ButtonIcon icon={X} size="sm" />
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -6,23 +6,23 @@ import {useLingui} from '@lingui/react'
|
||||
import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants'
|
||||
import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
|
||||
import {
|
||||
EmbedPlayerParams,
|
||||
type EmbedPlayerParams,
|
||||
parseEmbedPlayerFromUrl,
|
||||
} from '#/lib/strings/embed-player'
|
||||
import {isAndroid} from '#/platform/detection'
|
||||
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 {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
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 {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
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 {GifEmbed} from '../util/post-embeds/GifEmbed'
|
||||
import {AltTextReminder} from './photos/Gallery'
|
||||
|
||||
export function GifAltTextDialog({
|
||||
|
||||
@@ -4,10 +4,10 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {
|
||||
ADULT_CONTENT_LABELS,
|
||||
AdultSelfLabel,
|
||||
type AdultSelfLabel,
|
||||
OTHER_SELF_LABELS,
|
||||
OtherSelfLabel,
|
||||
SelfLabel,
|
||||
type OtherSelfLabel,
|
||||
type SelfLabel,
|
||||
} from '#/lib/moderation'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {atoms as a, native, useTheme, web} from '#/alf'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -7,12 +7,12 @@ import {useLingui} from '@lingui/react'
|
||||
import {MAX_ALT_TEXT} from '#/lib/constants'
|
||||
import {enforceLen} from '#/lib/strings/helpers'
|
||||
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 {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
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 {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
@@ -8,7 +8,7 @@ import {useCameraPermission} from '#/lib/hooks/usePermissions'
|
||||
import {openCamera} from '#/lib/media/picker'
|
||||
import {logger} from '#/logger'
|
||||
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 {Button} from '#/components/Button'
|
||||
import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
ComponentProps,
|
||||
type ComponentProps,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
@@ -7,16 +7,16 @@ import React, {
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
NativeSyntheticEvent,
|
||||
type NativeSyntheticEvent,
|
||||
Text as RNText,
|
||||
TextInput as RNTextInput,
|
||||
TextInputSelectionChangeEventData,
|
||||
type TextInput as RNTextInput,
|
||||
type TextInputSelectionChangeEventData,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||
import PasteInput, {
|
||||
PastedFile,
|
||||
PasteInputRef,
|
||||
type PastedFile,
|
||||
type PasteInputRef, // @ts-expect-error no types when installing from github
|
||||
} from '@mattermost/react-native-paste-input'
|
||||
|
||||
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 {isAndroid, isNative} from '#/platform/detection'
|
||||
import {
|
||||
LinkFacetMatch,
|
||||
type LinkFacetMatch,
|
||||
suggestLinkCardUri,
|
||||
} from '#/view/com/composer/text-input/text-input-util'
|
||||
import {atoms as a, useAlf} from '#/alf'
|
||||
|
||||
+115
-107
@@ -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 {DrawerGestureContext} from 'react-native-drawer-layout'
|
||||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||
import PagerView, {
|
||||
PagerViewOnPageScrollEventData,
|
||||
PagerViewOnPageSelectedEvent,
|
||||
PagerViewOnPageSelectedEventData,
|
||||
PageScrollStateChangedNativeEventData,
|
||||
type PagerViewOnPageScrollEventData,
|
||||
type PagerViewOnPageSelectedEvent,
|
||||
type PagerViewOnPageSelectedEventData,
|
||||
type PageScrollStateChangedNativeEventData,
|
||||
} from 'react-native-pager-view'
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
SharedValue,
|
||||
type SharedValue,
|
||||
useEvent,
|
||||
useHandler,
|
||||
useSharedValue,
|
||||
@@ -36,8 +42,12 @@ export interface RenderTabBarFnProps {
|
||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||
|
||||
interface Props {
|
||||
ref?: React.Ref<PagerRef>
|
||||
initialPage?: number
|
||||
renderTabBar: RenderTabBarFn
|
||||
// tab pressed, yet to scroll to page
|
||||
onTabPressed?: (index: number) => void
|
||||
// scroll settled
|
||||
onPageSelected?: (index: number) => void
|
||||
onPageScrollStateChanged?: (
|
||||
scrollState: 'idle' | 'dragging' | 'settling',
|
||||
@@ -47,114 +57,112 @@ interface Props {
|
||||
|
||||
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
|
||||
|
||||
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
||||
function PagerImpl(
|
||||
export function Pager({
|
||||
ref,
|
||||
children,
|
||||
initialPage = 0,
|
||||
renderTabBar,
|
||||
onPageSelected: parentOnPageSelected,
|
||||
onTabPressed: parentOnTabPressed,
|
||||
onPageScrollStateChanged: parentOnPageScrollStateChanged,
|
||||
testID,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const [selectedPage, setSelectedPage] = useState(initialPage)
|
||||
const pagerView = useRef<PagerView>(null)
|
||||
|
||||
const [isIdle, setIsIdle] = useState(true)
|
||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const canSwipeDrawer = selectedPage === 0 && isIdle
|
||||
setDrawerSwipeDisabled(!canSwipeDrawer)
|
||||
return () => {
|
||||
setDrawerSwipeDisabled(false)
|
||||
}
|
||||
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
|
||||
)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setPage: (index: number) => {
|
||||
pagerView.current?.setPage(index)
|
||||
},
|
||||
}))
|
||||
|
||||
const onPageSelectedJSThread = useCallback(
|
||||
(nextPosition: number) => {
|
||||
setSelectedPage(nextPosition)
|
||||
parentOnPageSelected?.(nextPosition)
|
||||
},
|
||||
[setSelectedPage, parentOnPageSelected],
|
||||
)
|
||||
|
||||
const onTabBarSelect = useCallback(
|
||||
(index: number) => {
|
||||
parentOnTabPressed?.(index)
|
||||
pagerView.current?.setPage(index)
|
||||
},
|
||||
[pagerView, parentOnTabPressed],
|
||||
)
|
||||
|
||||
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
|
||||
const dragProgress = useSharedValue(selectedPage)
|
||||
const didInit = useSharedValue(false)
|
||||
const handlePageScroll = usePagerHandlers(
|
||||
{
|
||||
children,
|
||||
initialPage = 0,
|
||||
renderTabBar,
|
||||
onPageScrollStateChanged: parentOnPageScrollStateChanged,
|
||||
onPageSelected: parentOnPageSelected,
|
||||
testID,
|
||||
}: React.PropsWithChildren<Props>,
|
||||
ref,
|
||||
) {
|
||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
||||
const pagerView = React.useRef<PagerView>(null)
|
||||
|
||||
const [isIdle, setIsIdle] = React.useState(true)
|
||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const canSwipeDrawer = selectedPage === 0 && isIdle
|
||||
setDrawerSwipeDisabled(!canSwipeDrawer)
|
||||
return () => {
|
||||
setDrawerSwipeDisabled(false)
|
||||
onPageScroll(e: PagerViewOnPageScrollEventData) {
|
||||
'worklet'
|
||||
if (didInit.get() === false) {
|
||||
// On iOS, there's a spurious scroll event with 0 position
|
||||
// even if a different page was supplied as the initial page.
|
||||
// Ignore it and wait for the first confirmed selection instead.
|
||||
return
|
||||
}
|
||||
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
|
||||
)
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
setPage: (index: number) => {
|
||||
pagerView.current?.setPage(index)
|
||||
dragProgress.set(e.offset + e.position)
|
||||
},
|
||||
}))
|
||||
|
||||
const onPageSelectedJSThread = React.useCallback(
|
||||
(nextPosition: number) => {
|
||||
setSelectedPage(nextPosition)
|
||||
parentOnPageSelected?.(nextPosition)
|
||||
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
|
||||
'worklet'
|
||||
runOnJS(setIsIdle)(e.pageScrollState === 'idle')
|
||||
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
|
||||
// This is a programmatic scroll on Android.
|
||||
// Stay "idle" to match iOS and avoid confusing downstream code.
|
||||
return
|
||||
}
|
||||
dragState.set(e.pageScrollState)
|
||||
parentOnPageScrollStateChanged?.(e.pageScrollState)
|
||||
},
|
||||
[setSelectedPage, parentOnPageSelected],
|
||||
)
|
||||
|
||||
const onTabBarSelect = React.useCallback(
|
||||
(index: number) => {
|
||||
pagerView.current?.setPage(index)
|
||||
onPageSelected(e: PagerViewOnPageSelectedEventData) {
|
||||
'worklet'
|
||||
didInit.set(true)
|
||||
runOnJS(onPageSelectedJSThread)(e.position)
|
||||
},
|
||||
[pagerView],
|
||||
)
|
||||
},
|
||||
[parentOnPageScrollStateChanged],
|
||||
)
|
||||
|
||||
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
|
||||
const dragProgress = useSharedValue(selectedPage)
|
||||
const didInit = useSharedValue(false)
|
||||
const handlePageScroll = usePagerHandlers(
|
||||
{
|
||||
onPageScroll(e: PagerViewOnPageScrollEventData) {
|
||||
'worklet'
|
||||
if (didInit.get() === false) {
|
||||
// On iOS, there's a spurious scroll event with 0 position
|
||||
// even if a different page was supplied as the initial page.
|
||||
// Ignore it and wait for the first confirmed selection instead.
|
||||
return
|
||||
}
|
||||
dragProgress.set(e.offset + e.position)
|
||||
},
|
||||
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
|
||||
'worklet'
|
||||
runOnJS(setIsIdle)(e.pageScrollState === 'idle')
|
||||
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
|
||||
// This is a programmatic scroll on Android.
|
||||
// Stay "idle" to match iOS and avoid confusing downstream code.
|
||||
return
|
||||
}
|
||||
dragState.set(e.pageScrollState)
|
||||
parentOnPageScrollStateChanged?.(e.pageScrollState)
|
||||
},
|
||||
onPageSelected(e: PagerViewOnPageSelectedEventData) {
|
||||
'worklet'
|
||||
didInit.set(true)
|
||||
runOnJS(onPageSelectedJSThread)(e.position)
|
||||
},
|
||||
},
|
||||
[parentOnPageScrollStateChanged],
|
||||
)
|
||||
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
||||
const nativeGesture =
|
||||
Gesture.Native().requireExternalGestureToFail(drawerGesture)
|
||||
|
||||
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
||||
const nativeGesture =
|
||||
Gesture.Native().requireExternalGestureToFail(drawerGesture)
|
||||
|
||||
return (
|
||||
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
|
||||
{renderTabBar({
|
||||
selectedPage,
|
||||
onSelect: onTabBarSelect,
|
||||
dragProgress,
|
||||
dragState,
|
||||
})}
|
||||
<GestureDetector gesture={nativeGesture}>
|
||||
<AnimatedPagerView
|
||||
ref={pagerView}
|
||||
style={[a.flex_1]}
|
||||
initialPage={initialPage}
|
||||
onPageScroll={handlePageScroll}>
|
||||
{children}
|
||||
</AnimatedPagerView>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
|
||||
{renderTabBar({
|
||||
selectedPage,
|
||||
onSelect: onTabBarSelect,
|
||||
dragProgress,
|
||||
dragState,
|
||||
})}
|
||||
<GestureDetector gesture={nativeGesture}>
|
||||
<AnimatedPagerView
|
||||
ref={pagerView}
|
||||
style={[a.flex_1]}
|
||||
initialPage={initialPage}
|
||||
onPageScroll={handlePageScroll}>
|
||||
{children}
|
||||
</AnimatedPagerView>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function usePagerHandlers(
|
||||
handlers: {
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Children,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {flushSync} from 'react-dom'
|
||||
|
||||
import {s} from '#/lib/styles'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
export interface PagerRef {
|
||||
setPage: (index: number) => void
|
||||
}
|
||||
|
||||
export interface RenderTabBarFnProps {
|
||||
selectedPage: number
|
||||
@@ -12,30 +23,30 @@ export interface RenderTabBarFnProps {
|
||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||
|
||||
interface Props {
|
||||
ref?: React.Ref<PagerRef>
|
||||
initialPage?: number
|
||||
renderTabBar: RenderTabBarFn
|
||||
onPageSelected?: (index: number) => void
|
||||
}
|
||||
export const Pager = React.forwardRef(function PagerImpl(
|
||||
{
|
||||
children,
|
||||
initialPage = 0,
|
||||
renderTabBar,
|
||||
onPageSelected,
|
||||
}: React.PropsWithChildren<Props>,
|
||||
ref,
|
||||
) {
|
||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
||||
const scrollYs = React.useRef<Array<number | null>>([])
|
||||
const anchorRef = React.useRef(null)
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
export function Pager({
|
||||
ref,
|
||||
children,
|
||||
initialPage = 0,
|
||||
renderTabBar,
|
||||
onPageSelected,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const [selectedPage, setSelectedPage] = useState(initialPage)
|
||||
const scrollYs = useRef<Array<number | null>>([])
|
||||
const anchorRef = useRef(null)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setPage: (index: number) => {
|
||||
onTabBarSelect(index)
|
||||
},
|
||||
}))
|
||||
|
||||
const onTabBarSelect = React.useCallback(
|
||||
const onTabBarSelect = useCallback(
|
||||
(index: number) => {
|
||||
const scrollY = window.scrollY
|
||||
// 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} />,
|
||||
onSelect: e => onTabBarSelect(e),
|
||||
})}
|
||||
{React.Children.map(children, (child, i) => (
|
||||
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
|
||||
{Children.map(children, (child, i) => (
|
||||
<View
|
||||
style={selectedPage === i ? a.flex_1 : a.hidden}
|
||||
key={`page-${i}`}>
|
||||
{child}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import * as React from 'react'
|
||||
import {memo, useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {
|
||||
LayoutChangeEvent,
|
||||
NativeScrollEvent,
|
||||
ScrollView,
|
||||
type LayoutChangeEvent,
|
||||
type NativeScrollEvent,
|
||||
type ScrollView,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import Animated, {
|
||||
AnimatedRef,
|
||||
runOnJS,
|
||||
type AnimatedRef,
|
||||
runOnUI,
|
||||
scrollTo,
|
||||
SharedValue,
|
||||
type SharedValue,
|
||||
useAnimatedRef,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
@@ -20,9 +19,13 @@ import Animated, {
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
|
||||
import {
|
||||
Pager,
|
||||
type PagerRef,
|
||||
type RenderTabBarFnProps,
|
||||
} from '#/view/com/pager/Pager'
|
||||
import {useTheme} from '#/alf'
|
||||
import {ListMethods} from '../util/List'
|
||||
import {type ListMethods} from '../util/List'
|
||||
import {PagerHeaderProvider} from './PagerHeaderContext'
|
||||
import {TabBar} from './TabBar'
|
||||
|
||||
@@ -33,6 +36,7 @@ export interface PagerWithHeaderChildParams {
|
||||
}
|
||||
|
||||
export interface PagerWithHeaderProps {
|
||||
ref?: React.Ref<PagerRef>
|
||||
testID?: string
|
||||
children:
|
||||
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
||||
@@ -49,97 +53,94 @@ export interface PagerWithHeaderProps {
|
||||
onCurrentPageSelected?: (index: number) => void
|
||||
allowHeaderOverScroll?: boolean
|
||||
}
|
||||
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||
function PageWithHeaderImpl(
|
||||
{
|
||||
children,
|
||||
testID,
|
||||
export function PagerWithHeader({
|
||||
ref,
|
||||
children,
|
||||
testID,
|
||||
items,
|
||||
isHeaderReady,
|
||||
renderHeader,
|
||||
initialPage,
|
||||
onPageSelected,
|
||||
onCurrentPageSelected,
|
||||
allowHeaderOverScroll,
|
||||
}: PagerWithHeaderProps) {
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const [tabBarHeight, setTabBarHeight] = useState(0)
|
||||
const [headerOnlyHeight, setHeaderOnlyHeight] = useState(0)
|
||||
const scrollY = useSharedValue(0)
|
||||
const headerHeight = headerOnlyHeight + tabBarHeight
|
||||
|
||||
// capture the header bar sizing
|
||||
const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
|
||||
const height = evt.nativeEvent.layout.height
|
||||
if (height > 0) {
|
||||
// The rounding is necessary to prevent jumps on iOS
|
||||
setTabBarHeight(Math.round(height * 2) / 2)
|
||||
}
|
||||
})
|
||||
const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
|
||||
if (height > 0) {
|
||||
// The rounding is necessary to prevent jumps on iOS
|
||||
setHeaderOnlyHeight(Math.round(height * 2) / 2)
|
||||
}
|
||||
})
|
||||
|
||||
const renderTabBar = useCallback(
|
||||
(props: RenderTabBarFnProps) => {
|
||||
return (
|
||||
<PagerHeaderProvider scrollY={scrollY} headerHeight={headerOnlyHeight}>
|
||||
<PagerTabBar
|
||||
headerOnlyHeight={headerOnlyHeight}
|
||||
items={items}
|
||||
isHeaderReady={isHeaderReady}
|
||||
renderHeader={renderHeader}
|
||||
currentPage={currentPage}
|
||||
onCurrentPageSelected={onCurrentPageSelected}
|
||||
onTabBarLayout={onTabBarLayout}
|
||||
onHeaderOnlyLayout={onHeaderOnlyLayout}
|
||||
onSelect={props.onSelect}
|
||||
scrollY={scrollY}
|
||||
testID={testID}
|
||||
allowHeaderOverScroll={allowHeaderOverScroll}
|
||||
dragProgress={props.dragProgress}
|
||||
dragState={props.dragState}
|
||||
/>
|
||||
</PagerHeaderProvider>
|
||||
)
|
||||
},
|
||||
[
|
||||
headerOnlyHeight,
|
||||
items,
|
||||
isHeaderReady,
|
||||
renderHeader,
|
||||
initialPage,
|
||||
onPageSelected,
|
||||
currentPage,
|
||||
onCurrentPageSelected,
|
||||
onTabBarLayout,
|
||||
onHeaderOnlyLayout,
|
||||
scrollY,
|
||||
testID,
|
||||
allowHeaderOverScroll,
|
||||
}: PagerWithHeaderProps,
|
||||
ref,
|
||||
) {
|
||||
const [currentPage, setCurrentPage] = React.useState(0)
|
||||
const [tabBarHeight, setTabBarHeight] = React.useState(0)
|
||||
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
|
||||
const scrollY = useSharedValue(0)
|
||||
const headerHeight = headerOnlyHeight + tabBarHeight
|
||||
],
|
||||
)
|
||||
|
||||
// capture the header bar sizing
|
||||
const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
|
||||
const height = evt.nativeEvent.layout.height
|
||||
if (height > 0) {
|
||||
// The rounding is necessary to prevent jumps on iOS
|
||||
setTabBarHeight(Math.round(height * 2) / 2)
|
||||
}
|
||||
})
|
||||
const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
|
||||
if (height > 0) {
|
||||
// The rounding is necessary to prevent jumps on iOS
|
||||
setHeaderOnlyHeight(Math.round(height * 2) / 2)
|
||||
}
|
||||
})
|
||||
const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
|
||||
const registerRef = useCallback(
|
||||
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
|
||||
scrollRefs.modify(refs => {
|
||||
'worklet'
|
||||
refs[atIndex] = scrollRef
|
||||
return refs
|
||||
})
|
||||
},
|
||||
[scrollRefs],
|
||||
)
|
||||
|
||||
const renderTabBar = React.useCallback(
|
||||
(props: RenderTabBarFnProps) => {
|
||||
return (
|
||||
<PagerHeaderProvider
|
||||
scrollY={scrollY}
|
||||
headerHeight={headerOnlyHeight}>
|
||||
<PagerTabBar
|
||||
headerOnlyHeight={headerOnlyHeight}
|
||||
items={items}
|
||||
isHeaderReady={isHeaderReady}
|
||||
renderHeader={renderHeader}
|
||||
currentPage={currentPage}
|
||||
onCurrentPageSelected={onCurrentPageSelected}
|
||||
onTabBarLayout={onTabBarLayout}
|
||||
onHeaderOnlyLayout={onHeaderOnlyLayout}
|
||||
onSelect={props.onSelect}
|
||||
scrollY={scrollY}
|
||||
testID={testID}
|
||||
allowHeaderOverScroll={allowHeaderOverScroll}
|
||||
dragProgress={props.dragProgress}
|
||||
dragState={props.dragState}
|
||||
/>
|
||||
</PagerHeaderProvider>
|
||||
)
|
||||
},
|
||||
[
|
||||
headerOnlyHeight,
|
||||
items,
|
||||
isHeaderReady,
|
||||
renderHeader,
|
||||
currentPage,
|
||||
onCurrentPageSelected,
|
||||
onTabBarLayout,
|
||||
onHeaderOnlyLayout,
|
||||
scrollY,
|
||||
testID,
|
||||
allowHeaderOverScroll,
|
||||
],
|
||||
)
|
||||
|
||||
const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
|
||||
const registerRef = React.useCallback(
|
||||
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
|
||||
scrollRefs.modify(refs => {
|
||||
'worklet'
|
||||
refs[atIndex] = scrollRef
|
||||
return refs
|
||||
})
|
||||
},
|
||||
[scrollRefs],
|
||||
)
|
||||
|
||||
const lastForcedScrollY = useSharedValue(0)
|
||||
const adjustScrollForOtherPages = () => {
|
||||
const lastForcedScrollY = useSharedValue(0)
|
||||
const adjustScrollForOtherPages = useCallback(
|
||||
(scrollState: 'idle' | 'dragging' | 'settling') => {
|
||||
'worklet'
|
||||
if (scrollState !== 'dragging') return
|
||||
const currentScrollY = scrollY.get()
|
||||
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
|
||||
if (lastForcedScrollY.get() !== forcedScrollY) {
|
||||
@@ -152,75 +153,69 @@ 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 = useCallback(
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
const nextScrollY = e.contentOffset.y
|
||||
// HACK: onScroll is reporting some strange values on load (negative header height).
|
||||
// Highly improbable that you'd be overscrolled by over 400px -
|
||||
// in fact, I actually can't do it, so let's just ignore those. -sfn
|
||||
const isPossiblyInvalid =
|
||||
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
|
||||
if (!isPossiblyInvalid) {
|
||||
scrollY.set(nextScrollY)
|
||||
}
|
||||
})
|
||||
},
|
||||
[scrollY, headerHeight],
|
||||
)
|
||||
|
||||
const onScrollWorklet = React.useCallback(
|
||||
(e: NativeScrollEvent) => {
|
||||
'worklet'
|
||||
const nextScrollY = e.contentOffset.y
|
||||
// HACK: onScroll is reporting some strange values on load (negative header height).
|
||||
// Highly improbable that you'd be overscrolled by over 400px -
|
||||
// in fact, I actually can't do it, so let's just ignore those. -sfn
|
||||
const isPossiblyInvalid =
|
||||
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
|
||||
if (!isPossiblyInvalid) {
|
||||
scrollY.set(nextScrollY)
|
||||
runOnJS(queueThrottledOnScroll)()
|
||||
}
|
||||
},
|
||||
[scrollY, queueThrottledOnScroll, headerHeight],
|
||||
)
|
||||
const onPageSelectedInner = useCallback(
|
||||
(index: number) => {
|
||||
setCurrentPage(index)
|
||||
onPageSelected?.(index)
|
||||
},
|
||||
[onPageSelected, setCurrentPage],
|
||||
)
|
||||
|
||||
const onPageSelectedInner = React.useCallback(
|
||||
(index: number) => {
|
||||
setCurrentPage(index)
|
||||
onPageSelected?.(index)
|
||||
},
|
||||
[onPageSelected, setCurrentPage],
|
||||
)
|
||||
const onTabPressed = useCallback(() => {
|
||||
runOnUI(adjustScrollForOtherPages)('dragging')
|
||||
}, [adjustScrollForOtherPages])
|
||||
|
||||
return (
|
||||
<Pager
|
||||
ref={ref}
|
||||
testID={testID}
|
||||
initialPage={initialPage}
|
||||
onPageSelected={onPageSelectedInner}
|
||||
renderTabBar={renderTabBar}>
|
||||
{toArray(children)
|
||||
.filter(Boolean)
|
||||
.map((child, i) => {
|
||||
const isReady =
|
||||
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
|
||||
return (
|
||||
<View key={i} collapsable={false}>
|
||||
<PagerItem
|
||||
headerHeight={headerHeight}
|
||||
index={i}
|
||||
isReady={isReady}
|
||||
isFocused={i === currentPage}
|
||||
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
|
||||
registerRef={registerRef}
|
||||
renderTab={child}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</Pager>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<Pager
|
||||
ref={ref}
|
||||
testID={testID}
|
||||
initialPage={initialPage}
|
||||
onTabPressed={onTabPressed}
|
||||
onPageSelected={onPageSelectedInner}
|
||||
renderTabBar={renderTabBar}
|
||||
onPageScrollStateChanged={adjustScrollForOtherPages}>
|
||||
{toArray(children)
|
||||
.filter(Boolean)
|
||||
.map((child, i) => {
|
||||
const isReady =
|
||||
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
|
||||
return (
|
||||
<View key={i} collapsable={false}>
|
||||
<PagerItem
|
||||
headerHeight={headerHeight}
|
||||
index={i}
|
||||
isReady={isReady}
|
||||
isFocused={i === currentPage}
|
||||
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
|
||||
registerRef={registerRef}
|
||||
renderTab={child}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</Pager>
|
||||
)
|
||||
}
|
||||
|
||||
let PagerTabBar = ({
|
||||
currentPage,
|
||||
@@ -258,7 +253,7 @@ let PagerTabBar = ({
|
||||
dragState: SharedValue<'idle' | 'dragging' | 'settling'>
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0)
|
||||
const [minimumHeaderHeight, setMinimumHeaderHeight] = useState(0)
|
||||
const headerTransform = useAnimatedStyle(() => {
|
||||
const translateY =
|
||||
Math.min(
|
||||
@@ -275,7 +270,7 @@ let PagerTabBar = ({
|
||||
],
|
||||
}
|
||||
})
|
||||
const headerRef = React.useRef(null)
|
||||
const headerRef = useRef(null)
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents={isIOS ? 'auto' : 'box-none'}
|
||||
@@ -327,7 +322,7 @@ let PagerTabBar = ({
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
PagerTabBar = React.memo(PagerTabBar)
|
||||
PagerTabBar = memo(PagerTabBar)
|
||||
|
||||
function PagerItem({
|
||||
headerHeight,
|
||||
@@ -348,7 +343,7 @@ function PagerItem({
|
||||
}) {
|
||||
const scrollElRef = useAnimatedRef()
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
registerRef(scrollElRef, index)
|
||||
return () => {
|
||||
registerRef(null, index)
|
||||
|
||||
@@ -46,7 +46,6 @@ import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {Link} from '#/view/com/util/Link'
|
||||
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 {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
@@ -62,6 +61,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {PostHider} from '#/components/moderation/PostHider'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||
import {PostControls} from '#/components/PostControls'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
@@ -466,7 +466,7 @@ let PostThreadItemLoaded = ({
|
||||
) : undefined}
|
||||
{post.embed && (
|
||||
<View style={[a.py_xs]}>
|
||||
<PostEmbeds
|
||||
<Embed
|
||||
embed={post.embed}
|
||||
moderation={moderation}
|
||||
viewContext={PostEmbedViewContext.ThreadHighlighted}
|
||||
@@ -693,7 +693,7 @@ let PostThreadItemLoaded = ({
|
||||
) : undefined}
|
||||
{post.embed && (
|
||||
<View style={[a.pb_xs]}>
|
||||
<PostEmbeds
|
||||
<Embed
|
||||
embed={post.embed}
|
||||
moderation={moderation}
|
||||
viewContext={PostEmbedViewContext.Feed}
|
||||
|
||||
@@ -27,7 +27,6 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {precacheProfile} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {Link} from '#/view/com/util/Link'
|
||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
@@ -36,6 +35,7 @@ import {atoms as a} from '#/alf'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
|
||||
import {PostControls} from '#/components/PostControls'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {RichText} from '#/components/RichText'
|
||||
@@ -242,7 +242,7 @@ function PostInner({
|
||||
</View>
|
||||
) : undefined}
|
||||
{post.embed ? (
|
||||
<PostEmbeds
|
||||
<Embed
|
||||
embed={post.embed}
|
||||
moderation={moderation}
|
||||
viewContext={PostEmbedViewContext.Feed}
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
} from '#/state/unstable-post-source'
|
||||
import {FeedNameText} from '#/view/com/util/FeedInfoText'
|
||||
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 {Text} from '#/view/com/util/text/Text'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
@@ -53,6 +52,8 @@ import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
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 {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
@@ -562,7 +563,7 @@ let PostContent = ({
|
||||
) : undefined}
|
||||
{postEmbed ? (
|
||||
<View style={[a.pb_xs]}>
|
||||
<PostEmbeds
|
||||
<Embed
|
||||
embed={postEmbed}
|
||||
moderation={moderation}
|
||||
onOpen={onOpenEmbed}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
||||
import {AppBskyGraphDefs} from '@atproto/api'
|
||||
import Animated, {
|
||||
measure,
|
||||
type MeasuredDimensions,
|
||||
runOnJS,
|
||||
runOnUI,
|
||||
useAnimatedRef,
|
||||
} from 'react-native-reanimated'
|
||||
import {type AppBskyGraphDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||
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 {emitSoftReset} from '#/state/events'
|
||||
import {useLightboxControls} from '#/state/lightbox'
|
||||
import {TextLink} from '#/view/com/util/Link'
|
||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||
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 * as Layout from '#/components/Layout'
|
||||
|
||||
@@ -52,7 +57,7 @@ export function ProfileSubpageHeader({
|
||||
const {openLightbox} = useLightboxControls()
|
||||
const pal = usePalette('default')
|
||||
const canGoBack = navigation.canGoBack()
|
||||
const aviRef = useHandleRef()
|
||||
const aviRef = useAnimatedRef()
|
||||
|
||||
const _openLightbox = React.useCallback(
|
||||
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
||||
@@ -81,10 +86,9 @@ export function ProfileSubpageHeader({
|
||||
if (
|
||||
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
||||
) {
|
||||
const aviHandle = aviRef.current
|
||||
runOnUI(() => {
|
||||
'worklet'
|
||||
const rect = measureHandle(aviHandle)
|
||||
const rect = measure(aviRef)
|
||||
runOnJS(_openLightbox)(avatar, rect)
|
||||
})()
|
||||
}
|
||||
@@ -111,7 +115,7 @@ export function ProfileSubpageHeader({
|
||||
paddingBottom: 14,
|
||||
paddingHorizontal: isMobile ? 12 : 14,
|
||||
}}>
|
||||
<View ref={aviRef} collapsable={false}>
|
||||
<Animated.View ref={aviRef} collapsable={false}>
|
||||
<Pressable
|
||||
testID="headerAviButton"
|
||||
onPress={onPressAvi}
|
||||
@@ -125,7 +129,7 @@ export function ProfileSubpageHeader({
|
||||
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
</Animated.View>
|
||||
<View style={{flex: 1, gap: 4}}>
|
||||
{isLoading ? (
|
||||
<LoadingPlaceholder
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type FontAwesomeIconStyle,
|
||||
type Props as FontAwesomeProps,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import type React from 'react'
|
||||
|
||||
const DURATION = 3500
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
import React from 'react'
|
||||
import {
|
||||
FlatList,
|
||||
FlatListProps,
|
||||
ScrollViewProps,
|
||||
type FlatList,
|
||||
type FlatListProps,
|
||||
type ScrollViewProps,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewProps,
|
||||
type ViewProps,
|
||||
} from 'react-native'
|
||||
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