Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4137ec56b3 | |||
| 40a58be71f | |||
| 6d1c947398 | |||
| b42edb1b76 | |||
| abad9c92ae | |||
| b430ae7f72 | |||
| 84caf1e8d8 | |||
| 620f6a1bfd |
@@ -1,25 +1,28 @@
|
||||
import {
|
||||
downloadAndResize,
|
||||
DownloadAndResizeOpts,
|
||||
} from '../../src/lib/media/manip'
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
getResizedDimensions,
|
||||
} from 'lib/media/manip'
|
||||
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
import {createDownloadResumable, deleteAsync} from 'expo-file-system'
|
||||
|
||||
describe('downloadAndResize', () => {
|
||||
const errorSpy = jest.spyOn(global.console, 'error')
|
||||
|
||||
const mockResizedImage = {
|
||||
path: jest.fn().mockReturnValue('file://resized-image.jpg'),
|
||||
path: 'file://resized-image.jpg',
|
||||
size: 100,
|
||||
width: 50,
|
||||
height: 50,
|
||||
width: 100,
|
||||
height: 100,
|
||||
mime: 'image/jpeg',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const mockedCreateResizedImage =
|
||||
ImageResizer.createResizedImage as jest.Mock
|
||||
mockedCreateResizedImage.mockResolvedValue(mockResizedImage)
|
||||
const mockedCreateResizedImage = manipulateAsync as jest.Mock
|
||||
mockedCreateResizedImage.mockResolvedValue({
|
||||
uri: 'file://resized-image.jpg',
|
||||
...mockResizedImage,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -27,10 +30,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'),
|
||||
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 = {
|
||||
@@ -44,25 +49,22 @@ describe('downloadAndResize', () => {
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(result).toEqual(mockResizedImage)
|
||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
||||
fileCache: true,
|
||||
appendExt: 'jpeg',
|
||||
expect(createDownloadResumable).toHaveBeenCalledWith(
|
||||
opts.uri,
|
||||
expect.anything(),
|
||||
{
|
||||
cache: true,
|
||||
},
|
||||
)
|
||||
expect(manipulateAsync).toHaveBeenCalledWith(expect.anything(), [], {
|
||||
format: SaveFormat.JPEG,
|
||||
})
|
||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'https://example.com/image.jpg',
|
||||
)
|
||||
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
|
||||
'file://downloaded-image.jpg',
|
||||
100,
|
||||
100,
|
||||
'JPEG',
|
||||
100,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{mode: 'cover'},
|
||||
expect(manipulateAsync).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
[{resize: {height: opts.height, width: opts.width}}],
|
||||
{format: SaveFormat.JPEG, compress: 0.9},
|
||||
)
|
||||
expect(deleteAsync).toHaveBeenCalledWith(expect.anything())
|
||||
})
|
||||
|
||||
it('should return undefined for invalid URI', async () => {
|
||||
@@ -81,10 +83,12 @@ describe('downloadAndResize', () => {
|
||||
})
|
||||
|
||||
it('should return undefined for unsupported file type', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
||||
flush: jest.fn(),
|
||||
const mockedFetch = createDownloadResumable as jest.Mock
|
||||
mockedFetch.mockReturnValue({
|
||||
cancelAsync: jest.fn(),
|
||||
downloadAsync: jest
|
||||
.fn()
|
||||
.mockResolvedValue({uri: 'file://downloaded-image'}),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
@@ -98,24 +102,63 @@ describe('downloadAndResize', () => {
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(result).toEqual(mockResizedImage)
|
||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
||||
fileCache: true,
|
||||
appendExt: 'jpeg',
|
||||
expect(createDownloadResumable).toHaveBeenCalledWith(
|
||||
opts.uri,
|
||||
expect.anything(),
|
||||
{
|
||||
cache: true,
|
||||
},
|
||||
)
|
||||
expect(manipulateAsync).toHaveBeenCalledWith(expect.anything(), [], {
|
||||
format: SaveFormat.JPEG,
|
||||
})
|
||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'https://example.com/image',
|
||||
)
|
||||
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
|
||||
'file://downloaded-image',
|
||||
100,
|
||||
100,
|
||||
'JPEG',
|
||||
100,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{mode: 'cover'},
|
||||
expect(manipulateAsync).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
[{resize: {height: opts.height, width: opts.width}}],
|
||||
{format: SaveFormat.JPEG, compress: 0.9},
|
||||
)
|
||||
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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+2
-13
@@ -119,11 +119,6 @@ module.exports = function (config) {
|
||||
web: {
|
||||
favicon: './assets/favicon.png',
|
||||
},
|
||||
updates: {
|
||||
enabled: true,
|
||||
fallbackToCacheTimeout: 1000,
|
||||
url: 'https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302',
|
||||
},
|
||||
plugins: [
|
||||
'expo-localization',
|
||||
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
|
||||
@@ -132,23 +127,17 @@ module.exports = function (config) {
|
||||
{
|
||||
ios: {
|
||||
deploymentTarget: '13.4',
|
||||
newArchEnabled: false,
|
||||
newArchEnabled: true,
|
||||
},
|
||||
android: {
|
||||
compileSdkVersion: 34,
|
||||
targetSdkVersion: 34,
|
||||
buildToolsVersion: '34.0.0',
|
||||
kotlinVersion: '1.8.0',
|
||||
newArchEnabled: false,
|
||||
newArchEnabled: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'expo-updates',
|
||||
{
|
||||
username: 'blueskysocial',
|
||||
},
|
||||
],
|
||||
[
|
||||
'expo-notifications',
|
||||
{
|
||||
|
||||
+11
-6
@@ -36,14 +36,19 @@ 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', () => ({
|
||||
createDownloadResumable: jest.fn(),
|
||||
deleteAsync: jest.fn(),
|
||||
getInfoAsync: jest.fn().mockResolvedValue({
|
||||
size: 100,
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('@bam.tech/react-native-image-resizer', () => ({
|
||||
createResizedImage: jest.fn(),
|
||||
jest.mock('expo-image-manipulator', () => ({
|
||||
manipulateAsync: jest.fn().mockResolvedValue({
|
||||
uri: 'file://resized-image',
|
||||
}),
|
||||
SaveFormat: jest.requireActual('expo-image-manipulator').SaveFormat,
|
||||
}))
|
||||
|
||||
jest.mock('@segment/analytics-react-native', () => ({
|
||||
|
||||
@@ -10,15 +10,6 @@ cfg.transformer.getTransformOptions = async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: 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',
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
+30
-40
@@ -45,8 +45,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.10.5",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@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",
|
||||
"@expo/html-elements": "^0.4.2",
|
||||
"@expo/webpack-config": "^19.0.0",
|
||||
@@ -54,15 +54,11 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.0",
|
||||
"@gorhom/bottom-sheet": "^4.5.1",
|
||||
"@lingui/react": "^4.5.0",
|
||||
"@mattermost/react-native-paste-input": "^0.6.4",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@react-native-async-storage/async-storage": "1.21.0",
|
||||
"@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-async-storage/async-storage": "^1.22.3",
|
||||
"@react-native-masked-view/masked-view": "0.3.0",
|
||||
"@react-native-menu/menu": "^0.8.0",
|
||||
"@react-native-picker/picker": "2.6.1",
|
||||
@@ -100,27 +96,29 @@
|
||||
"email-validator": "^2.0.4",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "^50.0.0-preview.10",
|
||||
"expo-application": "~5.8.2",
|
||||
"expo-build-properties": "^0.11.0",
|
||||
"expo-camera": "~14.0.1",
|
||||
"expo-constants": "~15.4.3",
|
||||
"expo-dev-client": "~3.3.5",
|
||||
"expo-device": "~5.9.2",
|
||||
"expo-image": "~1.10.3",
|
||||
"expo": "^50.0.8",
|
||||
"expo-application": "~5.8.3",
|
||||
"expo-build-properties": "^0.11.1",
|
||||
"expo-camera": "~14.0.4",
|
||||
"expo-clipboard": "^5.0.1",
|
||||
"expo-constants": "~15.4.5",
|
||||
"expo-device": "~5.9.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-picker": "~14.7.1",
|
||||
"expo-linear-gradient": "^12.7.1",
|
||||
"expo-linking": "^6.2.2",
|
||||
"expo-localization": "~14.8.2",
|
||||
"expo-localization": "~14.8.3",
|
||||
"expo-media-library": "~15.9.1",
|
||||
"expo-notifications": "~0.27.3",
|
||||
"expo-notifications": "~0.27.6",
|
||||
"expo-sharing": "^11.10.0",
|
||||
"expo-splash-screen": "~0.26.2",
|
||||
"expo-splash-screen": "~0.26.4",
|
||||
"expo-status-bar": "~1.11.1",
|
||||
"expo-system-ui": "~2.9.3",
|
||||
"expo-task-manager": "~11.7.0",
|
||||
"expo-updates": "~0.24.7",
|
||||
"expo-web-browser": "~12.8.1",
|
||||
"expo-task-manager": "~11.7.2",
|
||||
"expo-web-browser": "~12.8.2",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
"js-sha256": "^0.9.0",
|
||||
@@ -135,7 +133,6 @@
|
||||
"lodash.samplesize": "^4.2.0",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.shuffle": "^4.2.0",
|
||||
"lru_map": "^0.4.1",
|
||||
"mobx": "^6.6.1",
|
||||
"mobx-react-lite": "^3.4.0",
|
||||
"mobx-utils": "^6.0.6",
|
||||
@@ -146,44 +143,36 @@
|
||||
"psl": "^1.9.0",
|
||||
"react": "18.2.0",
|
||||
"react-avatar-editor": "^13.0.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-keyed-flatten-children": "^3.0.0",
|
||||
"react-native": "0.73.2",
|
||||
"react-native-appstate-hook": "^1.0.6",
|
||||
"react-native": "~0.73.5",
|
||||
"react-native-date-picker": "^4.4.0",
|
||||
"react-native-date-picker": "^4.4.0",
|
||||
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "~2.14.0",
|
||||
"react-native-gesture-handler": "~2.15.0",
|
||||
"react-native-get-random-values": "~1.8.0",
|
||||
"react-native-haptic-feedback": "^1.14.0",
|
||||
"react-native-image-crop-picker": "^0.38.1",
|
||||
"react-native-image-crop-picker": "~0.40.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-picker-select": "^8.1.0",
|
||||
"react-native-picker-select": "~9.0.1",
|
||||
"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-safe-area-context": "4.8.2",
|
||||
"react-native-safe-area-context": "~4.9.0",
|
||||
"react-native-screens": "~3.29.0",
|
||||
"react-native-svg": "14.1.0",
|
||||
"react-native-ui-text-view": "link:./modules/react-native-ui-text-view",
|
||||
"react-native-svg": "~15.1.0",
|
||||
"react-native-uitextview": "^1.1.4",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-uuid": "^2.0.1",
|
||||
"react-native-version-number": "^0.3.6",
|
||||
"react-native-web": "~0.19.6",
|
||||
"react-native-web-linear-gradient": "^1.1.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",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"sentry-expo": "~7.0.1",
|
||||
"sentry-expo": "~7.2.0",
|
||||
"statsig-react": "^1.36.0",
|
||||
"statsig-react-native-expo": "^4.6.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
"use-deep-compare": "^1.1.0",
|
||||
"zeego": "^1.6.2",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
@@ -204,6 +193,7 @@
|
||||
"@testing-library/react-native": "^11.5.2",
|
||||
"@tsconfig/react-native": "^2.0.3",
|
||||
"@types/he": "^1.1.2",
|
||||
"@types/invariant": "^2.2.37",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/lodash.chunk": "^4.2.7",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
|
||||
@@ -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),
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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.
|
||||
+24
-25
@@ -41,7 +41,6 @@ import {
|
||||
} from 'state/session'
|
||||
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {Splash} from '#/Splash'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {msg} from '@lingui/macro'
|
||||
@@ -53,7 +52,7 @@ import {isAndroid} from 'platform/detection'
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
function InnerApp() {
|
||||
const {isInitialLoad, currentAccount} = useSession()
|
||||
const {currentAccount} = useSession()
|
||||
const {resumeSession} = useSessionApi()
|
||||
const theme = useColorModeTheme()
|
||||
const {_} = useLingui()
|
||||
@@ -74,29 +73,29 @@ function InnerApp() {
|
||||
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||
{isAndroid && <StatusBar />}
|
||||
<Alf theme={theme}>
|
||||
<Splash isReady={!isInitialLoad}>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</StatsigProvider>
|
||||
</React.Fragment>
|
||||
</Splash>
|
||||
{/*<Splash isReady={!isInitialLoad}>*/}
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</StatsigProvider>
|
||||
</React.Fragment>
|
||||
{/*</Splash>*/}
|
||||
</Alf>
|
||||
</SafeAreaProvider>
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
StyleSheet,
|
||||
StyleProp,
|
||||
} 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 {Props as SVGIconProps} from '#/components/icons/common'
|
||||
|
||||
@@ -7,7 +7,7 @@ import BottomSheet, {
|
||||
BottomSheetView,
|
||||
useBottomSheet,
|
||||
WINDOW_HEIGHT,
|
||||
} from '@gorhom/bottom-sheet'
|
||||
} from '@discord/bottom-sheet/src'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import type {AccessibilityProps} from 'react-native'
|
||||
import {BottomSheetProps} from '@gorhom/bottom-sheet'
|
||||
import {BottomSheetProps} from '@discord/bottom-sheet/src'
|
||||
|
||||
import {ViewStyleProp} from '#/alf'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
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 {isIOS} from '#/platform/detection'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 POST_TIMEOUT = 60e3 // 60s
|
||||
@@ -33,9 +33,26 @@ async function fetchHandler(
|
||||
// we get around that by renaming the file ext to .bin
|
||||
// see https://github.com/facebook/react-native/issues/27099
|
||||
// -prf
|
||||
const newPath = reqBody.replace(/\.jpe?g$/, '.bin')
|
||||
await RNFS.moveFile(reqBody, newPath)
|
||||
reqBody = newPath
|
||||
|
||||
// On some platforms, moving this file is not possible. We will attempt to move it (this is optimal, since
|
||||
// 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
|
||||
// React native treats bodies with {uri: string} as file uploads to pull from cache
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {deleteAsync} from 'expo-file-system'
|
||||
import {
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedExternal,
|
||||
@@ -39,10 +40,14 @@ export async function uploadBlob(
|
||||
})
|
||||
} else {
|
||||
// `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
|
||||
{encoding},
|
||||
)
|
||||
try {
|
||||
deleteAsync(blob)
|
||||
} catch (e) {} // Don't need to handle
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-5
@@ -1,5 +1,2 @@
|
||||
import VersionNumber from 'react-native-version-number'
|
||||
import * as Updates from 'expo-updates'
|
||||
export const updateChannel = Updates.channel
|
||||
|
||||
export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})`
|
||||
import {nativeBuildVersion, nativeApplicationVersion} from 'expo-application'
|
||||
export const appVersion = `${nativeApplicationVersion} (${nativeBuildVersion})`
|
||||
|
||||
+17
-11
@@ -1,28 +1,34 @@
|
||||
import {isIOS, isWeb} from 'platform/detection'
|
||||
import ReactNativeHapticFeedback, {
|
||||
HapticFeedbackTypes,
|
||||
} from 'react-native-haptic-feedback'
|
||||
import {
|
||||
impactAsync,
|
||||
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 {
|
||||
static default() {
|
||||
if (isWeb) {
|
||||
return
|
||||
}
|
||||
ReactNativeHapticFeedback.trigger(hapticImpact)
|
||||
impactAsync(hapticImpact)
|
||||
}
|
||||
static impact(type: HapticFeedbackTypes = hapticImpact) {
|
||||
static impact(type: ImpactFeedbackStyle = hapticImpact) {
|
||||
if (isWeb) {
|
||||
return
|
||||
}
|
||||
ReactNativeHapticFeedback.trigger(type)
|
||||
impactAsync(type)
|
||||
}
|
||||
static selection() {
|
||||
if (isWeb) {
|
||||
return
|
||||
}
|
||||
ReactNativeHapticFeedback.trigger('selection')
|
||||
selectionAsync()
|
||||
}
|
||||
static notification = (type: 'success' | 'warning' | 'error') => {
|
||||
if (isWeb) {
|
||||
@@ -30,11 +36,11 @@ export class Haptics {
|
||||
}
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return ReactNativeHapticFeedback.trigger('notificationSuccess')
|
||||
return notificationAsync(NotificationFeedbackType.Success)
|
||||
case 'warning':
|
||||
return ReactNativeHapticFeedback.trigger('notificationWarning')
|
||||
return notificationAsync(NotificationFeedbackType.Warning)
|
||||
case 'error':
|
||||
return ReactNativeHapticFeedback.trigger('notificationError')
|
||||
return notificationAsync(NotificationFeedbackType.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,69 @@
|
||||
import * as Updates from 'expo-updates'
|
||||
import {useCallback, useEffect} from 'react'
|
||||
import {AppState} from 'react-native'
|
||||
import {useEffect} from 'react'
|
||||
import {logger} from '#/logger'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {t} from '@lingui/macro'
|
||||
|
||||
export function useOTAUpdate() {
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
// HELPER FUNCTIONS
|
||||
const showUpdatePopup = useCallback(() => {
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: t`Update Available`,
|
||||
message: t`A new version of the app is available. Please update to continue using the app.`,
|
||||
onPressConfirm: async () => {
|
||||
Updates.reloadAsync().catch(err => {
|
||||
throw err
|
||||
})
|
||||
},
|
||||
})
|
||||
}, [openModal])
|
||||
const checkForUpdate = useCallback(async () => {
|
||||
logger.debug('useOTAUpdate: Checking for update...')
|
||||
try {
|
||||
// Check if new OTA update is available
|
||||
const update = await Updates.checkForUpdateAsync()
|
||||
// If updates aren't available stop the function execution
|
||||
if (!update.isAvailable) {
|
||||
return
|
||||
}
|
||||
// Otherwise fetch the update in the background, so even if the user rejects switching to latest version it will be done automatically on next relaunch.
|
||||
await Updates.fetchUpdateAsync()
|
||||
// show a popup modal
|
||||
showUpdatePopup()
|
||||
} catch (e) {
|
||||
logger.error('useOTAUpdate: Error while checking for update', {
|
||||
message: e,
|
||||
})
|
||||
}
|
||||
}, [showUpdatePopup])
|
||||
const updateEventListener = useCallback(
|
||||
(event: Updates.UpdateEvent) => {
|
||||
logger.debug('useOTAUpdate: Listening for update...')
|
||||
if (event.type === Updates.UpdateEventType.ERROR) {
|
||||
logger.error('useOTAUpdate: Error while listening for update', {
|
||||
message: event.message,
|
||||
})
|
||||
} else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) {
|
||||
// Handle no update available
|
||||
// do nothing
|
||||
} else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) {
|
||||
// Handle update available
|
||||
// open modal, ask for user confirmation, and reload the app
|
||||
showUpdatePopup()
|
||||
}
|
||||
},
|
||||
[showUpdatePopup],
|
||||
)
|
||||
// // HELPER FUNCTIONS
|
||||
// const showUpdatePopup = useCallback(() => {
|
||||
// openModal({
|
||||
// name: 'confirm',
|
||||
// title: t`Update Available`,
|
||||
// message: t`A new version of the app is available. Please update to continue using the app.`,
|
||||
// onPressConfirm: async () => {
|
||||
// // Updates.reloadAsync().catch(err => {
|
||||
// // throw err
|
||||
// // })
|
||||
// },
|
||||
// })
|
||||
// }, [openModal])
|
||||
// const checkForUpdate = useCallback(async () => {
|
||||
// logger.debug('useOTAUpdate: Checking for update...')
|
||||
// try {
|
||||
// // Check if new OTA update is available
|
||||
// // const update = await Updates.checkForUpdateAsync()
|
||||
// // If updates aren't available stop the function execution
|
||||
// // if (!update.isAvailable) {
|
||||
// // return
|
||||
// // }
|
||||
// // // Otherwise fetch the update in the background, so even if the user rejects switching to latest version it will be done automatically on next relaunch.
|
||||
// // await Updates.fetchUpdateAsync()
|
||||
// // show a popup modal
|
||||
// showUpdatePopup()
|
||||
// } catch (e) {
|
||||
// logger.error('useOTAUpdate: Error while checking for update', {
|
||||
// message: e,
|
||||
// })
|
||||
// }
|
||||
// }, [showUpdatePopup])
|
||||
// const updateEventListener = useCallback((event: Updates.UpdateEvent) => {
|
||||
logger.debug('useOTAUpdate: Listening for update...')
|
||||
// if (event.type === Updates.UpdateEventType.ERROR) {
|
||||
// logger.error('useOTAUpdate: Error while listening for update', {
|
||||
// message: event.message,
|
||||
// })
|
||||
// } else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) {
|
||||
// // Handle no update available
|
||||
// // do nothing
|
||||
// } else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) {
|
||||
// // Handle update available
|
||||
// // open modal, ask for user confirmation, and reload the app
|
||||
// showUpdatePopup()
|
||||
// }
|
||||
// }, [])
|
||||
|
||||
useEffect(() => {
|
||||
// ADD EVENT LISTENERS
|
||||
const updateEventSubscription = Updates.addListener(updateEventListener)
|
||||
const appStateSubscription = AppState.addEventListener('change', state => {
|
||||
if (state === 'active' && !__DEV__) {
|
||||
checkForUpdate()
|
||||
}
|
||||
})
|
||||
// const updateEventSubscription = Updates.addListener(updateEventListener)
|
||||
// const appStateSubscription = AppState.addEventListener('change', state => {
|
||||
// if (state === 'active' && !__DEV__) {
|
||||
// checkForUpdate()
|
||||
// }
|
||||
// })
|
||||
|
||||
// REMOVE EVENT LISTENERS (CLEANUP)
|
||||
return () => {
|
||||
updateEventSubscription.remove()
|
||||
appStateSubscription.remove()
|
||||
// updateEventSubscription.remove()
|
||||
// appStateSubscription.remove()
|
||||
}
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
// disable exhaustive deps because we don't want to run this effect again
|
||||
}
|
||||
|
||||
+151
-79
@@ -1,13 +1,20 @@
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer'
|
||||
import {Image as RNImage, Share as RNShare} from 'react-native'
|
||||
import {Image as RNImage} from 'react-native'
|
||||
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 * as Sharing from 'expo-sharing'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
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(
|
||||
img: Image,
|
||||
@@ -53,26 +60,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)
|
||||
|
||||
let localUri = downloadRes.path()
|
||||
if (!localUri.startsWith('file://')) {
|
||||
localUri = `file://${localUri}`
|
||||
}
|
||||
|
||||
return await doResize(localUri, opts)
|
||||
await downloadImage(opts.uri, path, opts.timeout)
|
||||
return await doResize(path, opts)
|
||||
} finally {
|
||||
if (downloadRes) {
|
||||
downloadRes.flush()
|
||||
}
|
||||
deleteAsync(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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',
|
||||
})
|
||||
// Usually whenever we share an image it will already be available in the cache. If it isn't, then we
|
||||
// will download it.
|
||||
let imageUri = await ExpoImage.getCachePathAsync(uri)
|
||||
if (!imageUri) {
|
||||
// NOTE
|
||||
// assuming PNG
|
||||
// we're currently relying on the fact our CDN only serves pngs
|
||||
// -prf
|
||||
imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||
}
|
||||
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}) {
|
||||
// download the file to cache
|
||||
// NOTE
|
||||
// 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)
|
||||
let imageUri = await ExpoImage.getCachePathAsync(uri)
|
||||
if (!imageUri) {
|
||||
// download the file to cache
|
||||
// NOTE
|
||||
// assuming PNG
|
||||
// we're currently relying on the fact our CDN only serves pngs
|
||||
// -prf
|
||||
imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||
}
|
||||
|
||||
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||
|
||||
// save
|
||||
await MediaLibrary.createAssetAsync(imagePath)
|
||||
|
||||
deleteAsync(imagePath)
|
||||
}
|
||||
|
||||
export function getImageDim(path: string): Promise<Dimensions> {
|
||||
@@ -147,27 +139,48 @@ interface DoResizeOpts {
|
||||
}
|
||||
|
||||
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++) {
|
||||
const quality = 100 - i * 10
|
||||
const resizeRes = await ImageResizer.createResizedImage(
|
||||
const quality = 0.9 - 0.1 * i
|
||||
const resizeRes = await manipulateAsync(
|
||||
localUri,
|
||||
opts.width,
|
||||
opts.height,
|
||||
'JPEG',
|
||||
quality,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{mode: opts.mode},
|
||||
[{resize: {height: newDimensions.height, width: newDimensions.width}}],
|
||||
{
|
||||
format: SaveFormat.JPEG,
|
||||
compress: quality,
|
||||
},
|
||||
)
|
||||
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 {
|
||||
path: normalizePath(resizeRes.path),
|
||||
path: normalizePath(resizeRes.uri),
|
||||
mime: 'image/jpeg',
|
||||
size: resizeRes.size,
|
||||
size: info.size,
|
||||
width: resizeRes.width,
|
||||
height: resizeRes.height,
|
||||
}
|
||||
} else {
|
||||
await deleteAsync(resizeRes.uri)
|
||||
}
|
||||
}
|
||||
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
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -203,11 +233,53 @@ function joinPath(a: string, b: string) {
|
||||
return a + '/' + b
|
||||
}
|
||||
|
||||
function normalizePath(str: string, allPlatforms = false): string {
|
||||
if (isAndroid || allPlatforms) {
|
||||
if (!str.startsWith('file://')) {
|
||||
return `file://${str}`
|
||||
}
|
||||
function normalizePath(str: string): string {
|
||||
if (!str.startsWith('file://')) {
|
||||
return `file://${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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import RNFS from 'react-native-fs'
|
||||
import {CropperOptions} from './types'
|
||||
import {compressIfNeeded} from './manip'
|
||||
import {
|
||||
documentDirectory,
|
||||
getInfoAsync,
|
||||
readDirectoryAsync,
|
||||
} from 'expo-file-system'
|
||||
|
||||
let _imageCounter = 0
|
||||
async function getFile() {
|
||||
let files = await RNFS.readDir(
|
||||
RNFS.LibraryDirectoryPath.split('/')
|
||||
// This *should* work. In RNFS, there was a LibraryDirectoryPath constant, which is not present in
|
||||
// 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)
|
||||
.concat(['Media', 'DCIM', '100APPLE'])
|
||||
.join('/'),
|
||||
)
|
||||
files = files.filter(file => file.path.endsWith('.JPG'))
|
||||
const file = files[_imageCounter++ % files.length]
|
||||
paths = paths.filter(path => path.endsWith('.JPG'))
|
||||
const path = paths[_imageCounter++ % paths.length]
|
||||
return await compressIfNeeded({
|
||||
path: file.path,
|
||||
path,
|
||||
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,
|
||||
height: 2848,
|
||||
})
|
||||
|
||||
+5
-8
@@ -4,24 +4,21 @@
|
||||
*/
|
||||
|
||||
import {Platform} from 'react-native'
|
||||
import app from 'react-native-version-number'
|
||||
import * as info from 'expo-updates'
|
||||
import {init} from 'sentry-expo'
|
||||
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
|
||||
|
||||
/**
|
||||
* Matches the build profile `channel` props in `eas.json`
|
||||
*/
|
||||
const buildChannel = (info.channel || 'development') as
|
||||
| 'development'
|
||||
| 'preview'
|
||||
| 'production'
|
||||
// TODO FABRIC expo updates .info
|
||||
const buildChannel = 'development' as 'development' | 'preview' | 'production'
|
||||
|
||||
/**
|
||||
* Examples:
|
||||
* - `dev`
|
||||
* - `1.57.0`
|
||||
*/
|
||||
const release = app.appVersion ?? 'dev'
|
||||
const release = nativeApplicationVersion ?? 'dev'
|
||||
|
||||
/**
|
||||
* Examples:
|
||||
@@ -33,7 +30,7 @@ const release = app.appVersion ?? 'dev'
|
||||
* - `android.1.57.0.46`
|
||||
*/
|
||||
const dist = `${Platform.OS}.${release}${
|
||||
app.buildVersion ? `.${app.buildVersion}` : ''
|
||||
nativeBuildVersion ? `.${nativeBuildVersion}` : ''
|
||||
}`
|
||||
|
||||
init({
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
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 {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
|
||||
@@ -18,7 +18,7 @@ export async function shareUrl(url: string) {
|
||||
} else {
|
||||
// React Native Share is not supported by web. Web Share API
|
||||
// has increasing but not full support, so default to clipboard
|
||||
Clipboard.setString(url)
|
||||
setStringAsync(url)
|
||||
Toast.show('Copied to clipboard')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {Image} from 'expo-image'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
View,
|
||||
} from 'react-native'
|
||||
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 {RichText} from '@atproto/api'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {setStringAsync} from 'expo-clipboard'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
@@ -72,7 +72,7 @@ export function Component({}: {}) {
|
||||
|
||||
const onCopy = React.useCallback(() => {
|
||||
if (appPassword) {
|
||||
Clipboard.setString(appPassword)
|
||||
setStringAsync(appPassword)
|
||||
Toast.show(_(msg`Copied to clipboard`))
|
||||
setWasCopied(true)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {MAX_ALT_TEXT} from 'lib/constants'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
|
||||
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 {ImageModel} from 'state/models/media/image'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {useState} from 'react'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {setStringAsync} from 'expo-clipboard'
|
||||
import {ComAtprotoServerDescribeServer} from '@atproto/api'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {
|
||||
@@ -321,9 +321,7 @@ function CustomHandleForm({
|
||||
// events
|
||||
// =
|
||||
const onPressCopy = React.useCallback(() => {
|
||||
Clipboard.setString(
|
||||
isDNSForm ? `did=${currentAccount.did}` : currentAccount.did,
|
||||
)
|
||||
setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did)
|
||||
Toast.show('Copied to clipboard')
|
||||
}, [currentAccount, isDNSForm])
|
||||
const onChangeHandle = React.useCallback(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import {LabelPreference} from '@atproto/api'
|
||||
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 {s, colors, gradients} from 'lib/styles'
|
||||
import {Text} from '../util/text/Text'
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
AppBskyRichtextFacet,
|
||||
RichText as RichTextAPI,
|
||||
} 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 {Text} from '../util/text/Text'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
View,
|
||||
} from 'react-native'
|
||||
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 {Text} from '../util/text/Text'
|
||||
import {s, colors, gradients} from 'lib/styles'
|
||||
|
||||
@@ -5,7 +5,7 @@ import {useWindowDimensions} from 'react-native'
|
||||
import {gradients, s} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
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 ImageEditor, {Position} from 'react-avatar-editor'
|
||||
import {TextInput} from './util'
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} 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 {AppBskyActorDefs} from '@atproto/api'
|
||||
import {Text} from '../util/text/Text'
|
||||
@@ -125,6 +125,7 @@ export function Component({
|
||||
newUserAvatar,
|
||||
newUserBanner,
|
||||
})
|
||||
|
||||
Toast.show(_(msg`Profile updated`))
|
||||
onUpdate?.()
|
||||
closeModal()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
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 {Text} from '../util/text/Text'
|
||||
import {ScrollView} from './util'
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {setStringAsync} from 'expo-clipboard'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {Button} from '../util/forms/Button'
|
||||
import * as Toast from '../util/Toast'
|
||||
@@ -148,7 +148,7 @@ function InviteCode({
|
||||
const uses = invite.uses
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
Clipboard.setString(invite.code)
|
||||
setStringAsync(invite.code)
|
||||
Toast.show(_(msg`Copied to clipboard`))
|
||||
setInviteCopied(invite.code)
|
||||
}, [setInviteCopied, invite, _])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {useRef, useEffect} from 'react'
|
||||
import {StyleSheet} from 'react-native'
|
||||
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 {usePalette} from 'lib/hooks/usePalette'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
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 {Text} from '../util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
|
||||
@@ -14,7 +14,7 @@ import {UserAvatar} from '../util/UserAvatar'
|
||||
import {AccountDropdownBtn} from '../util/AccountDropdownBtn'
|
||||
import {Link} from '../util/Link'
|
||||
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 {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import ImageEditor from 'react-avatar-editor'
|
||||
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 {Dimensions} from 'lib/media/types'
|
||||
import {getDataUriSize} from 'lib/media/util'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
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 {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export {
|
||||
BottomSheetScrollView as ScrollView,
|
||||
BottomSheetTextInput as TextInput,
|
||||
} from '@gorhom/bottom-sheet'
|
||||
} from '@discord/bottom-sheet/src'
|
||||
|
||||
@@ -112,6 +112,8 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
||||
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
|
||||
scrollRefs.modify(refs => {
|
||||
'worklet'
|
||||
// TODO FABRIC
|
||||
// @ts-ignore
|
||||
refs[atIndex] = scrollRef
|
||||
return refs
|
||||
})
|
||||
|
||||
@@ -18,7 +18,6 @@ import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {isNative, isWeb} from 'platform/detection'
|
||||
import {BlurView} from '../util/BlurView'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {Text} from '../util/text/Text'
|
||||
@@ -649,9 +648,7 @@ let ProfileHeader = ({
|
||||
accessibilityLabel={_(msg`Back`)}
|
||||
accessibilityHint="">
|
||||
<View style={styles.backBtnWrapper}>
|
||||
<BlurView style={styles.backBtn} blurType="dark">
|
||||
<FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
|
||||
</BlurView>
|
||||
<FontAwesomeIcon size={18} icon="angle-left" style={s.white} />
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export {BlurView} from '@react-native-community/blur'
|
||||
@@ -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 {TouchableWithoutFeedback} from 'react-native'
|
||||
import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
|
||||
import {BottomSheetBackdropProps} from '@discord/bottom-sheet/src'
|
||||
import Animated, {
|
||||
Extrapolate,
|
||||
interpolate,
|
||||
|
||||
@@ -164,7 +164,7 @@ let UserAvatar = ({
|
||||
<Image
|
||||
accessibilityIgnoresInvertColors
|
||||
testID="userAvatarImage"
|
||||
style={aviStyle}
|
||||
style={[aviStyle, {overflow: 'hidden'}]}
|
||||
resizeMode="cover"
|
||||
source={{uri: avatar}}
|
||||
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
|
||||
@@ -172,7 +172,7 @@ let UserAvatar = ({
|
||||
) : (
|
||||
<HighPriorityImage
|
||||
testID="userAvatarImage"
|
||||
style={aviStyle}
|
||||
style={[aviStyle, {overflow: 'hidden'}]}
|
||||
contentFit="cover"
|
||||
source={{uri: avatar}}
|
||||
blurRadius={moderation?.blur ? BLUR_AMOUNT : 0}
|
||||
|
||||
@@ -113,8 +113,10 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl<ItemT>(
|
||||
props.dataSet.stableGutters = '1'
|
||||
}
|
||||
}
|
||||
// TODO FABRIC
|
||||
return (
|
||||
<Animated.FlatList
|
||||
// @ts-ignore
|
||||
ref={ref}
|
||||
contentContainerStyle={[
|
||||
styles.contentContainer,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {ComponentProps} from 'react'
|
||||
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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
@@ -58,16 +58,19 @@ const styles = StyleSheet.create({
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
sizeLarge: {
|
||||
width: 70,
|
||||
height: 70,
|
||||
borderRadius: 35,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
outer: {
|
||||
// @ts-ignore web-only
|
||||
position: isWeb ? 'fixed' : 'absolute',
|
||||
zIndex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
inner: {
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
View,
|
||||
PressableProps,
|
||||
} from 'react-native'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
import {
|
||||
@@ -157,7 +156,7 @@ let PostDropdownBtn = ({
|
||||
const onCopyPostText = React.useCallback(() => {
|
||||
const str = richTextToString(richText, true)
|
||||
|
||||
Clipboard.setString(str)
|
||||
setStringAsync(str)
|
||||
Toast.show(_(msg`Copied to clipboard`))
|
||||
}, [_, richText])
|
||||
|
||||
|
||||
@@ -33,5 +33,6 @@ const styles = StyleSheet.create({
|
||||
height: 100,
|
||||
borderRadius: 4,
|
||||
marginRight: 5,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Text as RNText, TextProps} from 'react-native'
|
||||
import {s, lh} from 'lib/styles'
|
||||
import {useTheme, TypographyVariant} from 'lib/ThemeContext'
|
||||
import {isIOS} from 'platform/detection'
|
||||
import {UITextView} from 'react-native-ui-text-view'
|
||||
import {UITextView} from 'react-native-uitextview'
|
||||
|
||||
export type CustomTextProps = TextProps & {
|
||||
type?: TypographyVariant
|
||||
|
||||
@@ -24,7 +24,7 @@ import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
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 {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
@@ -235,7 +235,7 @@ export function SettingsScreen({}: Props) {
|
||||
}, [onboardingDispatch, _])
|
||||
|
||||
const onPressBuildInfo = React.useCallback(() => {
|
||||
Clipboard.setString(
|
||||
setStringAsync(
|
||||
`Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
|
||||
)
|
||||
Toast.show(_(msg`Copied build version to clipboard`))
|
||||
@@ -856,9 +856,7 @@ export function SettingsScreen({}: Props) {
|
||||
accessibilityRole="button"
|
||||
onPress={onPressBuildInfo}>
|
||||
<Text type="sm" style={[styles.buildInfo, pal.textLight]}>
|
||||
<Trans>
|
||||
Build version {AppInfo.appVersion} {AppInfo.updateChannel}
|
||||
</Trans>
|
||||
<Trans>Build version {AppInfo.appVersion}</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text type="sm" style={[pal.textLight]}>
|
||||
|
||||
Reference in New Issue
Block a user