Fabric - Running on iOS and Android 🥳 (#3114)

* remove unused packages, switch to `expo-linear-gradient`

* upgrade expo deps

* rm blur view

* re-add normalize-url

* replace `react-native-version-number` with `expo-application`

* add `expo-haptics`

* rm `react-native-haptic-feedback`

* migrate to `expo-haptics`

* add `expo-clipboard`

* migrate to `expo-clipboard`

* add `expo-file-system`

* migrate to `expo-file-system` and `expo-image-manipulator`

passing tests

remove other `react-native-fs` usages

move to `expo-image-manipulator` for resizes

remove react-native-image-resizer

update tests

update jest setup

simplify some logic

properly cleanup files

migrate file downloads to `expo-file-system`

rm `rn-fetch-blob`

* delete file after uploading blob on native

* fix file move error

* add `react-native-date-picker`

* rm `@reactnativecommunity/datetimepicker`

* migrate to `react-native-date-picker`

* use modal on android

* fix android alf

* use @discord/bottom-sheet

* rm some patches

* remove expo-dev-client

* rm metro config changes that have been merged

* Working build

* ignore error for now

* ignore error for now

* add newArchEnabled flag

* add types/invariant
This commit is contained in:
Hailey
2024-03-05 15:46:40 -08:00
committed by GitHub
parent 620f6a1bfd
commit 84caf1e8d8
57 changed files with 949 additions and 1255 deletions
+94 -51
View File
@@ -1,25 +1,28 @@
import { import {
downloadAndResize, downloadAndResize,
DownloadAndResizeOpts, DownloadAndResizeOpts,
} from '../../src/lib/media/manip' getResizedDimensions,
import ImageResizer from '@bam.tech/react-native-image-resizer' } from 'lib/media/manip'
import RNFetchBlob from 'rn-fetch-blob' import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import {createDownloadResumable, deleteAsync} from 'expo-file-system'
describe('downloadAndResize', () => { describe('downloadAndResize', () => {
const errorSpy = jest.spyOn(global.console, 'error') const errorSpy = jest.spyOn(global.console, 'error')
const mockResizedImage = { const mockResizedImage = {
path: jest.fn().mockReturnValue('file://resized-image.jpg'), path: 'file://resized-image.jpg',
size: 100, size: 100,
width: 50, width: 100,
height: 50, height: 100,
mime: 'image/jpeg', mime: 'image/jpeg',
} }
beforeEach(() => { beforeEach(() => {
const mockedCreateResizedImage = const mockedCreateResizedImage = manipulateAsync as jest.Mock
ImageResizer.createResizedImage as jest.Mock mockedCreateResizedImage.mockResolvedValue({
mockedCreateResizedImage.mockResolvedValue(mockResizedImage) uri: 'file://resized-image.jpg',
...mockResizedImage,
})
}) })
afterEach(() => { afterEach(() => {
@@ -27,10 +30,12 @@ describe('downloadAndResize', () => {
}) })
it('should return resized image for valid URI and options', async () => { it('should return resized image for valid URI and options', async () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock const mockedFetch = createDownloadResumable as jest.Mock
mockedFetch.mockResolvedValueOnce({ mockedFetch.mockReturnValue({
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'), cancelAsync: jest.fn(),
flush: jest.fn(), downloadAsync: jest
.fn()
.mockResolvedValue({uri: 'file://resized-image.jpg'}),
}) })
const opts: DownloadAndResizeOpts = { const opts: DownloadAndResizeOpts = {
@@ -44,25 +49,22 @@ describe('downloadAndResize', () => {
const result = await downloadAndResize(opts) const result = await downloadAndResize(opts)
expect(result).toEqual(mockResizedImage) expect(result).toEqual(mockResizedImage)
expect(RNFetchBlob.config).toHaveBeenCalledWith({ expect(createDownloadResumable).toHaveBeenCalledWith(
fileCache: true, opts.uri,
appendExt: 'jpeg', expect.anything(),
{
cache: true,
},
)
expect(manipulateAsync).toHaveBeenCalledWith(expect.anything(), [], {
format: SaveFormat.JPEG,
}) })
expect(RNFetchBlob.fetch).toHaveBeenCalledWith( expect(manipulateAsync).toHaveBeenCalledWith(
'GET', expect.anything(),
'https://example.com/image.jpg', [{resize: {height: opts.height, width: opts.width}}],
) {format: SaveFormat.JPEG, compress: 0.9},
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
'file://downloaded-image.jpg',
100,
100,
'JPEG',
100,
undefined,
undefined,
undefined,
{mode: 'cover'},
) )
expect(deleteAsync).toHaveBeenCalledWith(expect.anything())
}) })
it('should return undefined for invalid URI', async () => { it('should return undefined for invalid URI', async () => {
@@ -81,10 +83,12 @@ describe('downloadAndResize', () => {
}) })
it('should return undefined for unsupported file type', async () => { it('should return undefined for unsupported file type', async () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock const mockedFetch = createDownloadResumable as jest.Mock
mockedFetch.mockResolvedValueOnce({ mockedFetch.mockReturnValue({
path: jest.fn().mockReturnValue('file://downloaded-image'), cancelAsync: jest.fn(),
flush: jest.fn(), downloadAsync: jest
.fn()
.mockResolvedValue({uri: 'file://downloaded-image'}),
}) })
const opts: DownloadAndResizeOpts = { const opts: DownloadAndResizeOpts = {
@@ -98,24 +102,63 @@ describe('downloadAndResize', () => {
const result = await downloadAndResize(opts) const result = await downloadAndResize(opts)
expect(result).toEqual(mockResizedImage) expect(result).toEqual(mockResizedImage)
expect(RNFetchBlob.config).toHaveBeenCalledWith({ expect(createDownloadResumable).toHaveBeenCalledWith(
fileCache: true, opts.uri,
appendExt: 'jpeg', expect.anything(),
{
cache: true,
},
)
expect(manipulateAsync).toHaveBeenCalledWith(expect.anything(), [], {
format: SaveFormat.JPEG,
}) })
expect(RNFetchBlob.fetch).toHaveBeenCalledWith( expect(manipulateAsync).toHaveBeenCalledWith(
'GET', expect.anything(),
'https://example.com/image', [{resize: {height: opts.height, width: opts.width}}],
) {format: SaveFormat.JPEG, compress: 0.9},
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
'file://downloaded-image',
100,
100,
'JPEG',
100,
undefined,
undefined,
undefined,
{mode: 'cover'},
) )
expect(deleteAsync).toHaveBeenCalledWith(expect.anything())
})
})
describe('produces correct new sizes for images', () => {
it('should not downsize whenever dimensions are below the max dimensions', () => {
const initialDimensionsOne = {
width: 1200,
height: 1000,
}
const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
const initialDimensionsTwo = {
width: 1000,
height: 1200,
}
const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
expect(resizedDimensionsOne).toEqual(initialDimensionsOne)
expect(resizedDimensionsTwo).toEqual(initialDimensionsTwo)
})
it('should resize dimensions and maintain aspect ratio if they are above the max dimensons', () => {
const initialDimensionsOne = {
width: 3000,
height: 1500,
}
const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
const initialDimensionsTwo = {
width: 2000,
height: 4000,
}
const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
expect(resizedDimensionsOne).toEqual({
width: 2000,
height: 1000,
})
expect(resizedDimensionsTwo).toEqual({
width: 1000,
height: 2000,
})
}) })
}) })
+11 -6
View File
@@ -36,14 +36,19 @@ jest.mock('react-native-safe-area-context', () => {
} }
}) })
jest.mock('rn-fetch-blob', () => ({ jest.mock('expo-file-system', () => ({
config: jest.fn().mockReturnThis(), createDownloadResumable: jest.fn(),
cancel: jest.fn(), deleteAsync: jest.fn(),
fetch: jest.fn(), getInfoAsync: jest.fn().mockResolvedValue({
size: 100,
}),
})) }))
jest.mock('@bam.tech/react-native-image-resizer', () => ({ jest.mock('expo-image-manipulator', () => ({
createResizedImage: jest.fn(), manipulateAsync: jest.fn().mockResolvedValue({
uri: 'file://resized-image',
}),
SaveFormat: jest.requireActual('expo-image-manipulator').SaveFormat,
})) }))
jest.mock('@segment/analytics-react-native', () => ({ jest.mock('@segment/analytics-react-native', () => ({
-9
View File
@@ -10,15 +10,6 @@ cfg.transformer.getTransformOptions = async () => ({
transform: { transform: {
experimentalImportSupport: true, experimentalImportSupport: true,
inlineRequires: true, inlineRequires: true,
nonInlinedRequires: [
// We can remove this option and rely on the default after
// https://github.com/facebook/metro/pull/1126 is released.
'React',
'react',
'react/jsx-dev-runtime',
'react/jsx-runtime',
'react-native',
],
}, },
}) })
+31 -41
View File
@@ -45,8 +45,8 @@
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.10.4", "@atproto/api": "^0.10.4",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
"@expo/html-elements": "^0.4.2", "@expo/html-elements": "^0.4.2",
"@expo/webpack-config": "^19.0.0", "@expo/webpack-config": "^19.0.0",
@@ -54,15 +54,10 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.0", "@fortawesome/react-native-fontawesome": "^0.3.0",
"@gorhom/bottom-sheet": "^4.5.1",
"@lingui/react": "^4.5.0", "@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.6.4", "@mattermost/react-native-paste-input": "^0.6.4",
"@miblanchard/react-native-slider": "^2.3.1", "@miblanchard/react-native-slider": "^2.3.1",
"@react-native-async-storage/async-storage": "1.21.0", "@react-native-async-storage/async-storage": "^1.22.3",
"@react-native-camera-roll/camera-roll": "^5.2.2",
"@react-native-clipboard/clipboard": "^1.10.0",
"@react-native-community/blur": "^4.3.0",
"@react-native-community/datetimepicker": "7.6.1",
"@react-native-masked-view/masked-view": "0.3.0", "@react-native-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^0.8.0", "@react-native-menu/menu": "^0.8.0",
"@react-native-picker/picker": "2.6.1", "@react-native-picker/picker": "2.6.1",
@@ -100,27 +95,30 @@
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.5.2",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"expo": "^50.0.0-preview.10", "expo": "^50.0.8",
"expo-application": "~5.8.2", "expo-application": "~5.8.3",
"expo-build-properties": "^0.11.0", "expo-build-properties": "^0.11.1",
"expo-camera": "~14.0.1", "expo-camera": "~14.0.4",
"expo-constants": "~15.4.3", "expo-clipboard": "^5.0.1",
"expo-dev-client": "~3.3.5", "expo-constants": "~15.4.5",
"expo-device": "~5.9.2", "expo-device": "~5.9.3",
"expo-image": "~1.10.3", "expo-file-system": "^16.0.7",
"expo-haptics": "^12.8.1",
"expo-image": "~1.10.6",
"expo-image-manipulator": "^11.8.0", "expo-image-manipulator": "^11.8.0",
"expo-image-picker": "~14.7.1", "expo-image-picker": "~14.7.1",
"expo-linear-gradient": "^12.7.1",
"expo-linking": "^6.2.2", "expo-linking": "^6.2.2",
"expo-localization": "~14.8.2", "expo-localization": "~14.8.3",
"expo-media-library": "~15.9.1", "expo-media-library": "~15.9.1",
"expo-notifications": "~0.27.3", "expo-notifications": "~0.27.6",
"expo-sharing": "^11.10.0", "expo-sharing": "^11.10.0",
"expo-splash-screen": "~0.26.2", "expo-splash-screen": "~0.26.4",
"expo-status-bar": "~1.11.1", "expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.9.3", "expo-system-ui": "~2.9.3",
"expo-task-manager": "~11.7.0", "expo-task-manager": "~11.7.2",
"expo-updates": "~0.24.7", "expo-updates": "~0.24.10",
"expo-web-browser": "~12.8.1", "expo-web-browser": "~12.8.2",
"fast-text-encoding": "^1.0.6", "fast-text-encoding": "^1.0.6",
"history": "^5.3.0", "history": "^5.3.0",
"js-sha256": "^0.9.0", "js-sha256": "^0.9.0",
@@ -135,7 +133,6 @@
"lodash.samplesize": "^4.2.0", "lodash.samplesize": "^4.2.0",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"lodash.shuffle": "^4.2.0", "lodash.shuffle": "^4.2.0",
"lru_map": "^0.4.1",
"mobx": "^6.6.1", "mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0", "mobx-react-lite": "^3.4.0",
"mobx-utils": "^6.0.6", "mobx-utils": "^6.0.6",
@@ -146,40 +143,32 @@
"psl": "^1.9.0", "psl": "^1.9.0",
"react": "18.2.0", "react": "18.2.0",
"react-avatar-editor": "^13.0.0", "react-avatar-editor": "^13.0.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-native": "0.73.2", "react-native": "~0.73.5",
"react-native-appstate-hook": "^1.0.6", "react-native-date-picker": "^4.4.0",
"react-native-drawer-layout": "^4.0.0-alpha.3", "react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.15.0",
"react-native-gesture-handler": "~2.14.0",
"react-native-get-random-values": "~1.8.0", "react-native-get-random-values": "~1.8.0",
"react-native-haptic-feedback": "^1.14.0", "react-native-image-crop-picker": "~0.40.3",
"react-native-image-crop-picker": "^0.38.1",
"react-native-ios-context-menu": "^1.15.3", "react-native-ios-context-menu": "^1.15.3",
"react-native-linear-gradient": "^2.6.2",
"react-native-pager-view": "6.2.3", "react-native-pager-view": "6.2.3",
"react-native-picker-select": "^8.1.0", "react-native-picker-select": "~9.0.1",
"react-native-progress": "bluesky-social/react-native-progress", "react-native-progress": "bluesky-social/react-native-progress",
"react-native-reanimated": "^3.6.0", "react-native-reanimated": "~3.7.2",
"react-native-root-siblings": "^4.1.1", "react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "4.8.2", "react-native-safe-area-context": "~4.9.0",
"react-native-screens": "~3.29.0", "react-native-screens": "~3.29.0",
"react-native-svg": "14.1.0", "react-native-svg": "~15.1.0",
"react-native-ui-text-view": "link:./modules/react-native-ui-text-view", "react-native-uitextview": "^1.1.4",
"react-native-url-polyfill": "^1.3.0", "react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.1", "react-native-uuid": "^2.0.1",
"react-native-version-number": "^0.3.6",
"react-native-web": "~0.19.6", "react-native-web": "~0.19.6",
"react-native-web-linear-gradient": "^1.1.2",
"react-native-web-webview": "^1.0.2", "react-native-web-webview": "^1.0.2",
"react-native-webview": "13.6.4", "react-native-webview": "~13.8.1",
"react-responsive": "^9.0.2", "react-responsive": "^9.0.2",
"rn-fetch-blob": "^0.12.0", "sentry-expo": "~7.2.0",
"sentry-expo": "~7.0.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tlds": "^1.234.0", "tlds": "^1.234.0",
"use-deep-compare": "^1.1.0",
"zeego": "^1.6.2", "zeego": "^1.6.2",
"zod": "^3.20.2" "zod": "^3.20.2"
}, },
@@ -200,6 +189,7 @@
"@testing-library/react-native": "^11.5.2", "@testing-library/react-native": "^11.5.2",
"@tsconfig/react-native": "^2.0.3", "@tsconfig/react-native": "^2.0.3",
"@types/he": "^1.1.2", "@types/he": "^1.1.2",
"@types/invariant": "^2.2.37",
"@types/jest": "^29.4.0", "@types/jest": "^29.4.0",
"@types/lodash.chunk": "^4.2.7", "@types/lodash.chunk": "^4.2.7",
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.7",
-44
View File
@@ -1,44 +0,0 @@
diff --git a/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js b/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
index 48a1409..ef185c9 100644
--- a/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
+++ b/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
@@ -70,14 +70,19 @@ function wrapModule(
importDefaultName,
importAllName,
dependencyMapName,
- globalPrefix
+ globalPrefix,
+ moduleFactoryName
) {
const params = buildParameters(
importDefaultName,
importAllName,
dependencyMapName
);
- const factory = functionFromProgram(fileAst.program, params);
+ const factory = functionFromProgram(
+ fileAst.program,
+ params,
+ moduleFactoryName
+ );
const def = t.callExpression(t.identifier(`${globalPrefix}__d`), [factory]);
const ast = t.file(t.program([t.expressionStatement(def)]));
const requireName = renameRequires(ast);
@@ -107,7 +112,16 @@ function wrapJson(source, globalPrefix) {
"});",
].join("\n");
}
-function functionFromProgram(program, parameters) {
+const JS_INVALID_IDENT_RE = /[^a-zA-Z0-9$_]/g;
+function functionFromProgram(program, parameters, moduleFactoryName) {
+ let identifier;
+ if (typeof moduleFactoryName === "string" && moduleFactoryName !== "") {
+ // Keep the name readable so it shows up in profiler traces.
+ // Add an unlikely suffix to avoid collisions with the module code.
+ identifier = t.identifier(
+ `${moduleFactoryName.replace(JS_INVALID_IDENT_RE, "_")}__module_factory__`
+ );
+ }
return t.functionExpression(
undefined,
parameters.map(makeIdentifier),
-50
View File
@@ -1,50 +0,0 @@
diff --git a/node_modules/metro-runtime/src/polyfills/require.js b/node_modules/metro-runtime/src/polyfills/require.js
index ce67cb4..eeeae84 100644
--- a/node_modules/metro-runtime/src/polyfills/require.js
+++ b/node_modules/metro-runtime/src/polyfills/require.js
@@ -22,6 +22,13 @@ global.__c = clear;
global.__registerSegment = registerSegment;
var modules = clear();
+if (__DEV__) {
+ // Added by Dan for module init logging.
+ global.__INIT_LOGS__ = []
+ var initModuleCounter = 0
+ var initModuleStack = []
+}
+
// Don't use a Symbol here, it would pull in an extra polyfill with all sorts of
// additional stuff (e.g. Array.from).
const EMPTY = {};
@@ -303,7 +310,30 @@ function loadModuleImplementation(moduleId, module) {
throw module.error;
}
if (__DEV__) {
- var Systrace = requireSystrace();
+ // Added by Dan for module init logging.
+ var Systrace = {
+ beginEvent(label) {
+ let fullLabel = initModuleCounter++ + ' ' + label
+ global.__INIT_LOGS__.push(
+ ' '.repeat(initModuleStack.length) +
+ ' ENTER ' + fullLabel
+ )
+ initModuleStack.push({
+ fullLabel,
+ startTime: nativePerformanceNow(),
+ })
+ },
+ endEvent() {
+ const res = initModuleStack.pop()
+ const fullLabel = res.fullLabel
+ const startTime = res.startTime
+ const timeElapsed = Math.round(nativePerformanceNow() - startTime)
+ global.__INIT_LOGS__.push(
+ ' '.repeat(initModuleStack.length) +
+ ' LEAVE ' + fullLabel + ' [' + timeElapsed + 'ms]',
+ )
+ }
+ };
var Refresh = requireRefresh();
}
@@ -1,41 +0,0 @@
diff --git a/node_modules/metro-transform-worker/src/index.js b/node_modules/metro-transform-worker/src/index.js
index 9f2e3d2..5222c8e 100644
--- a/node_modules/metro-transform-worker/src/index.js
+++ b/node_modules/metro-transform-worker/src/index.js
@@ -189,6 +189,10 @@ async function transformJS(file, { config, options, projectRoot }) {
let dependencyMapName = "";
let dependencies;
let wrappedAst;
+ const minify =
+ options.minify &&
+ options.unstable_transformProfile !== "hermes-canary" &&
+ options.unstable_transformProfile !== "hermes-stable";
// If the module to transform is a script (meaning that is not part of the
// dependency graph and it code will just be prepended to the bundle modules),
@@ -228,19 +232,20 @@ async function transformJS(file, { config, options, projectRoot }) {
if (config.unstable_disableModuleWrapping === true) {
wrappedAst = ast;
} else {
+ let moduleFactoryName;
+ if (options.dev && !minify) {
+ moduleFactoryName = file.filename;
+ }
({ ast: wrappedAst } = JsFileWrapping.wrapModule(
ast,
importDefault,
importAll,
dependencyMapName,
- config.globalPrefix
+ config.globalPrefix,
+ moduleFactoryName
));
}
}
- const minify =
- options.minify &&
- options.unstable_transformProfile !== "hermes-canary" &&
- options.unstable_transformProfile !== "hermes-stable";
const reserved = [];
if (config.unstable_dependencyMapReservedName != null) {
reserved.push(config.unstable_dependencyMapReservedName);
-92
View File
@@ -1,92 +0,0 @@
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
index 9dca6a5..090bda5 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
@@ -266,11 +266,10 @@ - (void)textViewDidChange:(__unused UITextView *)textView
- (void)textViewDidChangeSelection:(__unused UITextView *)textView
{
- if (_lastStringStateWasUpdatedWith && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
+ if (![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
[self textViewDidChange:_backedTextInputView];
_ignoreNextTextInputCall = YES;
}
- _lastStringStateWasUpdatedWith = _backedTextInputView.attributedText;
[self textViewProbablyDidChangeSelection];
}
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
index 1f06b79..ab458f3 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
@@ -87,7 +87,7 @@ - (void)invalidateContentSize
return;
}
- CGSize maximumSize = self.layoutMetrics.frame.size;
+ CGSize maximumSize = self.layoutMetrics.contentFrame.size;
if (_maximumNumberOfLines == 1) {
maximumSize.width = CGFLOAT_MAX;
@@ -158,6 +158,8 @@ - (void)uiManagerWillPerformMounting
[attributedText insertAttributedString:propertyAttributedText atIndex:0];
}
+ [self postprocessAttributedText:attributedText];
+
NSAttributedString *newAttributedText;
if (![_previousAttributedText isEqualToAttributedString:attributedText]) {
// We have to follow `set prop` pattern:
@@ -191,6 +193,52 @@ - (void)uiManagerWillPerformMounting
}];
}
+- (void)postprocessAttributedText:(NSMutableAttributedString *)attributedText
+{
+ __block CGFloat maximumLineHeight = 0;
+
+ [attributedText enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:NSMakeRange(0, attributedText.length)
+ options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) {
+ if (!paragraphStyle) {
+ return;
+ }
+
+ maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight);
+ }];
+
+ if (maximumLineHeight == 0) {
+ // `lineHeight` was not specified, nothing to do.
+ return;
+ }
+
+ __block CGFloat maximumFontLineHeight = 0;
+
+ [attributedText enumerateAttribute:NSFontAttributeName
+ inRange:NSMakeRange(0, attributedText.length)
+ options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
+ if (!font) {
+ return;
+ }
+
+ if (maximumFontLineHeight <= font.lineHeight) {
+ maximumFontLineHeight = font.lineHeight;
+ }
+ }];
+
+ if (maximumLineHeight < maximumFontLineHeight) {
+ return;
+ }
+
+ CGFloat baseLineOffset = maximumLineHeight / 2.0 - maximumFontLineHeight / 2.0;
+
+ [attributedText addAttribute:NSBaselineOffsetAttributeName
+ value:@(baseLineOffset)
+ range:NSMakeRange(0, attributedText.length)];
+}
+
#pragma mark -
- (NSAttributedString *)measurableAttributedText
-5
View File
@@ -1,5 +0,0 @@
# TextInput Patch
Patching `RCTBaseTextShadowInput.mm` from https://github.com/facebook/react-native/pull/38359. This fixes some text
getting cut off inside the composer. This was merged in December, so we should be able to remove this patch when RN
ships the next release.
+22 -23
View File
@@ -41,7 +41,6 @@ import {
} from 'state/session' } from 'state/session'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -50,7 +49,7 @@ import {useIntentHandler} from 'lib/hooks/useIntentHandler'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
function InnerApp() { function InnerApp() {
const {isInitialLoad, currentAccount} = useSession() const {currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
const theme = useColorModeTheme() const theme = useColorModeTheme()
const {_} = useLingui() const {_} = useLingui()
@@ -70,27 +69,27 @@ function InnerApp() {
return ( return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}> <SafeAreaProvider initialMetrics={initialWindowMetrics}>
<Alf theme={theme}> <Alf theme={theme}>
<Splash isReady={!isInitialLoad}> {/*<Splash isReady={!isInitialLoad}>*/}
<React.Fragment <React.Fragment
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:
key={currentAccount?.did}> key={currentAccount?.did}>
<LoggedOutViewProvider> <LoggedOutViewProvider>
<SelectedFeedProvider> <SelectedFeedProvider>
<UnreadNotifsProvider> <UnreadNotifsProvider>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
{/* All components should be within this provider */} {/* All components should be within this provider */}
<RootSiblingParent> <RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}> <GestureHandlerRootView style={s.h100pct}>
<TestCtrls /> <TestCtrls />
<Shell /> <Shell />
</GestureHandlerRootView> </GestureHandlerRootView>
</RootSiblingParent> </RootSiblingParent>
</ThemeProvider> </ThemeProvider>
</UnreadNotifsProvider> </UnreadNotifsProvider>
</SelectedFeedProvider> </SelectedFeedProvider>
</LoggedOutViewProvider> </LoggedOutViewProvider>
</React.Fragment> </React.Fragment>
</Splash> {/*</Splash>*/}
</Alf> </Alf>
</SafeAreaProvider> </SafeAreaProvider>
) )
+1 -1
View File
@@ -11,7 +11,7 @@ import {
StyleSheet, StyleSheet,
StyleProp, StyleProp,
} from 'react-native' } from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {useTheme, atoms as a, tokens, android, flatten} from '#/alf' import {useTheme, atoms as a, tokens, android, flatten} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common' import {Props as SVGIconProps} from '#/components/icons/common'
+1 -1
View File
@@ -7,7 +7,7 @@ import BottomSheet, {
BottomSheetView, BottomSheetView,
useBottomSheet, useBottomSheet,
WINDOW_HEIGHT, WINDOW_HEIGHT,
} from '@gorhom/bottom-sheet' } from '@discord/bottom-sheet/src'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Animated, {useAnimatedStyle} from 'react-native-reanimated' import Animated, {useAnimatedStyle} from 'react-native-reanimated'
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import type {AccessibilityProps} from 'react-native' import type {AccessibilityProps} from 'react-native'
import {BottomSheetProps} from '@gorhom/bottom-sheet' import {BottomSheetProps} from '@discord/bottom-sheet/src'
import {ViewStyleProp} from '#/alf' import {ViewStyleProp} from '#/alf'
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {Text as RNText, TextStyle, TextProps as RNTextProps} from 'react-native' import {Text as RNText, TextStyle, TextProps as RNTextProps} from 'react-native'
import {UITextView} from 'react-native-ui-text-view' import {UITextView} from 'react-native-uitextview'
import {useTheme, atoms, web, flatten} from '#/alf' import {useTheme, atoms, web, flatten} from '#/alf'
import {isIOS} from '#/platform/detection' import {isIOS} from '#/platform/detection'
@@ -1,8 +1,5 @@
import React from 'react' import React from 'react'
import {View, Pressable} from 'react-native' import {View, Pressable} from 'react-native'
import DateTimePicker, {
BaseProps as DateTimePickerProps,
} from '@react-native-community/datetimepicker'
import {useTheme, atoms} from '#/alf' import {useTheme, atoms} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -15,6 +12,8 @@ import {
localizeDate, localizeDate,
toSimpleDateString, toSimpleDateString,
} from '#/components/forms/DateField/utils' } from '#/components/forms/DateField/utils'
import DatePicker from 'react-native-date-picker'
import {isAndroid} from 'platform/detection'
export * as utils from '#/components/forms/DateField/utils' export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label export const Label = TextField.Label
@@ -38,20 +37,20 @@ export function DateField({
const {chromeFocus, chromeError, chromeErrorHover} = const {chromeFocus, chromeError, chromeErrorHover} =
TextField.useSharedInputStyles() TextField.useSharedInputStyles()
const onChangeInternal = React.useCallback< const onChangeInternal = React.useCallback(
Required<DateTimePickerProps>['onChange'] (date: Date) => {
>(
(_event, date) => {
setOpen(false) setOpen(false)
if (date) { const formatted = toSimpleDateString(date)
const formatted = toSimpleDateString(date) onChangeDate(formatted)
onChangeDate(formatted)
}
}, },
[onChangeDate, setOpen], [onChangeDate, setOpen],
) )
const onCancel = React.useCallback(() => {
setOpen(false)
}, [])
return ( return (
<View style={[atoms.relative, atoms.w_full]}> <View style={[atoms.relative, atoms.w_full]}>
<Pressable <Pressable
@@ -89,18 +88,18 @@ export function DateField({
</Pressable> </Pressable>
{open && ( {open && (
<DateTimePicker <DatePicker
modal={isAndroid}
open={isAndroid}
theme={t.name === 'light' ? 'light' : 'dark'}
date={new Date(value)}
onConfirm={onChangeInternal}
onCancel={onCancel}
mode="date"
testID={`${testID}-datepicker`}
aria-label={label} aria-label={label}
accessibilityLabel={label} accessibilityLabel={label}
accessibilityHint={undefined} accessibilityHint={undefined}
testID={`${testID}-datepicker`}
mode="date"
timeZoneName={'Etc/UTC'}
display="spinner"
// @ts-ignore applies in iOS only -prf
themeVariant={t.name === 'light' ? 'light' : 'dark'}
value={new Date(value)}
onChange={onChangeInternal}
/> />
)} )}
</View> </View>
+8 -12
View File
@@ -1,13 +1,11 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import DateTimePicker, {
DateTimePickerEvent,
} from '@react-native-community/datetimepicker'
import {useTheme, atoms} from '#/alf' import {useTheme, atoms} from '#/alf'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import {toSimpleDateString} from '#/components/forms/DateField/utils' import {toSimpleDateString} from '#/components/forms/DateField/utils'
import {DateFieldProps} from '#/components/forms/DateField/types' import {DateFieldProps} from '#/components/forms/DateField/types'
import DatePicker from 'react-native-date-picker'
export * as utils from '#/components/forms/DateField/utils' export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label export const Label = TextField.Label
@@ -28,7 +26,7 @@ export function DateField({
const t = useTheme() const t = useTheme()
const onChangeInternal = React.useCallback( const onChangeInternal = React.useCallback(
(event: DateTimePickerEvent, date: Date | undefined) => { (date: Date | undefined) => {
if (date) { if (date) {
const formatted = toSimpleDateString(date) const formatted = toSimpleDateString(date)
onChangeDate(formatted) onChangeDate(formatted)
@@ -39,17 +37,15 @@ export function DateField({
return ( return (
<View style={[atoms.relative, atoms.w_full]}> <View style={[atoms.relative, atoms.w_full]}>
<DateTimePicker <DatePicker
theme={t.name === 'light' ? 'light' : 'dark'}
date={new Date(value)}
onDateChange={onChangeInternal}
mode="date"
testID={`${testID}-datepicker`}
aria-label={label} aria-label={label}
accessibilityLabel={label} accessibilityLabel={label}
accessibilityHint={undefined} accessibilityHint={undefined}
testID={`${testID}-datepicker`}
mode="date"
timeZoneName={'Etc/UTC'}
display="spinner"
themeVariant={t.name === 'light' ? 'light' : 'dark'}
value={new Date(value)}
onChange={onChangeInternal}
/> />
</View> </View>
) )
+21 -4
View File
@@ -1,5 +1,5 @@
import {BskyAgent, stringifyLex, jsonToLex} from '@atproto/api' import {BskyAgent, stringifyLex, jsonToLex} from '@atproto/api'
import RNFS from 'react-native-fs' import {cacheDirectory, copyAsync, moveAsync} from 'expo-file-system'
const GET_TIMEOUT = 15e3 // 15s const GET_TIMEOUT = 15e3 // 15s
const POST_TIMEOUT = 60e3 // 60s const POST_TIMEOUT = 60e3 // 60s
@@ -33,9 +33,26 @@ async function fetchHandler(
// we get around that by renaming the file ext to .bin // we get around that by renaming the file ext to .bin
// see https://github.com/facebook/react-native/issues/27099 // see https://github.com/facebook/react-native/issues/27099
// -prf // -prf
const newPath = reqBody.replace(/\.jpe?g$/, '.bin')
await RNFS.moveFile(reqBody, newPath) // On some platforms, moving this file is not possible. We will attempt to move it (this is optimal, since
reqBody = newPath // we don't create duplicates) and if there is an error, we will instead copy the file to the cache directory
const fileName = reqBody.split('/').pop() ?? ''
const newPath = `${cacheDirectory ?? ''}${fileName.replace(
/\.jpe?g$/,
'.bin',
)}`
try {
await moveAsync({
from: reqBody,
to: newPath,
})
reqBody = newPath
} catch (e) {
await copyAsync({
from: reqBody,
to: newPath,
})
}
} }
// NOTE // NOTE
// React native treats bodies with {uri: string} as file uploads to pull from cache // React native treats bodies with {uri: string} as file uploads to pull from cache
+6 -1
View File
@@ -1,3 +1,4 @@
import {deleteAsync} from 'expo-file-system'
import { import {
AppBskyEmbedImages, AppBskyEmbedImages,
AppBskyEmbedExternal, AppBskyEmbedExternal,
@@ -39,10 +40,14 @@ export async function uploadBlob(
}) })
} else { } else {
// `blob` should be a path to a file in the local FS // `blob` should be a path to a file in the local FS
return agent.uploadBlob( const res = await agent.uploadBlob(
blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
{encoding}, {encoding},
) )
try {
deleteAsync(blob)
} catch (e) {} // Don't need to handle
return res
} }
} }
+4 -5
View File
@@ -1,5 +1,4 @@
import VersionNumber from 'react-native-version-number' import {nativeBuildVersion, nativeApplicationVersion} from 'expo-application'
import * as Updates from 'expo-updates' import {channel} from 'expo-updates'
export const updateChannel = Updates.channel export const updateChannel = channel
export const appVersion = `${nativeApplicationVersion} (${nativeBuildVersion})`
export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})`
+17 -11
View File
@@ -1,28 +1,34 @@
import {isIOS, isWeb} from 'platform/detection' import {isIOS, isWeb} from 'platform/detection'
import ReactNativeHapticFeedback, { import {
HapticFeedbackTypes, impactAsync,
} from 'react-native-haptic-feedback' ImpactFeedbackStyle,
notificationAsync,
NotificationFeedbackType,
selectionAsync,
} from 'expo-haptics'
const hapticImpact: HapticFeedbackTypes = isIOS ? 'impactMedium' : 'impactLight' // Users said the medium impact was too strong on Android; see APP-537s const hapticImpact: ImpactFeedbackStyle = isIOS
? ImpactFeedbackStyle.Medium
: ImpactFeedbackStyle.Light // Users said the medium impact was too strong on Android; see APP-537s
export class Haptics { export class Haptics {
static default() { static default() {
if (isWeb) { if (isWeb) {
return return
} }
ReactNativeHapticFeedback.trigger(hapticImpact) impactAsync(hapticImpact)
} }
static impact(type: HapticFeedbackTypes = hapticImpact) { static impact(type: ImpactFeedbackStyle = hapticImpact) {
if (isWeb) { if (isWeb) {
return return
} }
ReactNativeHapticFeedback.trigger(type) impactAsync(type)
} }
static selection() { static selection() {
if (isWeb) { if (isWeb) {
return return
} }
ReactNativeHapticFeedback.trigger('selection') selectionAsync()
} }
static notification = (type: 'success' | 'warning' | 'error') => { static notification = (type: 'success' | 'warning' | 'error') => {
if (isWeb) { if (isWeb) {
@@ -30,11 +36,11 @@ export class Haptics {
} }
switch (type) { switch (type) {
case 'success': case 'success':
return ReactNativeHapticFeedback.trigger('notificationSuccess') return notificationAsync(NotificationFeedbackType.Success)
case 'warning': case 'warning':
return ReactNativeHapticFeedback.trigger('notificationWarning') return notificationAsync(NotificationFeedbackType.Warning)
case 'error': case 'error':
return ReactNativeHapticFeedback.trigger('notificationError') return notificationAsync(NotificationFeedbackType.Error)
} }
} }
} }
+151 -79
View File
@@ -1,13 +1,20 @@
import RNFetchBlob from 'rn-fetch-blob' import {Image as RNImage} from 'react-native'
import ImageResizer from '@bam.tech/react-native-image-resizer'
import {Image as RNImage, Share as RNShare} from 'react-native'
import {Image} from 'react-native-image-crop-picker' import {Image} from 'react-native-image-crop-picker'
import * as RNFS from 'react-native-fs' import {
cacheDirectory,
copyAsync,
createDownloadResumable,
deleteAsync,
FileInfo,
getInfoAsync,
} from 'expo-file-system'
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import uuid from 'react-native-uuid' import uuid from 'react-native-uuid'
import * as Sharing from 'expo-sharing' import * as Sharing from 'expo-sharing'
import * as MediaLibrary from 'expo-media-library' import * as MediaLibrary from 'expo-media-library'
import {Dimensions} from './types' import {Dimensions} from './types'
import {isAndroid, isIOS} from 'platform/detection' import {Image as ExpoImage} from 'expo-image'
import {POST_IMG_MAX} from 'lib/constants'
export async function compressIfNeeded( export async function compressIfNeeded(
img: Image, img: Image,
@@ -53,26 +60,13 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
return return
} }
let downloadRes const path = createPath(appendExt)
try { try {
const downloadResPromise = RNFetchBlob.config({ await downloadImage(opts.uri, path, opts.timeout)
fileCache: true, return await doResize(path, opts)
appendExt,
}).fetch('GET', opts.uri)
const to1 = setTimeout(() => downloadResPromise.cancel(), opts.timeout)
downloadRes = await downloadResPromise
clearTimeout(to1)
let localUri = downloadRes.path()
if (!localUri.startsWith('file://')) {
localUri = `file://${localUri}`
}
return await doResize(localUri, opts)
} finally { } finally {
if (downloadRes) { deleteAsync(path)
downloadRes.flush()
}
} }
} }
@@ -81,47 +75,45 @@ export async function shareImageModal({uri}: {uri: string}) {
// TODO might need to give an error to the user in this case -prf // TODO might need to give an error to the user in this case -prf
return return
} }
const downloadResponse = await RNFetchBlob.config({
fileCache: true,
}).fetch('GET', uri)
// NOTE // Usually whenever we share an image it will already be available in the cache. If it isn't, then we
// assuming PNG // will download it.
// we're currently relying on the fact our CDN only serves pngs let imageUri = await ExpoImage.getCachePathAsync(uri)
// -prf if (!imageUri) {
// NOTE
let imagePath = downloadResponse.path() // assuming PNG
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true) // we're currently relying on the fact our CDN only serves pngs
// -prf
// NOTE imageUri = await downloadImage(uri, createPath('png'), 5e3)
// 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',
})
} }
RNFS.unlink(imagePath)
const imagePath = await moveToPermanentPath(imageUri, '.png')
await Sharing.shareAsync(imagePath, {
mimeType: 'image/png',
UTI: 'image/png',
})
deleteAsync(imagePath)
} }
export async function saveImageToMediaLibrary({uri}: {uri: string}) { export async function saveImageToMediaLibrary({uri}: {uri: string}) {
// download the file to cache let imageUri = await ExpoImage.getCachePathAsync(uri)
// NOTE if (!imageUri) {
// assuming PNG // download the file to cache
// we're currently relying on the fact our CDN only serves pngs // NOTE
// -prf // assuming PNG
const downloadResponse = await RNFetchBlob.config({ // we're currently relying on the fact our CDN only serves pngs
fileCache: true, // -prf
}).fetch('GET', uri) imageUri = await downloadImage(uri, createPath('png'), 5e3)
let imagePath = downloadResponse.path() }
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
const imagePath = await moveToPermanentPath(imageUri, '.png')
// save // save
await MediaLibrary.createAssetAsync(imagePath) await MediaLibrary.createAssetAsync(imagePath)
deleteAsync(imagePath)
} }
export function getImageDim(path: string): Promise<Dimensions> { export function getImageDim(path: string): Promise<Dimensions> {
@@ -147,27 +139,48 @@ interface DoResizeOpts {
} }
async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> { async function doResize(localUri: string, opts: DoResizeOpts): Promise<Image> {
// This is a bit of a hack, but it lets us get the original size of the image. The old image manipulation library
// allowed us to supply a max height/width and it would handle the resizing. With expo-image-manipulator, we have
// to supply the exact size and width that we want to resize to instead. We will calculate that ourselves based on
// the height/width results of this first manipulation
const imageRes = await manipulateAsync(localUri, [], {
format: SaveFormat.JPEG,
})
const newDimensions = getResizedDimensions({
width: imageRes.width,
height: imageRes.height,
})
for (let i = 0; i < 9; i++) { for (let i = 0; i < 9; i++) {
const quality = 100 - i * 10 const quality = 0.9 - 0.1 * i
const resizeRes = await ImageResizer.createResizedImage( const resizeRes = await manipulateAsync(
localUri, localUri,
opts.width, [{resize: {height: newDimensions.height, width: newDimensions.width}}],
opts.height, {
'JPEG', format: SaveFormat.JPEG,
quality, compress: quality,
undefined, },
undefined,
undefined,
{mode: opts.mode},
) )
if (resizeRes.size < opts.maxSize) {
// @ts-ignore This is valid, `getInfoAsync` will always return a size. The type is wonky
const info: FileInfo & {size: number} = await getInfoAsync(resizeRes.uri, {
size: true,
})
// We want to clean up every resize _except_ the final result. We'll clean that one up later when we're finished
// with it
if (info.size < opts.maxSize) {
await deleteAsync(imageRes.uri)
return { return {
path: normalizePath(resizeRes.path), path: normalizePath(resizeRes.uri),
mime: 'image/jpeg', mime: 'image/jpeg',
size: resizeRes.size, size: info.size,
width: resizeRes.width, width: resizeRes.width,
height: resizeRes.height, height: resizeRes.height,
} }
} else {
await deleteAsync(resizeRes.uri)
} }
} }
throw new Error( throw new Error(
@@ -182,12 +195,29 @@ async function moveToPermanentPath(path: string, ext = ''): Promise<string> {
https://github.com/ivpusic/react-native-image-crop-picker/issues/1199 https://github.com/ivpusic/react-native-image-crop-picker/issues/1199
*/ */
const filename = uuid.v4() const filename = uuid.v4()
const destinationPath = joinPath(cacheDirectory ?? '', `${filename}${ext}`)
await copyAsync({
from: normalizePath(path),
to: destinationPath,
})
// This is just to try and clean up whenever we can. We won't always be able to, so in cases where we can't
// we just catch the error. We can't simply move some files though such as image caches, so we will copy then
// and attempt to clean up the original file
// Paths that are image caches shouldn't be removed. com.hackemist.SDImageCache is used by SDWebImage and
// image_manager_disk_cache is used by Glide
if (
!path.includes('com.hackemist.SDImageCache') &&
!path.includes('image_manager_disk_cache')
) {
try {
deleteAsync(path)
} catch (e) {
// No need to handle
}
}
const destinationPath = joinPath(
RNFS.TemporaryDirectoryPath,
`${filename}${ext}`,
)
await RNFS.moveFile(path, destinationPath)
return normalizePath(destinationPath) return normalizePath(destinationPath)
} }
@@ -203,11 +233,53 @@ function joinPath(a: string, b: string) {
return a + '/' + b return a + '/' + b
} }
function normalizePath(str: string, allPlatforms = false): string { function normalizePath(str: string): string {
if (isAndroid || allPlatforms) { if (!str.startsWith('file://')) {
if (!str.startsWith('file://')) { return `file://${str}`
return `file://${str}`
}
} }
return str return str
} }
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 downloadResumable = createDownloadResumable(uri, path, {
cache: true,
})
const to1 = setTimeout(() => downloadResumable.cancelAsync(), timeout)
const downloadRes = await downloadResumable.downloadAsync()
clearTimeout(to1)
if (!downloadRes?.uri) {
throw new Error()
}
return normalizePath(downloadRes.uri)
}
export function getResizedDimensions(originalDims: {
width: number
height: number
}) {
if (
originalDims.width <= POST_IMG_MAX.width &&
originalDims.height <= POST_IMG_MAX.height
) {
return originalDims
}
const ratio = Math.min(
POST_IMG_MAX.width / originalDims.width,
POST_IMG_MAX.height / originalDims.height,
)
return {
width: Math.round(originalDims.width * ratio),
height: Math.round(originalDims.height * ratio),
}
}
+18 -7
View File
@@ -1,22 +1,33 @@
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import RNFS from 'react-native-fs'
import {CropperOptions} from './types' import {CropperOptions} from './types'
import {compressIfNeeded} from './manip' import {compressIfNeeded} from './manip'
import {
documentDirectory,
getInfoAsync,
readDirectoryAsync,
} from 'expo-file-system'
let _imageCounter = 0 let _imageCounter = 0
async function getFile() { async function getFile() {
let files = await RNFS.readDir( // This *should* work. In RNFS, there was a LibraryDirectoryPath constant, which is not present in
RNFS.LibraryDirectoryPath.split('/') // expo-file-system. This should work as a work around at least on simulators (probably elsewhere
// too though). Since this is only used for e2e though, this should be safe.
const libraryDirPath = documentDirectory?.split('/data/')[0] + '/data'
let paths = await readDirectoryAsync(
libraryDirPath
.split('/')
.slice(0, -5) .slice(0, -5)
.concat(['Media', 'DCIM', '100APPLE']) .concat(['Media', 'DCIM', '100APPLE'])
.join('/'), .join('/'),
) )
files = files.filter(file => file.path.endsWith('.JPG')) paths = paths.filter(path => path.endsWith('.JPG'))
const file = files[_imageCounter++ % files.length] const path = paths[_imageCounter++ % paths.length]
return await compressIfNeeded({ return await compressIfNeeded({
path: file.path, path,
mime: 'image/jpeg', mime: 'image/jpeg',
size: file.size, // @ts-ignore Size is available here, type is incorrect
size: (await getInfoAsync(path, {size: true})).size ?? 0,
width: 4288, width: 4288,
height: 2848, height: 2848,
}) })
+3 -3
View File
@@ -4,9 +4,9 @@
*/ */
import {Platform} from 'react-native' import {Platform} from 'react-native'
import app from 'react-native-version-number'
import * as info from 'expo-updates' import * as info from 'expo-updates'
import {init} from 'sentry-expo' import {init} from 'sentry-expo'
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
/** /**
* Matches the build profile `channel` props in `eas.json` * Matches the build profile `channel` props in `eas.json`
@@ -21,7 +21,7 @@ const buildChannel = (info.channel || 'development') as
* - `dev` * - `dev`
* - `1.57.0` * - `1.57.0`
*/ */
const release = app.appVersion ?? 'dev' const release = nativeApplicationVersion ?? 'dev'
/** /**
* Examples: * Examples:
@@ -33,7 +33,7 @@ const release = app.appVersion ?? 'dev'
* - `android.1.57.0.46` * - `android.1.57.0.46`
*/ */
const dist = `${Platform.OS}.${release}${ const dist = `${Platform.OS}.${release}${
app.buildVersion ? `.${app.buildVersion}` : '' nativeBuildVersion ? `.${nativeBuildVersion}` : ''
}` }`
init({ init({
+4 -4
View File
@@ -1,8 +1,8 @@
import {isIOS, isAndroid} from 'platform/detection' import {isIOS, isAndroid} from 'platform/detection'
// import * as Sharing from 'expo-sharing'
import Clipboard from '@react-native-clipboard/clipboard'
import * as Toast from '../view/com/util/Toast'
import {Share} from 'react-native' import {Share} from 'react-native'
import {setStringAsync} from 'expo-clipboard'
// import * as Sharing from 'expo-sharing'
import * as Toast from '../view/com/util/Toast'
/** /**
* This function shares a URL using the native Share API if available, or copies it to the clipboard * This function shares a URL using the native Share API if available, or copies it to the clipboard
@@ -18,7 +18,7 @@ export async function shareUrl(url: string) {
} else { } else {
// React Native Share is not supported by web. Web Share API // React Native Share is not supported by web. Web Share API
// has increasing but not full support, so default to clipboard // has increasing but not full support, so default to clipboard
Clipboard.setString(url) setStringAsync(url)
Toast.show('Copied to clipboard') Toast.show('Copied to clipboard')
} }
} }
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
+1 -1
View File
@@ -13,7 +13,7 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {RichText} from '@atproto/api' import {RichText} from '@atproto/api'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
+2 -2
View File
@@ -9,7 +9,7 @@ import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import Clipboard from '@react-native-clipboard/clipboard' import {setStringAsync} from 'expo-clipboard'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
@@ -72,7 +72,7 @@ export function Component({}: {}) {
const onCopy = React.useCallback(() => { const onCopy = React.useCallback(() => {
if (appPassword) { if (appPassword) {
Clipboard.setString(appPassword) setStringAsync(appPassword)
Toast.show(_(msg`Copied to clipboard`)) Toast.show(_(msg`Copied to clipboard`))
setWasCopied(true) setWasCopied(true)
} }
+1 -1
View File
@@ -17,7 +17,7 @@ import {MAX_ALT_TEXT} from 'lib/constants'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible' import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {ImageModel} from 'state/models/media/image' import {ImageModel} from 'state/models/media/image'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
+2 -4
View File
@@ -1,5 +1,5 @@
import React, {useState} from 'react' import React, {useState} from 'react'
import Clipboard from '@react-native-clipboard/clipboard' import {setStringAsync} from 'expo-clipboard'
import {ComAtprotoServerDescribeServer} from '@atproto/api' import {ComAtprotoServerDescribeServer} from '@atproto/api'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
import { import {
@@ -321,9 +321,7 @@ function CustomHandleForm({
// events // events
// = // =
const onPressCopy = React.useCallback(() => { const onPressCopy = React.useCallback(() => {
Clipboard.setString( setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did)
isDNSForm ? `did=${currentAccount.did}` : currentAccount.did,
)
Toast.show('Copied to clipboard') Toast.show('Copied to clipboard')
}, [currentAccount, isDNSForm]) }, [currentAccount, isDNSForm])
const onChangeHandle = React.useCallback( const onChangeHandle = React.useCallback(
@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import {LabelPreference} from '@atproto/api' import {LabelPreference} from '@atproto/api'
import {StyleSheet, Pressable, View, Linking} from 'react-native' import {StyleSheet, Pressable, View, Linking} from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {ScrollView} from './util' import {ScrollView} from './util'
import {s, colors, gradients} from 'lib/styles' import {s, colors, gradients} from 'lib/styles'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
+1 -1
View File
@@ -13,7 +13,7 @@ import {
AppBskyRichtextFacet, AppBskyRichtextFacet,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
+1 -1
View File
@@ -7,7 +7,7 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import {TextInput, ScrollView} from './util' import {TextInput, ScrollView} from './util'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {s, colors, gradients} from 'lib/styles' import {s, colors, gradients} from 'lib/styles'
+1 -1
View File
@@ -5,7 +5,7 @@ import {useWindowDimensions} from 'react-native'
import {gradients, s} from 'lib/styles' import {gradients, s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import ImageEditor, {Position} from 'react-avatar-editor' import ImageEditor, {Position} from 'react-avatar-editor'
import {TextInput} from './util' import {TextInput} from './util'
+2 -1
View File
@@ -9,7 +9,7 @@ import {
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native' } from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs} from '@atproto/api'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
@@ -125,6 +125,7 @@ export function Component({
newUserAvatar, newUserAvatar,
newUserBanner, newUserBanner,
}) })
Toast.show(_(msg`Profile updated`)) Toast.show(_(msg`Profile updated`))
onUpdate?.() onUpdate?.()
closeModal() closeModal()
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {s, colors, gradients} from 'lib/styles' import {s, colors, gradients} from 'lib/styles'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {ScrollView} from './util' import {ScrollView} from './util'
+2 -2
View File
@@ -10,7 +10,7 @@ import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import Clipboard from '@react-native-clipboard/clipboard' import {setStringAsync} from 'expo-clipboard'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button' import {Button} from '../util/forms/Button'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
@@ -148,7 +148,7 @@ function InviteCode({
const uses = invite.uses const uses = invite.uses
const onPress = React.useCallback(() => { const onPress = React.useCallback(() => {
Clipboard.setString(invite.code) setStringAsync(invite.code)
Toast.show(_(msg`Copied to clipboard`)) Toast.show(_(msg`Copied to clipboard`))
setInviteCopied(invite.code) setInviteCopied(invite.code)
}, [setInviteCopied, invite, _]) }, [setInviteCopied, invite, _])
+1 -1
View File
@@ -1,7 +1,7 @@
import React, {useRef, useEffect} from 'react' import React, {useRef, useEffect} from 'react'
import {StyleSheet} from 'react-native' import {StyleSheet} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context' import {SafeAreaView} from 'react-native-safe-area-context'
import BottomSheet from '@gorhom/bottom-sheet' import BottomSheet from '@discord/bottom-sheet/src'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
+1 -1
View File
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {s, colors, gradients} from 'lib/styles' import {s, colors, gradients} from 'lib/styles'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
+1 -1
View File
@@ -14,7 +14,7 @@ import {UserAvatar} from '../util/UserAvatar'
import {AccountDropdownBtn} from '../util/AccountDropdownBtn' import {AccountDropdownBtn} from '../util/AccountDropdownBtn'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {BottomSheetScrollView} from '@gorhom/bottom-sheet' import {BottomSheetScrollView} from '@discord/bottom-sheet/src'
import {Haptics} from 'lib/haptics' import {Haptics} from 'lib/haptics'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
+1 -1
View File
@@ -10,7 +10,7 @@ import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {s, gradients} from 'lib/styles' import {s, gradients} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@@ -2,7 +2,7 @@ import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import ImageEditor from 'react-avatar-editor' import ImageEditor from 'react-avatar-editor'
import {Slider} from '@miblanchard/react-native-slider' import {Slider} from '@miblanchard/react-native-slider'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {Dimensions} from 'lib/media/types' import {Dimensions} from 'lib/media/types'
import {getDataUriSize} from 'lib/media/util' import {getDataUriSize} from 'lib/media/util'
@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {StyleSheet, Text, View, Pressable} from 'react-native' import {StyleSheet, Text, View, Pressable} from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {s, colors, gradients} from 'lib/styles' import {s, colors, gradients} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import { import {
ActivityIndicator, ActivityIndicator,
StyleSheet, StyleSheet,
+1 -1
View File
@@ -1,4 +1,4 @@
export { export {
BottomSheetScrollView as ScrollView, BottomSheetScrollView as ScrollView,
BottomSheetTextInput as TextInput, BottomSheetTextInput as TextInput,
} from '@gorhom/bottom-sheet' } from '@discord/bottom-sheet/src'
+2
View File
@@ -112,6 +112,8 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
(scrollRef: AnimatedRef<any> | null, atIndex: number) => { (scrollRef: AnimatedRef<any> | null, atIndex: number) => {
scrollRefs.modify(refs => { scrollRefs.modify(refs => {
'worklet' 'worklet'
// TODO FABRIC
// @ts-ignore
refs[atIndex] = scrollRef refs[atIndex] = scrollRef
return refs return refs
}) })
+1 -4
View File
@@ -18,7 +18,6 @@ import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {isNative, isWeb} from 'platform/detection' import {isNative, isWeb} from 'platform/detection'
import {BlurView} from '../util/BlurView'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
@@ -649,9 +648,7 @@ let ProfileHeader = ({
accessibilityLabel={_(msg`Back`)} accessibilityLabel={_(msg`Back`)}
accessibilityHint=""> accessibilityHint="">
<View style={styles.backBtnWrapper}> <View style={styles.backBtnWrapper}>
<BlurView style={styles.backBtn} blurType="dark"> <FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
<FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
</BlurView>
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
)} )}
-30
View File
@@ -1,30 +0,0 @@
import React from 'react'
import {StyleSheet, View, ViewProps} from 'react-native'
import {addStyle} from 'lib/styles'
type BlurViewProps = ViewProps & {
blurType?: 'dark' | 'light'
blurAmount?: number
}
export const BlurView = ({
style,
blurType,
...props
}: React.PropsWithChildren<BlurViewProps>) => {
if (blurType === 'dark') {
style = addStyle(style, styles.dark)
} else {
style = addStyle(style, styles.light)
}
return <View style={style} {...props} />
}
const styles = StyleSheet.create({
dark: {
backgroundColor: '#0008',
},
light: {
backgroundColor: '#fff8',
},
})
-1
View File
@@ -1 +0,0 @@
export {BlurView} from '@react-native-community/blur'
-35
View File
@@ -1,35 +0,0 @@
import React from 'react'
import {StyleSheet, View, ViewProps} from 'react-native'
import {addStyle} from 'lib/styles'
type BlurViewProps = ViewProps & {
blurType?: 'dark' | 'light'
blurAmount?: number
}
export const BlurView = ({
style,
blurType,
blurAmount,
...props
}: React.PropsWithChildren<BlurViewProps>) => {
// @ts-ignore using an RNW-specific attribute here -prf
let blur = `blur(${blurAmount || 10}px`
// @ts-ignore using an RNW-specific attribute here -prf
style = addStyle(style, {backdropFilter: blur, WebkitBackdropFilter: blur})
if (blurType === 'dark') {
style = addStyle(style, styles.dark)
} else {
style = addStyle(style, styles.light)
}
return <View style={style} {...props} />
}
const styles = StyleSheet.create({
dark: {
backgroundColor: '#0008',
},
light: {
backgroundColor: '#fff8',
},
})
@@ -1,6 +1,6 @@
import React, {useMemo} from 'react' import React, {useMemo} from 'react'
import {TouchableWithoutFeedback} from 'react-native' import {TouchableWithoutFeedback} from 'react-native'
import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet' import {BottomSheetBackdropProps} from '@discord/bottom-sheet/src'
import Animated, { import Animated, {
Extrapolate, Extrapolate,
interpolate, interpolate,
+2
View File
@@ -104,8 +104,10 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
props.dataSet.stableGutters = '1' props.dataSet.stableGutters = '1'
} }
} }
// TODO FABRIC
return ( return (
<Animated.FlatList <Animated.FlatList
// @ts-ignore
ref={ref} ref={ref}
contentContainerStyle={[ contentContainerStyle={[
styles.contentContainer, styles.contentContainer,
+1 -1
View File
@@ -1,6 +1,6 @@
import React, {ComponentProps} from 'react' import React, {ComponentProps} from 'react'
import {StyleSheet, TouchableWithoutFeedback} from 'react-native' import {StyleSheet, TouchableWithoutFeedback} from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {gradients} from 'lib/styles' import {gradients} from 'lib/styles'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
+16 -15
View File
@@ -1,8 +1,5 @@
import React, {useState, useCallback} from 'react' import React, {useState, useCallback} from 'react'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import DateTimePicker, {
DateTimePickerEvent,
} from '@react-native-community/datetimepicker'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
@@ -14,6 +11,7 @@ import {TypographyVariant} from 'lib/ThemeContext'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {getLocales} from 'expo-localization' import {getLocales} from 'expo-localization'
import DatePicker from 'react-native-date-picker'
const LOCALE = getLocales()[0] const LOCALE = getLocales()[0]
@@ -43,11 +41,9 @@ export function DateInput(props: Props) {
}, [props.handleAsUTC]) }, [props.handleAsUTC])
const onChangeInternal = useCallback( const onChangeInternal = useCallback(
(event: DateTimePickerEvent, date: Date | undefined) => { (date: Date) => {
setShow(false) setShow(false)
if (date) { props.onChange(date)
props.onChange(date)
}
}, },
[setShow, props], [setShow, props],
) )
@@ -56,6 +52,10 @@ export function DateInput(props: Props) {
setShow(true) setShow(true)
}, [setShow]) }, [setShow])
const onCancel = useCallback(() => {
setShow(false)
}, [])
return ( return (
<View> <View>
{isAndroid && ( {isAndroid && (
@@ -80,15 +80,16 @@ export function DateInput(props: Props) {
</Button> </Button>
)} )}
{(isIOS || show) && ( {(isIOS || show) && (
<DateTimePicker <DatePicker
testID={props.testID ? `${props.testID}-datepicker` : undefined} modal={isAndroid}
open={isAndroid}
theme={theme.colorScheme}
date={props.value}
onDateChange={onChangeInternal}
onConfirm={onChangeInternal}
onCancel={onCancel}
mode="date" mode="date"
timeZoneName={props.handleAsUTC ? 'Etc/UTC' : undefined} testID={props.testID ? `${props.testID}-datepicker` : undefined}
display="spinner"
// @ts-ignore applies in iOS only -prf
themeVariant={theme.colorScheme}
value={props.value}
onChange={onChangeInternal}
accessibilityLabel={props.accessibilityLabel} accessibilityLabel={props.accessibilityLabel}
accessibilityHint={props.accessibilityHint} accessibilityHint={props.accessibilityHint}
accessibilityLabelledBy={props.accessibilityLabelledBy} accessibilityLabelledBy={props.accessibilityLabelledBy}
+2 -2
View File
@@ -1,6 +1,6 @@
import React, {memo} from 'react' import React, {memo} from 'react'
import {StyleProp, View, ViewStyle} from 'react-native' import {StyleProp, View, ViewStyle} from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard' import {setStringAsync} from 'expo-clipboard'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import { import {
@@ -139,7 +139,7 @@ let PostDropdownBtn = ({
const onCopyPostText = React.useCallback(() => { const onCopyPostText = React.useCallback(() => {
const str = richTextToString(richText, true) const str = richTextToString(richText, true)
Clipboard.setString(str) setStringAsync(str)
Toast.show(_(msg`Copied to clipboard`)) Toast.show(_(msg`Copied to clipboard`))
}, [_, richText]) }, [_, richText])
+1 -1
View File
@@ -3,7 +3,7 @@ import {Text as RNText, TextProps} from 'react-native'
import {s, lh} from 'lib/styles' import {s, lh} from 'lib/styles'
import {useTheme, TypographyVariant} from 'lib/ThemeContext' import {useTheme, TypographyVariant} from 'lib/ThemeContext'
import {isIOS} from 'platform/detection' import {isIOS} from 'platform/detection'
import {UITextView} from 'react-native-ui-text-view' import {UITextView} from 'react-native-uitextview'
export type CustomTextProps = TextProps & { export type CustomTextProps = TextProps & {
type?: TypographyVariant type?: TypographyVariant
+2 -2
View File
@@ -24,7 +24,7 @@ import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {HandIcon, HashtagIcon} from 'lib/icons' import {HandIcon, HashtagIcon} from 'lib/icons'
import Clipboard from '@react-native-clipboard/clipboard' import {setStringAsync} from 'expo-clipboard'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
@@ -235,7 +235,7 @@ export function SettingsScreen({}: Props) {
}, [onboardingDispatch, _]) }, [onboardingDispatch, _])
const onPressBuildInfo = React.useCallback(() => { const onPressBuildInfo = React.useCallback(() => {
Clipboard.setString( setStringAsync(
`Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`, `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
) )
Toast.show(_(msg`Copied build version to clipboard`)) Toast.show(_(msg`Copied build version to clipboard`))
+484 -626
View File
File diff suppressed because it is too large Load Diff