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:
@@ -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
@@ -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', () => ({
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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.
|
|
||||||
+22
-23
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,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,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,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'
|
||||||
|
|||||||
@@ -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,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,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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,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'
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 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,
|
||||||
|
|||||||
@@ -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,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'
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`))
|
||||||
|
|||||||
Reference in New Issue
Block a user