Compare commits

...

8 Commits

Author SHA1 Message Date
Hailey 4137ec56b3 yarnlock 2024-03-10 17:49:21 -07:00
Hailey 40a58be71f Merge branch 'main' into fabric
# Conflicts:
#	app.config.js
#	package.json
#	src/App.native.tsx
#	src/view/com/modals/Waitlist.tsx
#	src/view/com/util/forms/DateInput.tsx
#	src/view/com/util/forms/PostDropdownBtn.tsx
#	yarn.lock
2024-03-10 17:44:22 -07:00
Hailey 6d1c947398 downgrade gesture handler 2024-03-05 19:53:40 -08:00
Hailey b42edb1b76 Changes to get Fabric building for production (#3119)
* rm expo-updates for now

* rm expo-updates for now

* push podfile
2024-03-05 19:41:49 -08:00
Hailey abad9c92ae fix some border radius problems on fabric (#3118)
* fix some border radi

* fix notifications image border radius

* types
2024-03-05 17:57:04 -08:00
Hailey b430ae7f72 Merge branch 'main' into fabric 2024-03-05 17:09:48 -08:00
Hailey 84caf1e8d8 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
2024-03-05 15:46:40 -08:00
Hailey 620f6a1bfd enable new arch 2024-03-05 10:10:51 -08:00
58 changed files with 964 additions and 1338 deletions
+94 -51
View File
@@ -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
View File
@@ -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
View File
@@ -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', () => ({
-9
View File
@@ -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
View File
@@ -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",
-44
View File
@@ -1,44 +0,0 @@
diff --git a/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js b/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
index 48a1409..ef185c9 100644
--- a/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
+++ b/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
@@ -70,14 +70,19 @@ function wrapModule(
importDefaultName,
importAllName,
dependencyMapName,
- globalPrefix
+ globalPrefix,
+ moduleFactoryName
) {
const params = buildParameters(
importDefaultName,
importAllName,
dependencyMapName
);
- const factory = functionFromProgram(fileAst.program, params);
+ const factory = functionFromProgram(
+ fileAst.program,
+ params,
+ moduleFactoryName
+ );
const def = t.callExpression(t.identifier(`${globalPrefix}__d`), [factory]);
const ast = t.file(t.program([t.expressionStatement(def)]));
const requireName = renameRequires(ast);
@@ -107,7 +112,16 @@ function wrapJson(source, globalPrefix) {
"});",
].join("\n");
}
-function functionFromProgram(program, parameters) {
+const JS_INVALID_IDENT_RE = /[^a-zA-Z0-9$_]/g;
+function functionFromProgram(program, parameters, moduleFactoryName) {
+ let identifier;
+ if (typeof moduleFactoryName === "string" && moduleFactoryName !== "") {
+ // Keep the name readable so it shows up in profiler traces.
+ // Add an unlikely suffix to avoid collisions with the module code.
+ identifier = t.identifier(
+ `${moduleFactoryName.replace(JS_INVALID_IDENT_RE, "_")}__module_factory__`
+ );
+ }
return t.functionExpression(
undefined,
parameters.map(makeIdentifier),
-50
View File
@@ -1,50 +0,0 @@
diff --git a/node_modules/metro-runtime/src/polyfills/require.js b/node_modules/metro-runtime/src/polyfills/require.js
index ce67cb4..eeeae84 100644
--- a/node_modules/metro-runtime/src/polyfills/require.js
+++ b/node_modules/metro-runtime/src/polyfills/require.js
@@ -22,6 +22,13 @@ global.__c = clear;
global.__registerSegment = registerSegment;
var modules = clear();
+if (__DEV__) {
+ // Added by Dan for module init logging.
+ global.__INIT_LOGS__ = []
+ var initModuleCounter = 0
+ var initModuleStack = []
+}
+
// Don't use a Symbol here, it would pull in an extra polyfill with all sorts of
// additional stuff (e.g. Array.from).
const EMPTY = {};
@@ -303,7 +310,30 @@ function loadModuleImplementation(moduleId, module) {
throw module.error;
}
if (__DEV__) {
- var Systrace = requireSystrace();
+ // Added by Dan for module init logging.
+ var Systrace = {
+ beginEvent(label) {
+ let fullLabel = initModuleCounter++ + ' ' + label
+ global.__INIT_LOGS__.push(
+ ' '.repeat(initModuleStack.length) +
+ ' ENTER ' + fullLabel
+ )
+ initModuleStack.push({
+ fullLabel,
+ startTime: nativePerformanceNow(),
+ })
+ },
+ endEvent() {
+ const res = initModuleStack.pop()
+ const fullLabel = res.fullLabel
+ const startTime = res.startTime
+ const timeElapsed = Math.round(nativePerformanceNow() - startTime)
+ global.__INIT_LOGS__.push(
+ ' '.repeat(initModuleStack.length) +
+ ' LEAVE ' + fullLabel + ' [' + timeElapsed + 'ms]',
+ )
+ }
+ };
var Refresh = requireRefresh();
}
@@ -1,41 +0,0 @@
diff --git a/node_modules/metro-transform-worker/src/index.js b/node_modules/metro-transform-worker/src/index.js
index 9f2e3d2..5222c8e 100644
--- a/node_modules/metro-transform-worker/src/index.js
+++ b/node_modules/metro-transform-worker/src/index.js
@@ -189,6 +189,10 @@ async function transformJS(file, { config, options, projectRoot }) {
let dependencyMapName = "";
let dependencies;
let wrappedAst;
+ const minify =
+ options.minify &&
+ options.unstable_transformProfile !== "hermes-canary" &&
+ options.unstable_transformProfile !== "hermes-stable";
// If the module to transform is a script (meaning that is not part of the
// dependency graph and it code will just be prepended to the bundle modules),
@@ -228,19 +232,20 @@ async function transformJS(file, { config, options, projectRoot }) {
if (config.unstable_disableModuleWrapping === true) {
wrappedAst = ast;
} else {
+ let moduleFactoryName;
+ if (options.dev && !minify) {
+ moduleFactoryName = file.filename;
+ }
({ ast: wrappedAst } = JsFileWrapping.wrapModule(
ast,
importDefault,
importAll,
dependencyMapName,
- config.globalPrefix
+ config.globalPrefix,
+ moduleFactoryName
));
}
}
- const minify =
- options.minify &&
- options.unstable_transformProfile !== "hermes-canary" &&
- options.unstable_transformProfile !== "hermes-stable";
const reserved = [];
if (config.unstable_dependencyMapReservedName != null) {
reserved.push(config.unstable_dependencyMapReservedName);
-92
View File
@@ -1,92 +0,0 @@
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
index 9dca6a5..090bda5 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
@@ -266,11 +266,10 @@ - (void)textViewDidChange:(__unused UITextView *)textView
- (void)textViewDidChangeSelection:(__unused UITextView *)textView
{
- if (_lastStringStateWasUpdatedWith && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
+ if (![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
[self textViewDidChange:_backedTextInputView];
_ignoreNextTextInputCall = YES;
}
- _lastStringStateWasUpdatedWith = _backedTextInputView.attributedText;
[self textViewProbablyDidChangeSelection];
}
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
index 1f06b79..ab458f3 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
@@ -87,7 +87,7 @@ - (void)invalidateContentSize
return;
}
- CGSize maximumSize = self.layoutMetrics.frame.size;
+ CGSize maximumSize = self.layoutMetrics.contentFrame.size;
if (_maximumNumberOfLines == 1) {
maximumSize.width = CGFLOAT_MAX;
@@ -158,6 +158,8 @@ - (void)uiManagerWillPerformMounting
[attributedText insertAttributedString:propertyAttributedText atIndex:0];
}
+ [self postprocessAttributedText:attributedText];
+
NSAttributedString *newAttributedText;
if (![_previousAttributedText isEqualToAttributedString:attributedText]) {
// We have to follow `set prop` pattern:
@@ -191,6 +193,52 @@ - (void)uiManagerWillPerformMounting
}];
}
+- (void)postprocessAttributedText:(NSMutableAttributedString *)attributedText
+{
+ __block CGFloat maximumLineHeight = 0;
+
+ [attributedText enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:NSMakeRange(0, attributedText.length)
+ options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) {
+ if (!paragraphStyle) {
+ return;
+ }
+
+ maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight);
+ }];
+
+ if (maximumLineHeight == 0) {
+ // `lineHeight` was not specified, nothing to do.
+ return;
+ }
+
+ __block CGFloat maximumFontLineHeight = 0;
+
+ [attributedText enumerateAttribute:NSFontAttributeName
+ inRange:NSMakeRange(0, attributedText.length)
+ options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
+ if (!font) {
+ return;
+ }
+
+ if (maximumFontLineHeight <= font.lineHeight) {
+ maximumFontLineHeight = font.lineHeight;
+ }
+ }];
+
+ if (maximumLineHeight < maximumFontLineHeight) {
+ return;
+ }
+
+ CGFloat baseLineOffset = maximumLineHeight / 2.0 - maximumFontLineHeight / 2.0;
+
+ [attributedText addAttribute:NSBaselineOffsetAttributeName
+ value:@(baseLineOffset)
+ range:NSMakeRange(0, attributedText.length)];
+}
+
#pragma mark -
- (NSAttributedString *)measurableAttributedText
-5
View File
@@ -1,5 +0,0 @@
# TextInput Patch
Patching `RCTBaseTextShadowInput.mm` from https://github.com/facebook/react-native/pull/38359. This fixes some text
getting cut off inside the composer. This was merged in December, so we should be able to remove this patch when RN
ships the next release.
+24 -25
View File
@@ -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>
)
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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'
+21 -4
View File
@@ -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
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
}
+57 -66
View File
@@ -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
View File
@@ -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),
}
}
+18 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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'
+1 -1
View File
@@ -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'
+2 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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'
+2 -4
View File
@@ -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'
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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'
+2 -1
View File
@@ -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 -1
View File
@@ -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'
+2 -2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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'
+1 -1
View File
@@ -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'
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
export {
BottomSheetScrollView as ScrollView,
BottomSheetTextInput as TextInput,
} from '@gorhom/bottom-sheet'
} from '@discord/bottom-sheet/src'
+2
View File
@@ -112,6 +112,8 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
scrollRefs.modify(refs => {
'worklet'
// TODO FABRIC
// @ts-ignore
refs[atIndex] = scrollRef
return refs
})
+1 -4
View File
@@ -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>
)}
-30
View File
@@ -1,30 +0,0 @@
import React from 'react'
import {StyleSheet, View, ViewProps} from 'react-native'
import {addStyle} from 'lib/styles'
type BlurViewProps = ViewProps & {
blurType?: 'dark' | 'light'
blurAmount?: number
}
export const BlurView = ({
style,
blurType,
...props
}: React.PropsWithChildren<BlurViewProps>) => {
if (blurType === 'dark') {
style = addStyle(style, styles.dark)
} else {
style = addStyle(style, styles.light)
}
return <View style={style} {...props} />
}
const styles = StyleSheet.create({
dark: {
backgroundColor: '#0008',
},
light: {
backgroundColor: '#fff8',
},
})
-1
View File
@@ -1 +0,0 @@
export {BlurView} from '@react-native-community/blur'
-35
View File
@@ -1,35 +0,0 @@
import React from 'react'
import {StyleSheet, View, ViewProps} from 'react-native'
import {addStyle} from 'lib/styles'
type BlurViewProps = ViewProps & {
blurType?: 'dark' | 'light'
blurAmount?: number
}
export const BlurView = ({
style,
blurType,
blurAmount,
...props
}: React.PropsWithChildren<BlurViewProps>) => {
// @ts-ignore using an RNW-specific attribute here -prf
let blur = `blur(${blurAmount || 10}px`
// @ts-ignore using an RNW-specific attribute here -prf
style = addStyle(style, {backdropFilter: blur, WebkitBackdropFilter: blur})
if (blurType === 'dark') {
style = addStyle(style, styles.dark)
} else {
style = addStyle(style, styles.light)
}
return <View style={style} {...props} />
}
const styles = StyleSheet.create({
dark: {
backgroundColor: '#0008',
},
light: {
backgroundColor: '#fff8',
},
})
@@ -1,6 +1,6 @@
import React, {useMemo} from 'react'
import {TouchableWithoutFeedback} from 'react-native'
import {BottomSheetBackdropProps} from '@gorhom/bottom-sheet'
import {BottomSheetBackdropProps} from '@discord/bottom-sheet/src'
import Animated, {
Extrapolate,
interpolate,
+2 -2
View File
@@ -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}
+2
View File
@@ -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,
+4 -1
View File
@@ -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',
+1 -2
View File
@@ -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',
},
})
+1 -1
View File
@@ -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
+3 -5
View File
@@ -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]}>
+477 -667
View File
File diff suppressed because it is too large Load Diff