+
diff --git a/eslint/index.js b/eslint/index.js
index 6f75f1bc34..cf5d41225d 100644
--- a/eslint/index.js
+++ b/eslint/index.js
@@ -5,6 +5,5 @@ module.exports = {
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
'use-exact-imports': require('./use-exact-imports'),
'use-typed-gates': require('./use-typed-gates'),
- 'use-prefixed-imports': require('./use-prefixed-imports'),
},
}
diff --git a/eslint/use-exact-imports.js b/eslint/use-exact-imports.js
index 26e688563e..06723043fe 100644
--- a/eslint/use-exact-imports.js
+++ b/eslint/use-exact-imports.js
@@ -1,3 +1,4 @@
+/* eslint-disable bsky-internal/use-exact-imports */
const BANNED_IMPORTS = [
'@fortawesome/free-regular-svg-icons',
'@fortawesome/free-solid-svg-icons',
@@ -5,12 +6,11 @@ const BANNED_IMPORTS = [
exports.create = function create(context) {
return {
- ImportDeclaration(node) {
- const source = node.source
- if (typeof source.value !== 'string') {
+ Literal(node) {
+ if (typeof node.value !== 'string') {
return
}
- if (BANNED_IMPORTS.includes(source.value)) {
+ if (BANNED_IMPORTS.includes(node.value)) {
context.report({
node,
message:
diff --git a/eslint/use-prefixed-imports.js b/eslint/use-prefixed-imports.js
deleted file mode 100644
index 141d536484..0000000000
--- a/eslint/use-prefixed-imports.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const BANNED_IMPORT_PREFIXES = [
- 'alf/',
- 'components/',
- 'lib/',
- 'locale/',
- 'logger/',
- 'platform/',
- 'state/',
- 'storage/',
- 'view/',
-]
-
-module.exports = {
- meta: {
- type: 'suggestion',
- fixable: 'code',
- },
- create(context) {
- return {
- ImportDeclaration(node) {
- const source = node.source
- if (typeof source.value !== 'string') {
- return
- }
- if (
- BANNED_IMPORT_PREFIXES.some(banned => source.value.startsWith(banned))
- ) {
- context.report({
- node: source,
- message: `Use '#/${source.value}'`,
- fix(fixer) {
- return fixer.replaceText(source, `'#/${source.value}'`)
- },
- })
- }
- },
- }
- },
-}
diff --git a/jest/jestSetup.js b/jest/jestSetup.js
index 50a33589ea..a68c1dc4bf 100644
--- a/jest/jestSetup.js
+++ b/jest/jestSetup.js
@@ -42,16 +42,8 @@ jest.mock('rn-fetch-blob', () => ({
fetch: jest.fn(),
}))
-jest.mock('expo-file-system', () => ({
- getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
- deleteAsync: jest.fn(),
-}))
-
-jest.mock('expo-image-manipulator', () => ({
- manipulateAsync: jest.fn().mockResolvedValue({
- uri: 'file://resized-image',
- }),
- SaveFormat: jest.requireActual('expo-image-manipulator').SaveFormat,
+jest.mock('@bam.tech/react-native-image-resizer', () => ({
+ createResizedImage: jest.fn(),
}))
jest.mock('@segment/analytics-react-native', () => ({
diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift
index 481402890f..f863eaf223 100644
--- a/modules/BlueskyNSE/NotificationService.swift
+++ b/modules/BlueskyNSE/NotificationService.swift
@@ -2,80 +2,46 @@ import UserNotifications
import UIKit
let APP_GROUP = "group.app.bsky"
-typealias ContentHandler = (UNNotificationContent) -> Void
-
-// This extension allows us to do some processing of the received notification
-// data before displaying the notification to the user. In our use case, there
-// are a few particular things that we want to do:
-//
-// - Determine whether we should play a sound for the notification
-// - Download and display any images for the notification
-// - Update the badge count accordingly
-//
-// The extension may or may not create a new process to handle a notification.
-// It is also possible that multiple notifications will be processed by the
-// same instance of `NotificationService`, though these will happen in
-// parallel.
-//
-// Because multiple instances of `NotificationService` may exist, we should
-// be careful in accessing preferences that will be mutated _by the
-// extension itself_. For example, we should not worry about `playChatSound`
-// changing, since we never mutate that value within the extension itself.
-// However, since we mutate `badgeCount` frequently, we should ensure that
-// these updates always run sync with each other and that the have access
-// to the most recent values.
class NotificationService: UNNotificationServiceExtension {
- private var contentHandler: ContentHandler?
- private var bestAttempt: UNMutableNotificationContent?
+ var prefs = UserDefaults(suiteName: APP_GROUP)
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
- self.contentHandler = contentHandler
-
- guard let bestAttempt = NSEUtil.createCopy(request.content),
+ guard let bestAttempt = createCopy(request.content),
let reason = request.content.userInfo["reason"] as? String
else {
contentHandler(request.content)
return
}
- self.bestAttempt = bestAttempt
if reason == "chat-message" {
mutateWithChatMessage(bestAttempt)
} else {
mutateWithBadge(bestAttempt)
}
- // Any image downloading (or other network tasks) should be handled at the end
- // of this block. Otherwise, if there is a timeout and serviceExtensionTimeWillExpire
- // gets called, we might not have all the needed mutations completed in time.
-
contentHandler(bestAttempt)
}
override func serviceExtensionTimeWillExpire() {
- guard let contentHandler = self.contentHandler,
- let bestAttempt = self.bestAttempt else {
- return
- }
- contentHandler(bestAttempt)
+ // If for some reason the alloted time expires, we don't actually want to display a notification
}
- // MARK: Mutations
+ func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
+ return content.mutableCopy() as? UNMutableNotificationContent
+ }
func mutateWithBadge(_ content: UNMutableNotificationContent) {
- NSEUtil.shared.prefsQueue.sync {
- var count = NSEUtil.shared.prefs?.integer(forKey: "badgeCount") ?? 0
- count += 1
+ var count = prefs?.integer(forKey: "badgeCount") ?? 0
+ count += 1
- // Set the new badge number for the notification, then store that value for using later
- content.badge = NSNumber(value: count)
- NSEUtil.shared.prefs?.setValue(count, forKey: "badgeCount")
- }
+ // Set the new badge number for the notification, then store that value for using later
+ content.badge = NSNumber(value: count)
+ prefs?.setValue(count, forKey: "badgeCount")
}
func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
- if NSEUtil.shared.prefs?.bool(forKey: "playSoundChat") == true {
+ if self.prefs?.bool(forKey: "playSoundChat") == true {
mutateWithDmSound(content)
}
}
@@ -88,18 +54,3 @@ class NotificationService: UNNotificationServiceExtension {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
}
}
-
-// NSEUtil's purpose is to create a shared instance of `UserDefaults` across
-// `NotificationService` instances. It also includes a queue so that we can process
-// updates to `UserDefaults` in parallel.
-
-private class NSEUtil {
- static let shared = NSEUtil()
-
- var prefs = UserDefaults(suiteName: APP_GROUP)
- var prefsQueue = DispatchQueue(label: "NSEPrefsQueue")
-
- static func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
- return content.mutableCopy() as? UNMutableNotificationContent
- }
-}
diff --git a/modules/Share-with-Bluesky/Info.plist b/modules/Share-with-Bluesky/Info.plist
index 43f46a5e56..421abb3c41 100644
--- a/modules/Share-with-Bluesky/Info.plist
+++ b/modules/Share-with-Bluesky/Info.plist
@@ -16,8 +16,6 @@
1
NSExtensionActivationSupportsImageWithMaxCount
10
-
NSExtensionActivationSupportsMovieWithMaxCount
-
1
NSExtensionPointIdentifier
@@ -40,4 +38,4 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
-
+
\ No newline at end of file
diff --git a/modules/Share-with-Bluesky/ShareViewController.swift b/modules/Share-with-Bluesky/ShareViewController.swift
index 63143277a5..c045d578fe 100644
--- a/modules/Share-with-Bluesky/ShareViewController.swift
+++ b/modules/Share-with-Bluesky/ShareViewController.swift
@@ -5,6 +5,7 @@ class ShareViewController: UIViewController {
// scheme.
let appScheme = Bundle.main.object(forInfoDictionaryKey: "MainAppScheme") as? String ?? "bluesky"
+ //
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
@@ -23,8 +24,6 @@ class ShareViewController: UIViewController {
await self.handleUrl(item: firstAttachment)
} else if firstAttachment.hasItemConformingToTypeIdentifier("public.image") {
await self.handleImages(items: attachments)
- } else if firstAttachment.hasItemConformingToTypeIdentifier("public.video") {
- await self.handleVideos(items: attachments)
} else {
self.completeRequest()
}
@@ -32,23 +31,31 @@ class ShareViewController: UIViewController {
}
private func handleText(item: NSItemProvider) async {
- if let data = try? await item.loadItem(forTypeIdentifier: "public.text") as? String {
- if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
- let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
- _ = self.openURL(url)
+ do {
+ if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String {
+ if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
+ let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
+ _ = self.openURL(url)
+ }
}
+ self.completeRequest()
+ } catch {
+ self.completeRequest()
}
- self.completeRequest()
}
private func handleUrl(item: NSItemProvider) async {
- if let data = try? await item.loadItem(forTypeIdentifier: "public.url") as? URL {
- if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
- let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
- _ = self.openURL(url)
+ do {
+ if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL {
+ if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
+ let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
+ _ = self.openURL(url)
+ }
}
+ self.completeRequest()
+ } catch {
+ self.completeRequest()
}
- self.completeRequest()
}
private func handleImages(items: [NSItemProvider]) async {
@@ -98,25 +105,6 @@ class ShareViewController: UIViewController {
self.completeRequest()
}
- private func handleVideos(items: [NSItemProvider]) async {
- let firstItem = items.first
-
- if let dataUri = try? await firstItem?.loadItem(forTypeIdentifier: "public.video") as? URL {
- let ext = String(dataUri.lastPathComponent.split(separator: ".").last ?? "mp4")
- if let tempUrl = getTempUrl(ext: ext) {
- let data = try? Data(contentsOf: dataUri)
- try? data?.write(to: tempUrl)
-
- if let encoded = dataUri.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
- let url = URL(string: "\(self.appScheme)://intent/compose?videoUri=\(encoded)") {
- _ = self.openURL(url)
- }
- }
- }
-
- self.completeRequest()
- }
-
private func saveImageWithInfo(_ image: UIImage?) -> String? {
guard let image = image else {
return nil
@@ -126,26 +114,27 @@ class ShareViewController: UIViewController {
// Saving this file to the bundle group's directory lets us access it from
// inside of the app. Otherwise, we wouldn't have access even though the
// extension does.
- if let tempUrl = getTempUrl(ext: "jpeg"),
- let jpegData = image.jpegData(compressionQuality: 1) {
- try jpegData.write(to: tempUrl)
- return "\(tempUrl.absoluteString)|\(image.size.width)|\(image.size.height)"
+ if let dir = FileManager()
+ .containerURL(
+ forSecurityApplicationGroupIdentifier: "group.app.bsky") {
+ let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"
+
+ if let newUri = URL(string: filePath),
+ let jpegData = image.jpegData(compressionQuality: 1) {
+ try jpegData.write(to: newUri)
+ return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)"
+ }
}
- } catch {}
- return nil
+ return nil
+ } catch {
+ return nil
+ }
}
private func completeRequest() {
self.extensionContext?.completeRequest(returningItems: nil)
}
- private func getTempUrl(ext: String) -> URL? {
- if let dir = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "group.app.bsky") {
- return URL(string: "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).\(ext)")!
- }
- return nil
- }
-
@objc func openURL(_ url: URL) -> Bool {
var responder: UIResponder? = self
while responder != nil {
diff --git a/package.json b/package.json
index 4b3486545e..ba7882902c 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"web": "expo start --web",
"use-build-number": "./scripts/useBuildNumberEnv.sh",
"use-build-number-with-bump": "./scripts/useBuildNumberEnvWithBump.sh",
- "build-web": "expo export:web && node ./scripts/post-web-build.js",
+ "build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/ && cp -v ./web-build/static/media/*.png ./bskyweb/static/media/",
"build-all": "yarn intl:build && yarn use-build-number-with-bump eas build --platform all",
"build-ios": "yarn use-build-number-with-bump eas build -p ios",
"build-android": "yarn use-build-number-with-bump eas build -p android",
@@ -49,11 +49,11 @@
"export": "npx expo export",
"make-deploy-bundle": "bash scripts/bundleUpdate.sh",
"generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 yarn build-web",
- "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web",
- "icons:optimize": "svgo -f ./assets/icons"
+ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto/api": "^0.13.7",
+ "@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1",
@@ -110,13 +110,11 @@
"await-lock": "^2.2.2",
"babel-plugin-transform-remove-console": "^6.9.4",
"base64-js": "^1.5.1",
- "bcp-47": "^2.1.0",
"bcp-47-match": "^2.0.3",
"date-fns": "^2.30.0",
"deprecated-react-native-prop-types": "^5.0.0",
"email-validator": "^2.0.4",
"emoji-mart": "^5.5.2",
- "emoji-regex": "^10.4.0",
"eventemitter3": "^5.0.1",
"expo": "^51.0.8",
"expo-application": "^5.9.1",
@@ -160,20 +158,24 @@
"lodash.set": "^4.3.2",
"lodash.shuffle": "^4.2.0",
"lodash.throttle": "^4.1.1",
+ "mobx": "^6.6.1",
+ "mobx-react-lite": "^3.4.0",
+ "mobx-utils": "^6.0.6",
"nanoid": "^5.0.5",
"normalize-url": "^8.0.0",
"patch-package": "^6.5.1",
"postinstall-postinstall": "^2.1.0",
"psl": "^1.9.0",
"react": "18.2.0",
+ "react-avatar-editor": "^13.0.0",
"react-compiler-runtime": "file:./lib/react-compiler-runtime",
"react-dom": "^18.2.0",
- "react-image-crop": "^11.0.7",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.74.1",
"react-native-compressor": "^1.8.24",
"react-native-date-picker": "^4.4.2",
"react-native-drawer-layout": "^4.0.0-alpha.3",
+ "react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.16.2",
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "0.41.2",
@@ -202,7 +204,6 @@
"statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
- "tldts": "^6.1.46",
"zeego": "^1.6.2",
"zod": "^3.20.2"
},
@@ -234,6 +235,7 @@
"@types/lodash.set": "^4.3.7",
"@types/lodash.shuffle": "^4.2.7",
"@types/psl": "^1.1.1",
+ "@types/react-avatar-editor": "^13.0.0",
"@types/react-dom": "^18.2.18",
"@types/react-responsive": "^8.0.5",
"@types/react-test-renderer": "^17.0.1",
@@ -267,7 +269,6 @@
"react-refresh": "^0.14.0",
"react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0",
- "svgo": "^3.3.2",
"ts-node": "^10.9.1",
"typescript": "^5.5.4",
"url-loader": "^4.1.1",
@@ -335,13 +336,8 @@
},
"lint-staged": {
"*{.js,.jsx,.ts,.tsx}": [
- "eslint --cache --fix"
- ],
- "*{.js,.jsx,.ts,.tsx,.css}": [
+ "eslint --cache --fix",
"prettier --cache --write --ignore-unknown"
- ],
- "assets/icons/*.svg": [
- "svgo"
]
}
}
diff --git a/patches/expo-modules-core+1.12.11.patch b/patches/expo-modules-core+1.12.11.patch
index ea26b821da..4878bb9f7e 100644
--- a/patches/expo-modules-core+1.12.11.patch
+++ b/patches/expo-modules-core+1.12.11.patch
@@ -4,23 +4,11 @@ index bb74e80..0aa0202 100644
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
@@ -90,8 +90,8 @@ public class NativeModulesProxy extends ReactContextBaseJavaModule {
mModuleRegistry.ensureIsInitialized();
-
+
KotlinInteropModuleRegistry kotlinModuleRegistry = getKotlinInteropModuleRegistry();
- kotlinModuleRegistry.emitOnCreate();
kotlinModuleRegistry.installJSIInterop();
+ kotlinModuleRegistry.emitOnCreate();
-
+
Map
constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
-diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
-index 109d3fe..c7fce9e 100644
---- a/node_modules/expo-modules-core/build/uuid/uuid.js
-+++ b/node_modules/expo-modules-core/build/uuid/uuid.js
-@@ -1,5 +1,7 @@
- import bytesToUuid from './lib/bytesToUuid';
- import { Uuidv5Namespace } from './uuid.types';
-+import { ensureNativeModulesAreInstalled } from '../ensureNativeModulesAreInstalled';
-+ensureNativeModulesAreInstalled();
- const nativeUuidv4 = globalThis?.expo?.uuidv4;
- const nativeUuidv5 = globalThis?.expo?.uuidv5;
- function uuidv4() {
diff --git a/patches/react-native+0.74.1.patch b/patches/react-native+0.74.1.patch
index aee3da1ecc..789ba84ace 100644
--- a/patches/react-native+0.74.1.patch
+++ b/patches/react-native+0.74.1.patch
@@ -1,18 +1,5 @@
-diff --git a/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm b/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm
-index caa5540..c5d4e67 100644
---- a/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm
-+++ b/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm
-@@ -73,7 +73,7 @@ @implementation RCTFileReaderModule
- } else {
- NSString *type = [RCTConvert NSString:blob[@"type"]];
- NSString *text = [NSString stringWithFormat:@"data:%@;base64,%@",
-- type != nil && [type length] > 0 ? type : @"application/octet-stream",
-+ ![type isEqual:[NSNull null]] && [type length] > 0 ? type : @"application/octet-stream",
- [data base64EncodedStringWithOptions:0]];
-
- resolve(text);
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
-index b0d71dc..41b9a0e 100644
+index b0d71dc..9974932 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
@@ -377,10 +377,6 @@ - (void)textInputDidBeginEditing
@@ -49,7 +36,7 @@ index e9b330f..1ecdf0a 100644
+
@end
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
-index b09e653..f93cb46 100644
+index b09e653..4c32b31 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
@@ -198,9 +198,53 @@ - (void)refreshControlValueChanged
diff --git a/scripts/post-web-build.js b/scripts/post-web-build.js
index 7bbee38554..baaa7cb8b7 100644
--- a/scripts/post-web-build.js
+++ b/scripts/post-web-build.js
@@ -20,30 +20,7 @@ console.log(`Writing ${templateFile}`)
const outputFile = entrypoints
.map(name => {
const file = path.basename(name)
- const ext = path.extname(file)
-
- if (ext === '.js') {
- return ``
- }
- if (ext === '.css') {
- return ``
- }
-
- return ''
+ return ``
})
.join('\n')
fs.writeFileSync(templateFile, outputFile)
-
-function copyFiles(sourceDir, targetDir) {
- const files = fs.readdirSync(path.join(projectRoot, sourceDir))
- files.forEach(file => {
- const sourcePath = path.join(projectRoot, sourceDir, file)
- const targetPath = path.join(projectRoot, targetDir, file)
- fs.copyFileSync(sourcePath, targetPath)
- console.log(`Copied ${sourcePath} to ${targetPath}`)
- })
-}
-
-copyFiles('web-build/static/js', 'bskyweb/static/js')
-copyFiles('web-build/static/css', 'bskyweb/static/css')
-copyFiles('web-build/static/media', 'bskyweb/static/media')
diff --git a/src/App.native.tsx b/src/App.native.tsx
index c6334379f7..9214253aca 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -1,6 +1,6 @@
import 'react-native-url-polyfill/auto'
-import '#/lib/sentry' // must be near top
-import '#/view/icons'
+import 'lib/sentry' // must be near top
+import 'view/icons'
import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
@@ -29,11 +29,6 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
-import {
- beginResolveGeolocation,
- ensureGeolocationResolved,
- Provider as GeolocationProvider,
-} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -60,7 +55,7 @@ import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast'
import {Shell} from '#/view/shell'
-import {ThemeProvider as Alf} from '#/alf'
+import {ThemeProvider as Alf, useFonts} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
@@ -71,11 +66,6 @@ import {BackgroundNotificationPreferencesProvider} from '../modules/expo-backgro
SplashScreen.preventAutoHideAsync()
-/**
- * Begin geolocation ASAP
- */
-beginResolveGeolocation()
-
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
@@ -116,64 +106,60 @@ function InnerApp() {
}, [_])
return (
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
)
}
function App() {
const [isReady, setReady] = useState(false)
+ const [loaded] = useFonts()
React.useEffect(() => {
- Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
- setReady(true),
- )
+ initPersistedState().then(() => setReady(true))
}, [])
- if (!isReady) {
+ if (!isReady || !loaded) {
return null
}
@@ -182,38 +168,36 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 1664812d08..1c66507336 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -1,6 +1,5 @@
-import '#/lib/sentry' // must be near top
-import '#/view/icons'
-import './style.css'
+import 'lib/sentry' // must be near top
+import 'view/icons'
import React, {useEffect, useState} from 'react'
import {KeyboardProvider} from 'react-native-keyboard-controller'
@@ -19,11 +18,6 @@ import {Provider as A11yProvider} from '#/state/a11y'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {listenSessionDropped} from '#/state/events'
-import {
- beginResolveGeolocation,
- ensureGeolocationResolved,
- Provider as GeolocationProvider,
-} from '#/state/geolocation'
import {Provider as InvitesStateProvider} from '#/state/invites'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
@@ -52,7 +46,7 @@ import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/Video
import * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index'
-import {ThemeProvider as Alf} from '#/alf'
+import {ThemeProvider as Alf, useFonts} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
@@ -60,11 +54,6 @@ import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialo
import {Provider as PortalProvider} from '#/components/Portal'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
-/**
- * Begin geolocation ASAP
- */
-beginResolveGeolocation()
-
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
@@ -107,64 +96,61 @@ function InnerApp() {
return (
-
-
-
-
-
-
+
+
+
+
+
+
-
-
- {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
)
}
function App() {
const [isReady, setReady] = useState(false)
+ const [loaded] = useFonts()
React.useEffect(() => {
- Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
- setReady(true),
- )
+ initPersistedState().then(() => setReady(true))
}, [])
- if (!isReady) {
+ if (!isReady || !loaded) {
return null
}
@@ -173,33 +159,31 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index 0c8eb330d7..9f75d305ae 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -276,13 +276,16 @@ export const atoms = {
letterSpacing: tokens.TRACKING,
},
font_normal: {
- fontWeight: tokens.fontWeight.regular,
+ fontWeight: tokens.fontWeight.normal,
},
- font_bold: {
+ font_semibold: {
fontWeight: tokens.fontWeight.semibold,
},
+ font_bold: {
+ fontWeight: tokens.fontWeight.bold,
+ },
font_heavy: {
- fontWeight: tokens.fontWeight.extrabold,
+ fontWeight: tokens.fontWeight.heavy,
},
italic: {
fontStyle: 'italic',
diff --git a/src/alf/fonts.ts b/src/alf/fonts.ts
index b11ce939f8..ce658fa05b 100644
--- a/src/alf/fonts.ts
+++ b/src/alf/fonts.ts
@@ -1,4 +1,6 @@
-import {isWeb} from '#/platform/detection'
+import {useFonts as defaultUseFonts} from 'expo-font'
+
+import {isNative, isWeb} from '#/platform/detection'
import {Device, device} from '#/storage'
const FAMILIES = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif`
@@ -32,6 +34,38 @@ export function setFontFamily(fontFamily: Device['fontFamily']) {
device.set(['fontFamily'], fontFamily)
}
+/*
+ * Unused fonts are commented out, but the files are there if we need them.
+ */
+export function useFonts() {
+ /**
+ * For native, the `expo-font` config plugin embeds the fonts in the
+ * application binary. But `expo-font` isn't supported on web, so we fall
+ * back to async loading here.
+ */
+ if (isNative) return [true, null]
+ return defaultUseFonts({
+ // 'Inter-Thin': require('../../assets/fonts/inter/Inter-Thin.otf'),
+ // 'Inter-ThinItalic': require('../../assets/fonts/inter/Inter-ThinItalic.otf'),
+ // 'Inter-ExtraLight': require('../../assets/fonts/inter/Inter-ExtraLight.otf'),
+ // 'Inter-ExtraLightItalic': require('../../assets/fonts/inter/Inter-ExtraLightItalic.otf'),
+ // 'Inter-Light': require('../../assets/fonts/inter/Inter-Light.otf'),
+ // 'Inter-LightItalic': require('../../assets/fonts/inter/Inter-LightItalic.otf'),
+ 'Inter-Regular': require('../../assets/fonts/inter/Inter-Regular.otf'),
+ 'Inter-Italic': require('../../assets/fonts/inter/Inter-Italic.otf'),
+ 'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'),
+ 'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'),
+ 'Inter-SemiBold': require('../../assets/fonts/inter/Inter-SemiBold.otf'),
+ 'Inter-SemiBoldItalic': require('../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
+ 'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'),
+ 'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'),
+ 'Inter-ExtraBold': require('../../assets/fonts/inter/Inter-ExtraBold.otf'),
+ 'Inter-ExtraBoldItalic': require('../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'),
+ 'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'),
+ 'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'),
+ })
+}
+
/*
* Unused fonts are commented out, but the files are there if we need them.
*/
@@ -74,10 +108,4 @@ export function applyFonts(
style.fontFamily = style.fontFamily || FAMILIES
}
}
-
- /**
- * Disable contextual ligatures
- * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant}
- */
- style.fontVariant = ['no-contextual']
}
diff --git a/src/alf/themes.ts b/src/alf/themes.ts
index 9f7ec5c673..f5d2247f9f 100644
--- a/src/alf/themes.ts
+++ b/src/alf/themes.ts
@@ -183,7 +183,7 @@ export function createThemes({
} as const
const darkPalette: Palette = {
- white: color.gray_25,
+ white: color.gray_0,
black: color.trueBlack,
contrast_25: color.gray_975,
diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts
index 3f30702e85..d43d2b67dd 100644
--- a/src/alf/tokens.ts
+++ b/src/alf/tokens.ts
@@ -47,16 +47,11 @@ export const borderRadius = {
full: 999,
} as const
-/**
- * These correspond to Inter font files we actually load.
- */
export const fontWeight = {
- regular: '400',
- // medium: '500',
- semibold: '600',
- // bold: '700',
- extrabold: '800',
- // black: '900',
+ normal: '400',
+ semibold: '500',
+ bold: '600',
+ heavy: '700',
} as const
export const gradients = {
diff --git a/src/components/AppLanguageDropdown.tsx b/src/components/AppLanguageDropdown.tsx
index 6170ab2e20..02cd0ce2d4 100644
--- a/src/components/AppLanguageDropdown.tsx
+++ b/src/components/AppLanguageDropdown.tsx
@@ -24,6 +24,8 @@ export function AppLanguageDropdown() {
if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
+ setLangPrefs.setPrimaryLanguage(value)
+ setLangPrefs.setContentLanguage(value)
// reset feeds to refetch content
resetPostsFeedQueries(queryClient)
diff --git a/src/components/AppLanguageDropdown.web.tsx b/src/components/AppLanguageDropdown.web.tsx
index 00a7b53011..a106d99663 100644
--- a/src/components/AppLanguageDropdown.web.tsx
+++ b/src/components/AppLanguageDropdown.web.tsx
@@ -27,6 +27,8 @@ export function AppLanguageDropdown() {
if (sanitizedLang !== value) {
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
}
+ setLangPrefs.setPrimaryLanguage(value)
+ setLangPrefs.setContentLanguage(value)
// reset feeds to refetch content
resetPostsFeedQueries(queryClient)
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 8728b88c2c..704aa9d987 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -14,7 +14,7 @@ import {
} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
-import {atoms as a, flatten, select, tokens, useTheme, web} from '#/alf'
+import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography'
@@ -30,7 +30,7 @@ export type ButtonColor =
| 'gradient_sunset'
| 'gradient_nordic'
| 'gradient_bonfire'
-export type ButtonSize = 'tiny' | 'small' | 'large'
+export type ButtonSize = 'tiny' | 'xsmall' | 'small' | 'medium' | 'large'
export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = {
/**
@@ -343,46 +343,39 @@ export const Button = React.forwardRef(
if (shape === 'default') {
if (size === 'large') {
- baseStyles.push({
- paddingVertical: 13,
- paddingHorizontal: 20,
- borderRadius: 8,
- gap: 8,
- })
+ baseStyles.push(
+ {paddingVertical: 15},
+ a.px_2xl,
+ a.rounded_sm,
+ a.gap_md,
+ )
+ } else if (size === 'medium') {
+ baseStyles.push(
+ {paddingVertical: 12},
+ a.px_2xl,
+ a.rounded_sm,
+ a.gap_md,
+ )
} else if (size === 'small') {
- baseStyles.push({
- paddingVertical: 8,
- paddingHorizontal: 12,
- borderRadius: 6,
- gap: 6,
- })
+ baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
+ } else if (size === 'xsmall') {
+ baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm)
} else if (size === 'tiny') {
- baseStyles.push({
- paddingVertical: 4,
- paddingHorizontal: 8,
- borderRadius: 4,
- gap: 4,
- })
+ baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs)
}
} else if (shape === 'round' || shape === 'square') {
if (size === 'large') {
if (shape === 'round') {
- baseStyles.push({height: 46, width: 46})
+ baseStyles.push({height: 54, width: 54})
} else {
- baseStyles.push({height: 44, width: 44})
+ baseStyles.push({height: 50, width: 50})
}
} else if (size === 'small') {
- if (shape === 'round') {
- baseStyles.push({height: 36, width: 36})
- } else {
- baseStyles.push({height: 34, width: 34})
- }
+ baseStyles.push({height: 34, width: 34})
+ } else if (size === 'xsmall') {
+ baseStyles.push({height: 28, width: 28})
} else if (size === 'tiny') {
- if (shape === 'round') {
- baseStyles.push({height: 22, width: 22})
- } else {
- baseStyles.push({height: 21, width: 21})
- }
+ baseStyles.push({height: 20, width: 20})
}
if (shape === 'round') {
@@ -626,11 +619,11 @@ export function useSharedButtonTextStyles() {
}
if (size === 'large') {
- baseStyles.push(a.text_md, a.leading_tight, web({paddingTop: 1}))
- } else if (size === 'small') {
- baseStyles.push(a.text_sm, a.leading_tight, web({paddingTop: 1}))
+ baseStyles.push(a.text_md, android({paddingBottom: 1}))
} else if (size === 'tiny') {
- baseStyles.push(a.text_xs, a.leading_tight)
+ baseStyles.push(a.text_xs, android({paddingBottom: 1}))
+ } else {
+ baseStyles.push(a.text_sm, android({paddingBottom: 1}))
}
return StyleSheet.flatten(baseStyles)
@@ -650,98 +643,31 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
export function ButtonIcon({
icon: Comp,
position,
- size,
+ size: iconSize,
}: {
icon: React.ComponentType
position?: 'left' | 'right'
size?: SVGIconProps['size']
}) {
- const {size: buttonSize, disabled} = useButtonContext()
+ const {size, disabled} = useButtonContext()
const textStyles = useSharedButtonTextStyles()
- const {iconSize, iconContainerSize} = React.useMemo(() => {
- /**
- * Pre-set icon sizes for different button sizes
- */
- const iconSizeShorthand =
- size ??
- (({
- large: 'sm',
- small: 'xs',
- tiny: 'xs',
- }[buttonSize || 'small'] || 'sm') as Exclude<
- SVGIconProps['size'],
- undefined
- >)
-
- /*
- * Copied here from icons/common.tsx so we can tweak if we need to, but
- * also so that we can calculate transforms.
- */
- const iconSize = {
- xs: 12,
- sm: 16,
- md: 20,
- lg: 24,
- xl: 28,
- '2xl': 32,
- }[iconSizeShorthand]
-
- /*
- * Goal here is to match rendered text size so that different size icons
- * don't increase button size
- */
- const iconContainerSize = {
- large: 18,
- small: 16,
- tiny: 13,
- }[buttonSize || 'small']
-
- return {
- iconSize,
- iconContainerSize,
- }
- }, [buttonSize, size])
return (
-
-
-
+
)
}
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx
index b28f66f839..e6d664cfda 100644
--- a/src/components/FeedCard.tsx
+++ b/src/components/FeedCard.tsx
@@ -11,17 +11,17 @@ import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
-import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
-import {precacheFeedFromGeneratorView} from '#/state/queries/feed'
import {
useAddSavedFeedsMutation,
usePreferencesQuery,
useRemoveFeedMutation,
} from '#/state/queries/preferences'
-import {useSession} from '#/state/session'
-import * as Toast from '#/view/com/util/Toast'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {precacheFeedFromGeneratorView} from 'state/queries/feed'
+import {useSession} from 'state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
+import * as Toast from 'view/com/util/Toast'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
@@ -121,10 +121,7 @@ export function TitleAndByline({
return (
-
+
{title}
{creator && (
diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx
index 35a346c3a5..4017a7b0be 100644
--- a/src/components/KnownFollowers.tsx
+++ b/src/components/KnownFollowers.tsx
@@ -5,7 +5,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {makeProfileLink} from '#/lib/routes/links'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Link, LinkProps} from '#/components/Link'
@@ -185,11 +185,11 @@ function KnownFollowersInner({
serverCount > 2 ? (
Followed by{' '}
-
+
{slice[0].profile.displayName}
,{' '}
-
+
{slice[1].profile.displayName}
, and{' '}
@@ -203,11 +203,11 @@ function KnownFollowersInner({
// only 2
Followed by{' '}
-
+
{slice[0].profile.displayName}
{' '}
and{' '}
-
+
{slice[1].profile.displayName}
@@ -216,7 +216,7 @@ function KnownFollowersInner({
// 1-n followers, including blocks
Followed by{' '}
-
+
{slice[0].profile.displayName}
{' '}
and{' '}
@@ -230,7 +230,7 @@ function KnownFollowersInner({
// only 1
Followed by{' '}
-
+
{slice[0].profile.displayName}
diff --git a/src/components/LabelingServiceCard/index.tsx b/src/components/LabelingServiceCard/index.tsx
index 03b8ece6b1..542f2d2993 100644
--- a/src/components/LabelingServiceCard/index.tsx
+++ b/src/components/LabelingServiceCard/index.tsx
@@ -9,7 +9,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {useLabelerInfoQuery} from '#/state/queries/labeler'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
-import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
@@ -44,45 +43,21 @@ export function Avatar({avatar}: {avatar?: string}) {
}
export function Title({value}: {value: string}) {
- return (
-
- {value}
-
- )
+ return {value}
}
export function Description({value, handle}: {value?: string; handle: string}) {
- const {_} = useLingui()
return value ? (
-
+
) : (
-
- {_(msg`By ${sanitizeHandle(handle, '@')}`)}
+
+ By {sanitizeHandle(handle, '@')}
)
}
-export function RegionalNotice() {
- const t = useTheme()
- return (
-
-
-
- Required in your region
-
-
- )
-}
-
export function LikeCount({count}: {count: number}) {
const t = useTheme()
return (
@@ -91,7 +66,7 @@ export function LikeCount({count}: {count: number}) {
a.mt_sm,
a.text_sm,
t.atoms.text_contrast_medium,
- {fontWeight: '600'},
+ {fontWeight: '500'},
]}>
@@ -110,7 +85,7 @@ export function Content({children}: React.PropsWithChildren<{}>) {
a.align_center,
a.justify_between,
]}>
- {children}
+ {children}
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index c80b9f3707..6c25faffb8 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -9,7 +9,6 @@ import {sanitizeUrl} from '@braintree/sanitize-url'
import {StackActions, useLinkProps} from '@react-navigation/native'
import {BSKY_DOWNLOAD_URL} from '#/lib/constants'
-import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
import {AllNavigatorParams} from '#/lib/routes/types'
import {shareUrl} from '#/lib/sharing'
import {
@@ -18,10 +17,11 @@ import {
isExternalUrl,
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
-import {isNative, isWeb} from '#/platform/detection'
+import {isNative} from '#/platform/detection'
import {shouldClickOpenNewTab} from '#/platform/urls'
import {useModalControls} from '#/state/modals'
import {useOpenLink} from '#/state/preferences/in-app-browser'
+import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import {useInteractionState} from '#/components/hooks/useInteractionState'
@@ -244,10 +244,7 @@ export function Link({
export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick
> &
- Pick & {
- disableUnderline?: boolean
- title?: TextProps['title']
- }
+ Pick
export function InlineLinkText({
children,
@@ -260,7 +257,6 @@ export function InlineLinkText({
selectable,
label,
shareOnLongPress,
- disableUnderline,
...rest
}: InlineLinkProps) {
const t = useTheme()
@@ -294,12 +290,11 @@ export function InlineLinkText({
{...rest}
style={[
{color: t.palette.primary_500},
- (hovered || focused || pressed) &&
- !disableUnderline && {
- ...web({outline: 0}),
- textDecorationLine: 'underline',
- textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
- },
+ (hovered || focused || pressed) && {
+ ...web({outline: 0}),
+ textDecorationLine: 'underline',
+ textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
+ },
flattenedStyle,
]}
role="link"
@@ -370,18 +365,3 @@ export function BaseLink({
)
}
-
-export function WebOnlyInlineLinkText({
- children,
- to,
- onPress,
- ...props
-}: InlineLinkProps) {
- return isWeb ? (
-
- {children}
-
- ) : (
- {children}
- )
-}
diff --git a/src/components/ListCard.tsx b/src/components/ListCard.tsx
index ed5838fb04..829f36d471 100644
--- a/src/components/ListCard.tsx
+++ b/src/components/ListCard.tsx
@@ -7,14 +7,13 @@ import {
moderateUserList,
ModerationUI,
} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {Trans} from '@lingui/macro'
import {useQueryClient} from '@tanstack/react-query'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {precacheList} from '#/state/queries/feed'
-import {useSession} from '#/state/session'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {useModerationOpts} from 'state/preferences/moderation-opts'
+import {precacheList} from 'state/queries/feed'
+import {useSession} from 'state/session'
import {atoms as a, useTheme} from '#/alf'
import {
Avatar,
@@ -112,7 +111,6 @@ export function TitleAndByline({
modUi?: ModerationUI
}) {
const t = useTheme()
- const {_} = useLingui()
const {currentAccount} = useSession()
return (
@@ -132,7 +130,6 @@ export function TitleAndByline({
{title}
@@ -142,12 +139,15 @@ export function TitleAndByline({
{creator && (
- {purpose === MODLIST
- ? _(msg`Moderation list by ${sanitizeHandle(creator.handle, '@')}`)
- : _(msg`List by ${sanitizeHandle(creator.handle, '@')}`)}
+ {purpose === MODLIST ? (
+
+ Moderation list by {sanitizeHandle(creator.handle, '@')}
+
+ ) : (
+ List by {sanitizeHandle(creator.handle, '@')}
+ )}
)}
diff --git a/src/components/MediaInsetBorder.tsx b/src/components/MediaInsetBorder.tsx
index ed89880f40..ef8b00e2e0 100644
--- a/src/components/MediaInsetBorder.tsx
+++ b/src/components/MediaInsetBorder.tsx
@@ -24,7 +24,7 @@ export function MediaInsetBorder({
return (
{children}
diff --git a/src/components/Pills.tsx b/src/components/Pills.tsx
index 974d83593f..742a11667c 100644
--- a/src/components/Pills.tsx
+++ b/src/components/Pills.tsx
@@ -130,10 +130,9 @@ export function Label({
)}
{name}
{handle}
diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
index 4cda42fdbe..3890790dbe 100644
--- a/src/components/ProfileHoverCard/index.web.tsx
+++ b/src/components/ProfileHoverCard/index.web.tsx
@@ -5,15 +5,15 @@ import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom'
import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {isTouchDevice} from '#/lib/browser'
import {getModerationCauseKey} from '#/lib/moderation'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
-import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
+import {isTouchDevice} from 'lib/browser'
+import {useProfileShadow} from 'state/cache/profile-shadow'
import {formatCount} from '#/view/com/util/numeric/format'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {ProfileHeaderHandle} from '#/screens/Profile/Header/Handle'
@@ -411,7 +411,6 @@ function Inner({
() => currentAccount?.did === profile.did,
[currentAccount, profile],
)
- const isLabeler = profile.associated?.labeler
return (
@@ -420,13 +419,11 @@ function Inner({
{!isMe &&
- !isLabeler &&
(isBlockedUser ? (
}) {
diff --git a/src/components/ProgressGuide/Task.tsx b/src/components/ProgressGuide/Task.tsx
index f2ceba52ac..a83715a425 100644
--- a/src/components/ProgressGuide/Task.tsx
+++ b/src/components/ProgressGuide/Task.tsx
@@ -35,7 +35,9 @@ export function ProgressGuideTask({
)}
- {title}
+
+ {title}
+
{subtitle && (
diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx
index 69e0082606..346312af51 100644
--- a/src/components/ProgressGuide/Toast.tsx
+++ b/src/components/ProgressGuide/Toast.tsx
@@ -154,7 +154,7 @@ export const ProgressGuideToast = React.forwardRef<
ref={animatedCheckRef}
/>
- {title}
+ {title}
{subtitle && (
{subtitle}
diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx
index 8765cdee31..7836bbef95 100644
--- a/src/components/Prompt.tsx
+++ b/src/components/Prompt.tsx
@@ -120,7 +120,7 @@ export function Cancel({
{!noDescription && record.description ? (
-
+
{record.description}
) : null}
diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
index a848cd5b92..870cbbb9fd 100644
--- a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
+++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
@@ -7,9 +7,9 @@ import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
-import {isWeb} from '#/platform/detection'
-import {useSession} from '#/state/session'
+import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
+import {isWeb} from 'platform/detection'
+import {useSession} from 'state/session'
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, native, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -125,7 +125,7 @@ export function WizardEditListDialog({
label={_(msg`Close`)}
variant="ghost"
color="primary"
- size="small"
+ size="xsmall"
onPress={() => control.close()}>
Close
diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx
index 44f01a1545..bd308fc73a 100644
--- a/src/components/StarterPack/Wizard/WizardListCard.tsx
+++ b/src/components/StarterPack/Wizard/WizardListCard.tsx
@@ -12,11 +12,11 @@ import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {useSession} from '#/state/session'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from 'lib/constants'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {useSession} from 'state/session'
+import {UserAvatar} from 'view/com/util/UserAvatar'
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -78,7 +78,6 @@ function WizardListCard({
/>
diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx
index 501e23872f..15f88468a7 100644
--- a/src/components/Typography.tsx
+++ b/src/components/Typography.tsx
@@ -1,85 +1,15 @@
import React from 'react'
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview'
-import createEmojiRegex from 'emoji-regex'
-import {logger} from '#/logger'
-import {isIOS, isNative} from '#/platform/detection'
+import {isNative} from '#/platform/detection'
import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
-import {IS_DEV} from '#/env'
-export type StringChild = string | (string | null)[]
-
-export type TextProps = Omit & {
+export type TextProps = RNTextProps & {
/**
* Lets the user select text, to use the native copy and paste functionality.
*/
selectable?: boolean
- /**
- * Provides `data-*` attributes to the underlying `UITextView` component on
- * web only.
- */
- dataSet?: Record
- /**
- * Appears as a small tooltip on web hover.
- */
- title?: string
-} & (
- | {
- emoji: true
- children: StringChild
- }
- | {
- emoji?: false
- children: RNTextProps['children']
- }
- )
-
-const EMOJI = createEmojiRegex()
-
-export function childHasEmoji(children: React.ReactNode) {
- return (Array.isArray(children) ? children : [children]).some(
- child => typeof child === 'string' && createEmojiRegex().test(child),
- )
-}
-
-export function childIsString(
- children: React.ReactNode,
-): children is StringChild {
- return (
- typeof children === 'string' ||
- (Array.isArray(children) &&
- children.every(child => typeof child === 'string' || child === null))
- )
-}
-
-export function renderChildrenWithEmoji(children: StringChild) {
- const normalized = Array.isArray(children) ? children : [children]
-
- return (
-
- {normalized.map(child => {
- if (typeof child !== 'string') return child
-
- const emojis = child.match(EMOJI)
-
- if (emojis === null) {
- return child
- }
-
- return child.split(EMOJI).map((stringPart, index) => (
-
- {stringPart}
- {emojis[index] ? (
-
- {emojis[index]}
-
- ) : null}
-
- ))
- })}
-
- )
}
/**
@@ -134,15 +64,7 @@ export function normalizeTextStyles(
/**
* Our main text component. Use this most of the time.
*/
-export function Text({
- children,
- emoji,
- style,
- selectable,
- title,
- dataSet,
- ...rest
-}: TextProps) {
+export function Text({style, selectable, ...rest}: TextProps) {
const {fonts, flags} = useAlf()
const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
@@ -151,29 +73,7 @@ export function Text({
flags,
})
- if (IS_DEV) {
- if (!emoji && childHasEmoji(children)) {
- logger.warn(
- `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add '`,
- )
- }
-
- if (emoji && !childIsString(children)) {
- logger.error('Text: when , children can only be strings.')
- }
- }
-
- return (
-
- {isIOS && emoji ? renderChildrenWithEmoji(children) : children}
-
- )
+ return
}
export function createHeadingElement({level}: {level: number}) {
diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx
index 08608f9d88..d831c6002a 100644
--- a/src/components/dialogs/BirthDateSettings.tsx
+++ b/src/components/dialogs/BirthDateSettings.tsx
@@ -117,7 +117,7 @@ function BirthdayInner({
diff --git a/src/components/dialogs/Embed.tsx b/src/components/dialogs/Embed.tsx
index ca75b01390..f43c3c6fe6 100644
--- a/src/components/dialogs/Embed.tsx
+++ b/src/components/dialogs/Embed.tsx
@@ -106,23 +106,21 @@ function EmbedDialogInner({
-
-
-
-
-
-
+
+
+
+
{
ref.current?.focus()
ref.current?.setSelection(0, snippet.length)
diff --git a/src/components/dialogs/EmbedConsent.tsx b/src/components/dialogs/EmbedConsent.tsx
index 765b8adc7e..f7e6145975 100644
--- a/src/components/dialogs/EmbedConsent.tsx
+++ b/src/components/dialogs/EmbedConsent.tsx
@@ -83,7 +83,7 @@ export function EmbedConsentDialog({
onPress={onShowAllPress}
onAccessibilityEscape={control.close}
color="primary"
- size="large"
+ size="medium"
variant="solid">
Enable external media
@@ -95,7 +95,7 @@ export function EmbedConsentDialog({
onPress={onShowPress}
onAccessibilityEscape={control.close}
color="secondary"
- size="large"
+ size="medium"
variant="solid">
Enable {externalEmbedLabels[source]} only
@@ -106,7 +106,7 @@ export function EmbedConsentDialog({
onAccessibilityEscape={control.close}
onPress={onHidePress}
color="secondary"
- size="large"
+ size="medium"
variant="ghost">
No thanks
diff --git a/src/components/dialogs/GifSelect.ios.tsx b/src/components/dialogs/GifSelect.ios.tsx
index 2f867e8657..091a23e51c 100644
--- a/src/components/dialogs/GifSelect.ios.tsx
+++ b/src/components/dialogs/GifSelect.ios.tsx
@@ -244,7 +244,7 @@ function ModalError({details, close}: {details?: string; close: () => void}) {
label={_(msg`Close dialog`)}
onPress={close}
color="primary"
- size="large"
+ size="medium"
variant="solid">
Close
diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx
index 1afc588dad..4c60c6ebeb 100644
--- a/src/components/dialogs/GifSelect.tsx
+++ b/src/components/dialogs/GifSelect.tsx
@@ -264,7 +264,7 @@ function DialogError({details}: {details?: string}) {
label={_(msg`Close dialog`)}
onPress={() => control.close()}
color="primary"
- size="large"
+ size="medium"
variant="solid">
Close
diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx
index 81a6141038..38273aad54 100644
--- a/src/components/dialogs/MutedWords.tsx
+++ b/src/components/dialogs/MutedWords.tsx
@@ -319,7 +319,7 @@ function MutedWordsInner() {
{_(msg`Save`)}
@@ -491,7 +491,9 @@ function Selectable({
},
style,
]}>
- {label}
+
+ {label}
+
{isSelected ? (
) : (
diff --git a/src/components/dialogs/nuxs/NeueTypography.tsx b/src/components/dialogs/nuxs/NeueTypography.tsx
index f160c87743..f33cea8e78 100644
--- a/src/components/dialogs/nuxs/NeueTypography.tsx
+++ b/src/components/dialogs/nuxs/NeueTypography.tsx
@@ -48,19 +48,20 @@ export function NeueTypography() {
-
- New font settings ✨
+
+ Introducing new font settings ✨
-
+
- We're introducing a new theme font, along with adjustable font
- sizing.
+ To the ensure the best possible experience, we're introducing a
+ new theme font, along with adjustable font sizing settings.
- You can adjust these in your Appearance Settings later.
+ Defaults are shown below. You can edit these in your Appearance
+ Settings later.
diff --git a/src/components/dialogs/nuxs/TenMillion/index.tsx b/src/components/dialogs/nuxs/TenMillion/index.tsx
index 21e775a108..8960824094 100644
--- a/src/components/dialogs/nuxs/TenMillion/index.tsx
+++ b/src/components/dialogs/nuxs/TenMillion/index.tsx
@@ -19,10 +19,10 @@ import {isIOS, isNative} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileQuery} from '#/state/queries/profile'
import {useAgent, useSession} from '#/state/session'
-import {useComposerControls} from '#/state/shell'
+import {useComposerControls} from 'state/shell'
import {formatCount} from '#/view/com/util/numeric/format'
-import * as Toast from '#/view/com/util/Toast'
import {Logomark} from '#/view/icons/Logomark'
+import * as Toast from 'view/com/util/Toast'
import {
atoms as a,
ThemeProvider,
@@ -441,10 +441,10 @@ export function TenMillionInner({
allowFontScaling={false}
style={[
a.absolute,
- a.font_heavy,
{
color: t.palette.primary_500,
fontSize: 32,
+ fontWeight: '900',
width: 32,
top: isNative ? -10 : 0,
left: 0,
@@ -462,11 +462,11 @@ export function TenMillionInner({
style={[
a.relative,
a.text_center,
- a.font_heavy,
{
fontStyle: 'italic',
fontSize: getFontSize(userNumber),
lineHeight: getFontSize(userNumber),
+ fontWeight: '900',
letterSpacing: -2,
},
]}>
@@ -536,7 +536,7 @@ export function TenMillionInner({
style={[
a.flex_1,
a.text_sm,
- a.font_bold,
+ a.font_semibold,
a.leading_snug,
lightTheme.atoms.text_contrast_medium,
]}>
@@ -551,7 +551,7 @@ export function TenMillionInner({
style={[
a.flex_1,
a.text_sm,
- a.font_bold,
+ a.font_semibold,
a.leading_snug,
a.text_right,
lightTheme.atoms.text_contrast_low,
@@ -643,7 +643,14 @@ export function TenMillionInner({
+ style={[
+ a.text_5xl,
+ a.leading_tight,
+ a.pb_lg,
+ {
+ fontWeight: '900',
+ },
+ ]}>
Thanks for being one of our first 10 million users.
diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx
index ab9ec16e4d..1a6bbbe601 100644
--- a/src/components/dms/MessagesListHeader.tsx
+++ b/src/components/dms/MessagesListHeader.tsx
@@ -10,14 +10,14 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
-import {BACK_HITSLOP} from '#/lib/constants'
-import {makeProfileLink} from '#/lib/routes/links'
-import {NavigationProp} from '#/lib/routes/types'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {isWeb} from '#/platform/detection'
-import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {isConvoActive, useConvo} from '#/state/messages/convo'
-import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
+import {BACK_HITSLOP} from 'lib/constants'
+import {makeProfileLink} from 'lib/routes/links'
+import {NavigationProp} from 'lib/routes/types'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {isWeb} from 'platform/detection'
+import {useProfileShadow} from 'state/cache/profile-shadow'
+import {isConvoActive, useConvo} from 'state/messages/convo'
+import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
@@ -170,7 +170,6 @@ function HeaderReady({
control.close()}>
diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx
index 1c78d2abbb..c916f4efce 100644
--- a/src/components/forms/DateField/index.tsx
+++ b/src/components/forms/DateField/index.tsx
@@ -76,7 +76,7 @@ export function DateField({
control.close()}
- size="large"
+ size="medium"
color="primary"
variant="solid">
diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx
index 94ee261e38..23229c8f44 100644
--- a/src/components/forms/TextField.tsx
+++ b/src/components/forms/TextField.tsx
@@ -9,8 +9,8 @@ import {
ViewStyle,
} from 'react-native'
-import {HITSLOP_20} from '#/lib/constants'
import {mergeRefs} from '#/lib/merge-refs'
+import {HITSLOP_20} from 'lib/constants'
import {android, atoms as a, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Props as SVGIconProps} from '#/components/icons/common'
@@ -73,7 +73,7 @@ export function Root({children, isInvalid = false}: RootProps) {
return (
inputRef.current?.focus(),
onMouseOver: onHoverIn,
diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx
index 4e3695bbf2..391b1c8b70 100644
--- a/src/components/forms/Toggle.tsx
+++ b/src/components/forms/Toggle.tsx
@@ -2,8 +2,8 @@ import React from 'react'
import {Pressable, View, ViewStyle} from 'react-native'
import Animated, {LinearTransition} from 'react-native-reanimated'
-import {HITSLOP_10} from '#/lib/constants'
import {isNative} from '#/platform/detection'
+import {HITSLOP_10} from 'lib/constants'
import {
atoms as a,
flatten,
@@ -351,8 +351,8 @@ export function Checkbox() {
t.atoms.border_contrast_high,
{
borderWidth: 1,
- height: 24,
- width: 24,
+ height: 20,
+ width: 20,
},
baseStyles,
hovered ? baseHoverStyles : {},
@@ -383,9 +383,9 @@ export function Switch() {
t.atoms.border_contrast_high,
{
borderWidth: 1,
- height: 24,
- width: 36,
- padding: 3,
+ height: 20,
+ width: 32,
+ padding: 2,
},
baseStyles,
hovered ? baseHoverStyles : {},
@@ -395,8 +395,8 @@ export function Switch() {
style={[
a.rounded_full,
{
- height: 16,
- width: 16,
+ height: 14,
+ width: 14,
},
selected
? {
@@ -436,8 +436,8 @@ export function Radio() {
t.atoms.border_contrast_high,
{
borderWidth: 1,
- height: 24,
- width: 24,
+ height: 20,
+ width: 20,
},
baseStyles,
hovered ? baseHoverStyles : {},
@@ -447,7 +447,7 @@ export function Radio() {
style={[
a.absolute,
a.rounded_full,
- {height: 16, width: 16},
+ {height: 12, width: 12},
selected
? {
backgroundColor: t.palette.primary_500,
diff --git a/src/components/icons/Accessibility.tsx b/src/components/icons/Accessibility.tsx
deleted file mode 100644
index 1e5ec0c090..0000000000
--- a/src/components/icons/Accessibility.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import {createSinglePathSVG} from './TEMPLATE'
-
-export const Accessibility_Stroke2_Corner2_Rounded = createSinglePathSVG({
- path: 'M4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm8-10C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm0 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Zm-2.86.26.014.002c.944.125 1.893.238 2.846.238.95 0 1.904-.113 2.846-.238l.014-.002h.003a1 1 0 0 1 .273 1.98l-.006.002-.017.002c-.67.089-1.341.162-2.014.21.195 1.32.65 2.33 1.626 3.357a1 1 0 0 1-1.45 1.378 8.3 8.3 0 0 1-1.234-1.647 8.2 8.2 0 0 1-1.342 1.673 1 1 0 0 1-1.398-1.43c.673-.658 1.088-1.274 1.342-1.922.163-.42.269-.878.32-1.404a33 33 0 0 1-2.075-.215l-.017-.002-.006-.001a1 1 0 0 1 .271-1.982l.004.001Z',
-})
diff --git a/src/components/icons/ArrowBoxLeft.tsx b/src/components/icons/ArrowBoxLeft.tsx
index 82e0d6e7f6..011bf6afa3 100644
--- a/src/components/icons/ArrowBoxLeft.tsx
+++ b/src/components/icons/ArrowBoxLeft.tsx
@@ -3,7 +3,3 @@ import {createSinglePathSVG} from './TEMPLATE'
export const ArrowBoxLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3.293 3.293A1 1 0 0 1 4 3h7.25a1 1 0 1 1 0 2H5v14h6.25a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1V4a1 1 0 0 1 .293-.707Zm11.5 3.5a1 1 0 0 1 1.414 0l4.5 4.5a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414-1.414L17.586 13H8.75a1 1 0 1 1 0-2h8.836l-2.793-2.793a1 1 0 0 1 0-1.414Z',
})
-
-export const ArrowBoxLeft_Stroke2_Corner2_Rounded = createSinglePathSVG({
- path: 'M6 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h5.25a1 1 0 1 1 0 2H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h5.25a1 1 0 1 1 0 2H6Zm8.793 1.793a1 1 0 0 1 1.414 0l4.5 4.5a1 1 0 0 1 0 1.414l-4.5 4.5a1 1 0 0 1-1.414-1.414L17.586 13H8.75a1 1 0 1 1 0-2h8.836l-2.793-2.793a1 1 0 0 1 0-1.414Z',
-})
diff --git a/src/components/icons/AspectRatio.tsx b/src/components/icons/AspectRatio.tsx
deleted file mode 100644
index b59c1680ef..0000000000
--- a/src/components/icons/AspectRatio.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import {createSinglePathSVG} from './TEMPLATE'
-
-export const AspectRatio11_Stroke2_Corner0_Rounded = createSinglePathSVG({
- path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v14h14V5H5Z',
-})
-
-export const AspectRatio43_Stroke2_Corner0_Rounded = createSinglePathSVG({
- path: 'M2 20.5c-.552 0-1-.41-1-.917V4.917C1 4.41 1.448 4 2 4h20c.552 0 1 .41 1 .917v14.666c0 .507-.448.917-1 .917H2Zm1-1.833h18V5.833H3v12.834Z',
-})
-
-export const AspectRatio34_Stroke2_Corner0_Rounded = createSinglePathSVG({
- path: 'M4 2c0-.552.41-1 .917-1h14.666c.507 0 .917.448.917 1v20c0 .552-.41 1-.917 1H4.917C4.41 23 4 22.552 4 22V2Zm1.833 1v18h12.834V3H5.833Z',
-})
diff --git a/src/components/icons/At.tsx b/src/components/icons/At.tsx
index ef0d1003f1..2487250545 100644
--- a/src/components/icons/At.tsx
+++ b/src/components/icons/At.tsx
@@ -1,9 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const At_Stroke2_Corner0_Rounded = createSinglePathSVG({
- path: 'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.96 9.96 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.2 4.2 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458s2.514-4.586 5.044-4.23c.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.22 2.22 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529s.784 3.03 1.98 3.198 2.543-.819 2.784-2.529-.784-3.03-1.98-3.198Z',
-})
-
-export const At_Stroke2_Corner2_Rounded = createSinglePathSVG({
- path: 'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.96 9.96 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.2 4.2 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458s2.514-4.586 5.044-4.23c.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.22 2.22 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529s.784 3.03 1.98 3.198 2.544-.819 2.784-2.529-.784-3.03-1.98-3.198Z',
+ path: 'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z',
})
diff --git a/src/components/icons/BirthdayCake.tsx b/src/components/icons/BirthdayCake.tsx
deleted file mode 100644
index 8e41cbac11..0000000000
--- a/src/components/icons/BirthdayCake.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import {createSinglePathSVG} from './TEMPLATE'
-
-export const BirthdayCake_Stroke2_Corner2_Rounded = createSinglePathSVG({
- path: 'm12 .757 2.122 2.122A3 3 0 0 1 13 7.829V9h4.5a3 3 0 0 1 3 3v1.646c0 .603-.18 1.177-.5 1.658V19a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3v-3.696a3 3 0 0 1-.5-1.658V12a3 3 0 0 1 3-3H11V7.829a3 3 0 0 1-1.121-4.95L12 .757ZM6.5 11a1 1 0 0 0-1 1v1.646a1 1 0 0 0 .629.928l.5.2a1 1 0 0 0 .742 0l1.015-.405a3 3 0 0 1 2.228 0l1.015.405a1 1 0 0 0 .742 0l1.015-.405a3 3 0 0 1 2.228 0l1.015.405a1 1 0 0 0 .742 0l.5-.2a1 1 0 0 0 .629-.928V12a1 1 0 0 0-1-1h-11ZM6 16.674V19a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-2.326a3 3 0 0 1-2.114-.043l-1.015-.405a1 1 0 0 0-.742 0l-1.015.405a3 3 0 0 1-2.228 0l-1.015-.405a1 1 0 0 0-.742 0l-1.015.405A3 3 0 0 1 6 16.674ZM12.002 6a1 1 0 0 0 .706-1.707L12 3.586l-.707.707A1 1 0 0 0 12.002 6Z',
-})
diff --git a/src/components/icons/BubbleInfo.tsx b/src/components/icons/BubbleInfo.tsx
deleted file mode 100644
index 2865713743..0000000000
--- a/src/components/icons/BubbleInfo.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import {createSinglePathSVG} from './TEMPLATE'
-
-export const BubbleInfo_Stroke2_Corner2_Rounded = createSinglePathSVG({
- path: 'M6.002 5h12a1 1 0 0 1 1 1v10.036a1 1 0 0 1-1 1h-2.626a2 2 0 0 0-1.276.46l-2.098 1.738-2.065-1.731a2 2 0 0 0-1.285-.467h-2.65a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm12-2h-12a3 3 0 0 0-3 3v10.036a3 3 0 0 0 3 3h2.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h2.626a3 3 0 0 0 3-3V6a3 3 0 0 0-3-3ZM13 11.75a1 1 0 1 0-2 0v2a1 1 0 1 0 2 0v-2ZM12 10a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z',
-})
diff --git a/src/components/icons/CircleQuestion.tsx b/src/components/icons/CircleQuestion.tsx
deleted file mode 100644
index 4eb369379b..0000000000
--- a/src/components/icons/CircleQuestion.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import {createSinglePathSVG} from './TEMPLATE'
-
-export const CircleQuestion_Stroke2_Corner2_Rounded = createSinglePathSVG({
- path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Z" clip-rule="evenodd"/>
Close
@@ -124,7 +124,7 @@ function Inner({control}: {control: DialogControlProps}) {
onPress={onPressResendEmail}
variant="solid"
color="primary"
- size="large"
+ size="medium"
disabled={sending}>
Resend Email
diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx
index bf9bae5171..f2d13f6424 100644
--- a/src/components/moderation/ContentHider.tsx
+++ b/src/components/moderation/ContentHider.tsx
@@ -94,7 +94,7 @@ export function ContentHider({
a.text_left,
a.font_bold,
a.leading_snug,
- gtMobile && [a.font_bold],
+ gtMobile && [a.font_semibold],
t.atoms.text_contrast_medium,
web({
marginBottom: 1,
@@ -107,7 +107,7 @@ export function ContentHider({
style={[
a.font_bold,
a.leading_snug,
- gtMobile && [a.font_bold],
+ gtMobile && [a.font_semibold],
t.atoms.text_contrast_high,
web({
marginBottom: 1,
diff --git a/src/components/moderation/LabelPreference.tsx b/src/components/moderation/LabelPreference.tsx
index d6dc45d1a5..78b50ff8b9 100644
--- a/src/components/moderation/LabelPreference.tsx
+++ b/src/components/moderation/LabelPreference.tsx
@@ -236,7 +236,8 @@ export function LabelerLabelPreference({
-
+
{adultDisabled ? (
Adult content is disabled.
) : isGlobalLabel ? (
diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx
index e63cea93b2..fe6932290d 100644
--- a/src/components/moderation/LabelsOnMeDialog.tsx
+++ b/src/components/moderation/LabelsOnMeDialog.tsx
@@ -132,10 +132,8 @@ function Label({
]}>
-
- {strings.name}
-
-
+ {strings.name}
+
{strings.description}
@@ -281,7 +279,7 @@ function AppealForm({
testID="backBtn"
variant="solid"
color="secondary"
- size="large"
+ size="medium"
onPress={onPressBack}
label={_(msg`Back`)}>
{_(msg`Back`)}
@@ -290,7 +288,7 @@ function AppealForm({
testID="submitBtn"
variant="solid"
color="primary"
- size="large"
+ size="medium"
onPress={onSubmit}
label={_(msg`Submit`)}>
{_(msg`Submit`)}
diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx
index 2259178538..d95717cf43 100644
--- a/src/components/moderation/ModerationDetailsDialog.tsx
+++ b/src/components/moderation/ModerationDetailsDialog.tsx
@@ -118,11 +118,7 @@ function ModerationDetailsDialogInner({
: _(msg`The author of this thread has hidden this reply.`)
} else if (modcause.type === 'label') {
name = desc.name
- description = (
-
- {desc.description}
-
- )
+ description = desc.description
} else {
// should never happen
name = ''
@@ -131,7 +127,7 @@ function ModerationDetailsDialogInner({
return (
-
+
{name}
diff --git a/src/components/moderation/ScreenHider.tsx b/src/components/moderation/ScreenHider.tsx
index 5680b60c2d..f855d63331 100644
--- a/src/components/moderation/ScreenHider.tsx
+++ b/src/components/moderation/ScreenHider.tsx
@@ -10,9 +10,9 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
-import {NavigationProp} from '#/lib/routes/types'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {NavigationProp} from 'lib/routes/types'
import {CenteredView} from '#/view/com/util/Views'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -86,7 +86,13 @@ export function ScreenHider({
+ style={[
+ a.text_4xl,
+ a.font_semibold,
+ a.text_center,
+ a.mb_md,
+ t.atoms.text,
+ ]}>
{isNoPwi ? (
Sign-in Required
) : (
@@ -112,7 +118,7 @@ export function ScreenHider({
(
// temporary file).
const newPath = uri.replace(/\.jpe?g$/, '.bin')
try {
- await copyAsync({from: uri, to: newPath})
+ await RNFS.copyFile(uri, newPath)
} catch {
// Failed to copy the file, just use the original
return await fn(uri)
@@ -76,7 +74,7 @@ async function withSafeFile(
return await fn(newPath)
} finally {
// Remove the temporary file
- await safeDeleteAsync(newPath)
+ await RNFS.unlink(newPath)
}
} else {
return fn(uri)
diff --git a/src/lib/embeds.ts b/src/lib/embeds.ts
index 2904f1cc36..a758987b20 100644
--- a/src/lib/embeds.ts
+++ b/src/lib/embeds.ts
@@ -1,7 +1,7 @@
import {
+ AppBskyFeedDefs,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
- AppBskyFeedDefs,
} from '@atproto/api'
export function isEmbedByEmbedder(
diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts
index f588808fc3..02940f793d 100644
--- a/src/lib/haptics.ts
+++ b/src/lib/haptics.ts
@@ -1,24 +1,20 @@
import React from 'react'
import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics'
-import {isIOS, isWeb} from '#/platform/detection'
-import {useHapticsDisabled} from '#/state/preferences/disable-haptics'
+import {isIOS, isWeb} from 'platform/detection'
+import {useHapticsDisabled} from 'state/preferences/disable-haptics'
+
+const hapticImpact: ImpactFeedbackStyle = isIOS
+ ? ImpactFeedbackStyle.Medium
+ : ImpactFeedbackStyle.Light // Users said the medium impact was too strong on Android; see APP-537s
export function useHaptics() {
const isHapticsDisabled = useHapticsDisabled()
- return React.useCallback(
- (strength: 'Light' | 'Medium' | 'Heavy' = 'Medium') => {
- if (isHapticsDisabled || isWeb) {
- return
- }
-
- // Users said the medium impact was too strong on Android; see APP-537s
- const style = isIOS
- ? ImpactFeedbackStyle[strength]
- : ImpactFeedbackStyle.Light
- impactAsync(style)
- },
- [isHapticsDisabled],
- )
+ return React.useCallback(() => {
+ if (isHapticsDisabled || isWeb) {
+ return
+ }
+ impactAsync(hapticImpact)
+ }, [isHapticsDisabled])
}
diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts
index e75f13755f..3f01e98c5e 100644
--- a/src/lib/media/manip.ts
+++ b/src/lib/media/manip.ts
@@ -6,20 +6,18 @@ import {
copyAsync,
deleteAsync,
EncodingType,
- getInfoAsync,
makeDirectoryAsync,
StorageAccessFramework,
writeAsStringAsync,
} from 'expo-file-system'
-import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import * as MediaLibrary from 'expo-media-library'
import * as Sharing from 'expo-sharing'
+import ImageResizer from '@bam.tech/react-native-image-resizer'
import {Buffer} from 'buffer'
import RNFetchBlob from 'rn-fetch-blob'
-import {POST_IMG_MAX} from '#/lib/constants'
import {logger} from '#/logger'
-import {isAndroid, isIOS} from '#/platform/detection'
+import {isAndroid, isIOS} from 'platform/detection'
import {Dimensions} from './types'
export async function compressIfNeeded(
@@ -167,47 +165,29 @@ interface DoResizeOpts {
}
async function doResize(localUri: string, opts: DoResizeOpts): Promise {
- // We need to get the dimensions of the image before we resize it. Previously, the library we used allowed us to enter
- // a "max size", and it would do the "best possible size" calculation for us.
- // Now instead, we have to supply the final dimensions to the manipulation function instead.
- // Performing an "empty" manipulation lets us get the dimensions of the original image. React Native's Image.getSize()
- // does not work for local files...
- const imageRes = await manipulateAsync(localUri, [], {})
- const newDimensions = getResizedDimensions({
- width: imageRes.width,
- height: imageRes.height,
- })
-
for (let i = 0; i < 9; i++) {
- // nearest 10th
- const quality = Math.round((1 - 0.1 * i) * 10) / 10
- const resizeRes = await manipulateAsync(
+ const quality = 100 - i * 10
+ const resizeRes = await ImageResizer.createResizedImage(
localUri,
- [{resize: newDimensions}],
- {
- format: SaveFormat.JPEG,
- compress: quality,
- },
+ opts.width,
+ opts.height,
+ 'JPEG',
+ quality,
+ undefined,
+ undefined,
+ undefined,
+ {mode: opts.mode},
)
-
- const fileInfo = await getInfoAsync(resizeRes.uri)
- if (!fileInfo.exists) {
- throw new Error(
- 'The image manipulation library failed to create a new image.',
- )
- }
-
- if (fileInfo.size < opts.maxSize) {
- safeDeleteAsync(imageRes.uri)
+ if (resizeRes.size < opts.maxSize) {
return {
- path: normalizePath(resizeRes.uri),
+ path: normalizePath(resizeRes.path),
mime: 'image/jpeg',
- size: fileInfo.size,
+ size: resizeRes.size,
width: resizeRes.width,
height: resizeRes.height,
}
} else {
- safeDeleteAsync(resizeRes.uri)
+ safeDeleteAsync(resizeRes.path)
}
}
throw new Error(
@@ -331,25 +311,3 @@ async function withTempFile(
safeDeleteAsync(tmpDirUri)
}
}
-
-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),
- }
-}
diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx
index fc6fcde45e..e6b46ba774 100644
--- a/src/lib/media/picker.e2e.tsx
+++ b/src/lib/media/picker.e2e.tsx
@@ -1,37 +1,25 @@
+import RNFS from 'react-native-fs'
import {
Image as RNImage,
openCropper as openCropperFn,
} from 'react-native-image-crop-picker'
-import {
- documentDirectory,
- getInfoAsync,
- readDirectoryAsync,
-} from 'expo-file-system'
import {compressIfNeeded} from './manip'
import {CropperOptions} from './types'
async function getFile() {
- const imagesDir = documentDirectory!
- .split('/')
- .slice(0, -6)
- .concat(['Media', 'DCIM', '100APPLE'])
- .join('/')
-
- let files = await readDirectoryAsync(imagesDir)
- files = files.filter(file => file.endsWith('.JPG'))
- const file = `${imagesDir}/${files[0]}`
-
- const fileInfo = await getInfoAsync(file)
-
- if (!fileInfo.exists) {
- throw new Error('Failed to get file info')
- }
-
+ let files = await RNFS.readDir(
+ RNFS.LibraryDirectoryPath.split('/')
+ .slice(0, -5)
+ .concat(['Media', 'DCIM', '100APPLE'])
+ .join('/'),
+ )
+ files = files.filter(file => file.path.endsWith('.JPG'))
+ const file = files[0]
return await compressIfNeeded({
- path: file,
+ path: file.path,
mime: 'image/jpeg',
- size: fileInfo.size,
+ size: file.size,
width: 4288,
height: 2848,
})
diff --git a/src/lib/media/picker.shared.ts b/src/lib/media/picker.shared.ts
index 85539a833e..9146cd7787 100644
--- a/src/lib/media/picker.shared.ts
+++ b/src/lib/media/picker.shared.ts
@@ -4,7 +4,7 @@ import {
MediaTypeOptions,
} from 'expo-image-picker'
-import * as Toast from '#/view/com/util/Toast'
+import * as Toast from 'view/com/util/Toast'
import {getDataUriSize} from './util'
export async function openPicker(opts?: ImagePickerOptions) {
@@ -28,7 +28,7 @@ export async function openPicker(opts?: ImagePickerOptions) {
return false
})
.map(image => ({
- mime: image.mimeType || 'image/jpeg',
+ mime: 'image/jpeg',
height: image.height,
width: image.width,
path: image.uri,
diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx
index a53ffc9614..8782e14570 100644
--- a/src/lib/media/picker.web.tsx
+++ b/src/lib/media/picker.web.tsx
@@ -18,11 +18,9 @@ export async function openCropper(opts: CropperOptions): Promise {
name: 'crop-image',
uri: opts.path,
dimensions:
- opts.width && opts.height
+ opts.height && opts.width
? {width: opts.width, height: opts.height}
: undefined,
- aspect: opts.webAspectRatio,
- circular: opts.webCircularCrop,
onSelect: (img?: RNImage) => {
if (img) {
resolve(img)
diff --git a/src/lib/media/types.ts b/src/lib/media/types.ts
index ec94256ea1..e6f442759f 100644
--- a/src/lib/media/types.ts
+++ b/src/lib/media/types.ts
@@ -18,7 +18,4 @@ export interface CameraOpts {
cropperCircleOverlay?: boolean
}
-export type CropperOptions = Parameters[0] & {
- webAspectRatio?: number
- webCircularCrop?: boolean
-}
+export type CropperOptions = Parameters[0]
diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts
index 7576a9c33c..59d88023bf 100644
--- a/src/lib/moderation.ts
+++ b/src/lib/moderation.ts
@@ -33,20 +33,6 @@ export function isJustAMute(modui: ModerationUI): boolean {
return modui.filters.length === 1 && modui.filters[0].type === 'muted'
}
-export function moduiContainsHideableOffense(modui: ModerationUI): boolean {
- const label = modui.filters.at(0)
- if (label && label.type === 'label') {
- return labelIsHideableOffense(label.label)
- }
- return false
-}
-
-export function labelIsHideableOffense(
- label: ComAtprotoLabelDefs.Label,
-): boolean {
- return ['!hide', '!takedown'].includes(label.val)
-}
-
export function getLabelingServiceTitle({
displayName,
handle,
diff --git a/src/lib/strings/__tests__/email.test.ts b/src/lib/strings/__tests__/email.test.ts
deleted file mode 100644
index 4dfda658f7..0000000000
--- a/src/lib/strings/__tests__/email.test.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import {describe, expect, it} from '@jest/globals'
-import tldts from 'tldts'
-
-import {isEmailMaybeInvalid} from '#/lib/strings/email'
-
-describe('emailTypoChecker', () => {
- const invalidCases = [
- 'gnail.com',
- 'gnail.co',
- 'gmaill.com',
- 'gmaill.co',
- 'gmai.com',
- 'gmai.co',
- 'gmal.com',
- 'gmal.co',
- 'gmail.co',
- 'iclod.com',
- 'iclod.co',
- 'outllok.com',
- 'outllok.co',
- 'outlook.co',
- 'yaoo.com',
- 'yaoo.co',
- 'yaho.com',
- 'yaho.co',
- 'yahooo.com',
- 'yahooo.co',
- 'yahoo.co',
- 'hithere.jul',
- 'agpowj.notshop',
- 'thisisnot.avalid.tld.nope',
- // old tld for czechoslovakia
- 'czechoslovakia.cs',
- // tlds that cbs was registering in 2024 but cancelled
- 'liveon.cbs',
- 'its.showtime',
- ]
- const validCases = [
- 'gmail.com',
- // subdomains (tests end of string)
- 'gnail.com.test.com',
- 'outlook.com',
- 'yahoo.com',
- 'icloud.com',
- 'firefox.com',
- 'firefox.co',
- 'hello.world.com',
- 'buy.me.a.coffee.shop',
- 'mayotte.yt',
- 'aland.ax',
- 'bouvet.bv',
- 'uk.gb',
- 'chad.td',
- 'somalia.so',
- 'plane.aero',
- 'cute.cat',
- 'together.coop',
- 'findme.jobs',
- 'nightatthe.museum',
- 'industrial.mil',
- 'czechrepublic.cz',
- 'lovakia.sk',
- // new gtlds in 2024
- 'whatsinyour.locker',
- 'letsmakea.deal',
- 'skeet.now',
- 'everyone.みんな',
- 'bourgeois.lifestyle',
- 'california.living',
- 'skeet.ing',
- 'listeningto.music',
- 'createa.meme',
- ]
-
- it.each(invalidCases)(`should be invalid: abcde@%s`, domain => {
- expect(isEmailMaybeInvalid(`abcde@${domain}`, tldts)).toEqual(true)
- })
-
- it.each(validCases)(`should be valid: abcde@%s`, domain => {
- expect(isEmailMaybeInvalid(`abcde@${domain}`, tldts)).toEqual(false)
- })
-})
diff --git a/src/lib/strings/email.ts b/src/lib/strings/email.ts
deleted file mode 100644
index 04b6038476..0000000000
--- a/src/lib/strings/email.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type tldts from 'tldts'
-
-const COMMON_ERROR_PATTERN =
- /([a-zA-Z0-9._%+-]+)@(gnail\.(co|com)|gmaill\.(co|com)|gmai\.(co|com)|gmail\.co|gmal\.(co|com)|iclod\.(co|com)|icloud\.co|outllok\.(co|com)|outlok\.(co|com)|outlook\.co|yaoo\.(co|com)|yaho\.(co|com)|yahoo\.co|yahooo\.(co|com))$/
-
-export function isEmailMaybeInvalid(email: string, dynamicTldts: typeof tldts) {
- const isIcann = dynamicTldts.parse(email).isIcann
- return !isIcann || COMMON_ERROR_PATTERN.test(email)
-}
diff --git a/src/lib/styles.ts b/src/lib/styles.ts
index 55fb1a844b..6a3d796110 100644
--- a/src/lib/styles.ts
+++ b/src/lib/styles.ts
@@ -1,6 +1,6 @@
import {Dimensions, StyleProp, StyleSheet, TextStyle} from 'react-native'
-import {isWeb} from '#/platform/detection'
+import {isWeb} from 'platform/detection'
import {Theme, TypographyVariant} from './ThemeContext'
// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
@@ -79,13 +79,14 @@ export const s = StyleSheet.create({
// font weights
fw600: {fontWeight: '600'},
- bold: {fontWeight: '600'},
- fw500: {fontWeight: '600'},
- semiBold: {fontWeight: '600'},
+ bold: {fontWeight: '700'},
+ fw500: {fontWeight: '500'},
+ semiBold: {fontWeight: '500'},
fw400: {fontWeight: '400'},
normal: {fontWeight: '400'},
fw300: {fontWeight: '400'},
light: {fontWeight: '400'},
+ fw200: {fontWeight: '200'},
// text decoration
underline: {textDecorationLine: 'underline'},
diff --git a/src/lib/themes.ts b/src/lib/themes.ts
index eb11872fa3..d16f9f632a 100644
--- a/src/lib/themes.ts
+++ b/src/lib/themes.ts
@@ -100,12 +100,12 @@ export const defaultTheme: Theme = {
'2xl-medium': {
fontSize: 18,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '500',
},
'2xl-bold': {
fontSize: 18,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '700',
},
'2xl-heavy': {
fontSize: 18,
@@ -125,12 +125,12 @@ export const defaultTheme: Theme = {
'xl-medium': {
fontSize: 17,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '500',
},
'xl-bold': {
fontSize: 17,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '700',
},
'xl-heavy': {
fontSize: 17,
@@ -150,12 +150,12 @@ export const defaultTheme: Theme = {
'lg-medium': {
fontSize: 16,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '500',
},
'lg-bold': {
fontSize: 16,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '700',
},
'lg-heavy': {
fontSize: 16,
@@ -175,12 +175,12 @@ export const defaultTheme: Theme = {
'md-medium': {
fontSize: 15,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '500',
},
'md-bold': {
fontSize: 15,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '700',
},
'md-heavy': {
fontSize: 15,
@@ -200,12 +200,12 @@ export const defaultTheme: Theme = {
'sm-medium': {
fontSize: 14,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '500',
},
'sm-bold': {
fontSize: 14,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '700',
},
'sm-heavy': {
fontSize: 14,
@@ -225,12 +225,12 @@ export const defaultTheme: Theme = {
'xs-medium': {
fontSize: 13,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '500',
},
'xs-bold': {
fontSize: 13,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '700',
},
'xs-heavy': {
fontSize: 13,
@@ -241,24 +241,24 @@ export const defaultTheme: Theme = {
'title-2xl': {
fontSize: 34,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '500',
},
'title-xl': {
fontSize: 28,
letterSpacing: tokens.TRACKING,
- fontWeight: '600',
+ fontWeight: '500',
},
'title-lg': {
fontSize: 22,
- fontWeight: '600',
+ fontWeight: '500',
},
title: {
- fontWeight: '600',
+ fontWeight: '500',
fontSize: 20,
letterSpacing: tokens.TRACKING,
},
'title-sm': {
- fontWeight: '600',
+ fontWeight: 'bold',
fontSize: 17,
letterSpacing: tokens.TRACKING,
},
@@ -273,12 +273,12 @@ export const defaultTheme: Theme = {
fontWeight: '400',
},
'button-lg': {
- fontWeight: '600',
+ fontWeight: '500',
fontSize: 18,
letterSpacing: tokens.TRACKING,
},
button: {
- fontWeight: '600',
+ fontWeight: '500',
fontSize: 14,
letterSpacing: tokens.TRACKING,
},
@@ -325,11 +325,11 @@ export const darkTheme: Theme = {
textInverted: colors.green2,
},
inverted: {
- background: darkPalette.white,
+ background: lightPalette.white,
backgroundLight: lightPalette.contrast_50,
text: lightPalette.black,
textLight: lightPalette.contrast_700,
- textInverted: darkPalette.white,
+ textInverted: lightPalette.white,
link: lightPalette.primary_500,
border: lightPalette.contrast_100,
borderDark: lightPalette.contrast_200,
diff --git a/src/locale/deviceLocales.ts b/src/locale/deviceLocales.ts
deleted file mode 100644
index 9e19e372b8..0000000000
--- a/src/locale/deviceLocales.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import {getLocales as defaultGetLocales, Locale} from 'expo-localization'
-
-import {dedupArray} from '#/lib/functions'
-
-type LocalWithLanguageCode = Locale & {
- languageCode: string
-}
-
-/**
- * Normalized locales
- *
- * Handles legacy migration for Java devices.
- *
- * {@link https://github.com/bluesky-social/social-app/pull/4461}
- * {@link https://xml.coverpages.org/iso639a.html}
- */
-export function getLocales() {
- const locales = defaultGetLocales?.() ?? []
- const output: LocalWithLanguageCode[] = []
-
- for (const locale of locales) {
- if (typeof locale.languageCode === 'string') {
- if (locale.languageCode === 'in') {
- // indonesian
- locale.languageCode = 'id'
- }
- if (locale.languageCode === 'iw') {
- // hebrew
- locale.languageCode = 'he'
- }
- if (locale.languageCode === 'ji') {
- // yiddish
- locale.languageCode = 'yi'
- }
-
- // @ts-ignore checked above
- output.push(locale)
- }
- }
-
- return output
-}
-
-export const deviceLocales = getLocales()
-
-/**
- * BCP-47 language tag without region e.g. array of 2-char lang codes
- *
- * {@link https://docs.expo.dev/versions/latest/sdk/localization/#locale}
- */
-export const deviceLanguageCodes = dedupArray(
- deviceLocales.map(l => l.languageCode),
-)
diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts
index eb60fc5cf4..3bae45214d 100644
--- a/src/locale/helpers.ts
+++ b/src/locale/helpers.ts
@@ -2,7 +2,7 @@ import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import * as bcp47Match from 'bcp-47-match'
import lande from 'lande'
-import {hasProp} from '#/lib/type-guards'
+import {hasProp} from 'lib/type-guards'
import {
AppLanguage,
LANGUAGES_MAP_CODE2,
@@ -160,13 +160,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.en
}
-/**
- * Handles legacy migration for Java devices.
- *
- * {@link https://github.com/bluesky-social/social-app/pull/4461}
- * {@link https://xml.coverpages.org/iso639a.html}
- */
export function fixLegacyLanguageCode(code: string | null): string | null {
+ // handle some legacy code conversions, see https://xml.coverpages.org/iso639a.html
if (code === 'in') {
// indonesian
return 'id'
@@ -181,20 +176,3 @@ export function fixLegacyLanguageCode(code: string | null): string | null {
}
return code
}
-
-/**
- * Find the first language supported by our translation infra. Values should be
- * in order of preference, and match the values of {@link AppLanguage}.
- *
- * If no match, returns `en`.
- */
-export function findSupportedAppLanguage(languageTags: (string | undefined)[]) {
- const supported = new Set(Object.values(AppLanguage))
- for (const tag of languageTags) {
- if (!tag) continue
- if (supported.has(tag as AppLanguage)) {
- return tag
- }
- }
- return AppLanguage.en
-}
diff --git a/src/platform/detection.ts b/src/platform/detection.ts
index dc30c2fd33..c62ae71aae 100644
--- a/src/platform/detection.ts
+++ b/src/platform/detection.ts
@@ -1,4 +1,8 @@
import {Platform} from 'react-native'
+import {getLocales} from 'expo-localization'
+
+import {fixLegacyLanguageCode} from '#/locale/helpers'
+import {dedupArray} from 'lib/functions'
export const isIOS = Platform.OS === 'ios'
export const isAndroid = Platform.OS === 'android'
@@ -11,3 +15,9 @@ export const isMobileWeb =
// @ts-ignore we know window exists -prf
global.window.matchMedia(isMobileWebMediaQuery)?.matches
export const isIPhoneWeb = isWeb && /iPhone/.test(navigator.userAgent)
+
+export const deviceLocales = dedupArray(
+ getLocales?.()
+ .map?.(locale => fixLegacyLanguageCode(locale.languageCode))
+ .filter(code => typeof code === 'string'),
+) as string[]
diff --git a/src/screens/Deactivated.tsx b/src/screens/Deactivated.tsx
index 9b0b5b1660..997fe419ed 100644
--- a/src/screens/Deactivated.tsx
+++ b/src/screens/Deactivated.tsx
@@ -142,7 +142,7 @@ export function Deactivated() {
@@ -153,7 +153,7 @@ export function Deactivated() {
@@ -212,7 +212,7 @@ export function Deactivated() {
setShowLoggedOut(true)}>
diff --git a/src/screens/E2E/SharedPreferencesTesterScreen.tsx b/src/screens/E2E/SharedPreferencesTesterScreen.tsx
index 3f4ce563be..380f1080b0 100644
--- a/src/screens/E2E/SharedPreferencesTesterScreen.tsx
+++ b/src/screens/E2E/SharedPreferencesTesterScreen.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import {View} from 'react-native'
-import {ScrollView} from '#/view/com/util/Views'
+import {ScrollView} from 'view/com/util/Views'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
@@ -23,7 +23,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
- size="small"
+ size="xsmall"
onPress={async () => {
SharedPrefs.removeValue('testerString')
SharedPrefs.setValue('testerString', 'Hello')
@@ -39,7 +39,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
- size="small"
+ size="xsmall"
onPress={async () => {
SharedPrefs.removeValue('testerString')
const str = SharedPrefs.getString('testerString')
@@ -53,7 +53,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
- size="small"
+ size="xsmall"
onPress={async () => {
SharedPrefs.removeValue('testerBool')
SharedPrefs.setValue('testerBool', true)
@@ -68,7 +68,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
- size="small"
+ size="xsmall"
onPress={async () => {
SharedPrefs.removeValue('testerNumber')
SharedPrefs.setValue('testerNumber', 123)
@@ -83,7 +83,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
- size="small"
+ size="xsmall"
onPress={async () => {
SharedPrefs.removeFromSet('testerSet', 'Hello!')
SharedPrefs.addToSet('testerSet', 'Hello!')
@@ -98,7 +98,7 @@ export function SharedPreferencesTesterScreen() {
style={[a.self_center]}
variant="solid"
color="primary"
- size="small"
+ size="xsmall"
onPress={async () => {
SharedPrefs.removeFromSet('testerSet', 'Hello!')
const contains = SharedPrefs.setContains('testerSet', 'Hello!')
diff --git a/src/screens/Home/NoFeedsPinned.tsx b/src/screens/Home/NoFeedsPinned.tsx
index 74412763f2..3a98b87341 100644
--- a/src/screens/Home/NoFeedsPinned.tsx
+++ b/src/screens/Home/NoFeedsPinned.tsx
@@ -91,7 +91,7 @@ export function NoFeedsPinned({
@@ -102,7 +102,7 @@ export function NoFeedsPinned({
diff --git a/src/screens/List/ListHiddenScreen.tsx b/src/screens/List/ListHiddenScreen.tsx
index a694cbb837..473bb08ea4 100644
--- a/src/screens/List/ListHiddenScreen.tsx
+++ b/src/screens/List/ListHiddenScreen.tsx
@@ -5,18 +5,18 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
-import {useGoBack} from '#/lib/hooks/useGoBack'
-import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {RQKEY_ROOT as listQueryRoot} from '#/state/queries/list'
-import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list'
+import {useGoBack} from 'lib/hooks/useGoBack'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {useListBlockMutation, useListMuteMutation} from 'state/queries/list'
import {
UsePreferencesQueryResponse,
useRemoveFeedMutation,
-} from '#/state/queries/preferences'
-import {useSession} from '#/state/session'
-import * as Toast from '#/view/com/util/Toast'
-import {CenteredView} from '#/view/com/util/Views'
+} from 'state/queries/preferences'
+import {useSession} from 'state/session'
+import * as Toast from 'view/com/util/Toast'
+import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
@@ -152,7 +152,7 @@ export function ListHiddenScreen({
@@ -168,7 +168,7 @@ export function ListHiddenScreen({
setIsContentVisible(true)}
disabled={isProcessing}>
@@ -180,7 +180,7 @@ export function ListHiddenScreen({
{
if (isModList) {
@@ -204,7 +204,7 @@ export function ListHiddenScreen({
color="primary"
label={_(msg`Return to previous page`)}
onPress={goBack}
- size="large"
+ size="medium"
disabled={isProcessing}>
Go Back
diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx
index 678ba51237..8c002b1600 100644
--- a/src/screens/Login/ChooseAccountForm.tsx
+++ b/src/screens/Login/ChooseAccountForm.tsx
@@ -98,7 +98,7 @@ export const ChooseAccountForm = ({
label={_(msg`Back`)}
variant="solid"
color="secondary"
- size="large"
+ size="medium"
onPress={onPressBack}>
{_(msg`Back`)}
diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx
index 7acaae5101..8588888b87 100644
--- a/src/screens/Login/ForgotPasswordForm.tsx
+++ b/src/screens/Login/ForgotPasswordForm.tsx
@@ -129,7 +129,7 @@ export const ForgotPasswordForm = ({
label={_(msg`Back`)}
variant="solid"
color="secondary"
- size="large"
+ size="medium"
onPress={onPressBack}>
Back
@@ -143,7 +143,7 @@ export const ForgotPasswordForm = ({
label={_(msg`Next`)}
variant="solid"
color={'primary'}
- size="large"
+ size="medium"
onPress={onPressNext}>
Next
@@ -170,7 +170,7 @@ export const ForgotPasswordForm = ({
onPress={onEmailSent}
label={_(msg`Go to next`)}
accessibilityHint={_(msg`Navigates to the next screen`)}
- size="large"
+ size="medium"
variant="ghost"
color="secondary">
diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx
index 9c2237214b..9a01c04990 100644
--- a/src/screens/Login/LoginForm.tsx
+++ b/src/screens/Login/LoginForm.tsx
@@ -14,14 +14,14 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
-import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {isNetworkError} from '#/lib/strings/errors'
import {cleanError} from '#/lib/strings/errors'
import {createFullHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
-import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
import {useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
+import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {FormError} from '#/components/forms/FormError'
@@ -285,7 +285,7 @@ export const LoginForm = ({
label={_(msg`Back`)}
variant="solid"
color="secondary"
- size="large"
+ size="medium"
onPress={onPressBack}>
Back
@@ -299,7 +299,7 @@ export const LoginForm = ({
accessibilityHint={_(msg`Retries login`)}
variant="solid"
color="secondary"
- size="large"
+ size="medium"
onPress={onPressRetryConnect}>
Retry
@@ -319,7 +319,7 @@ export const LoginForm = ({
accessibilityHint={_(msg`Navigates to the next screen`)}
variant="solid"
color="primary"
- size="large"
+ size="medium"
onPress={onPressNext}>
Next
diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx
index 03e7d86696..5407f3f1e3 100644
--- a/src/screens/Login/PasswordUpdatedForm.tsx
+++ b/src/screens/Login/PasswordUpdatedForm.tsx
@@ -39,7 +39,7 @@ export const PasswordUpdatedForm = ({
accessibilityHint={_(msg`Closes password update alert`)}
variant="solid"
color="primary"
- size="large">
+ size="medium">
Okay
diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx
index a6658621cc..88f7ec5416 100644
--- a/src/screens/Login/SetNewPasswordForm.tsx
+++ b/src/screens/Login/SetNewPasswordForm.tsx
@@ -160,7 +160,7 @@ export const SetNewPasswordForm = ({
label={_(msg`Back`)}
variant="solid"
color="secondary"
- size="large"
+ size="medium"
onPress={onPressBack}>
Back
@@ -174,7 +174,7 @@ export const SetNewPasswordForm = ({
label={_(msg`Next`)}
variant="solid"
color="primary"
- size="large"
+ size="medium"
onPress={onPressNext}>
Next
diff --git a/src/screens/Messages/Conversation/ChatDisabled.tsx b/src/screens/Messages/Conversation/ChatDisabled.tsx
index c768d2504b..23acc41cde 100644
--- a/src/screens/Messages/Conversation/ChatDisabled.tsx
+++ b/src/screens/Messages/Conversation/ChatDisabled.tsx
@@ -128,7 +128,7 @@ function DialogInner() {
testID="backBtn"
variant="solid"
color="secondary"
- size="large"
+ size="medium"
onPress={onBack}
label={_(msg`Back`)}>
{_(msg`Back`)}
@@ -137,7 +137,7 @@ function DialogInner() {
testID="submitBtn"
variant="solid"
color="primary"
- size="large"
+ size="medium"
onPress={onSubmit}
label={_(msg`Submit`)}>
{_(msg`Submit`)}
diff --git a/src/screens/Messages/Conversation/MessageInputEmbed.tsx b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
index 2d1551019e..bf28ed4fe9 100644
--- a/src/screens/Messages/Conversation/MessageInputEmbed.tsx
+++ b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
@@ -174,6 +174,7 @@ export function MessageInputEmbed({
showAvatar
author={post.author}
moderation={moderation}
+ authorHasWarning={!!post.author.labels?.length}
timestamp={post.indexedAt}
postHref={itemHref}
style={a.flex_0}
diff --git a/src/screens/Messages/List/ChatListItem.tsx b/src/screens/Messages/List/ChatListItem.tsx
index e9668b4e11..c45cc28d7a 100644
--- a/src/screens/Messages/List/ChatListItem.tsx
+++ b/src/screens/Messages/List/ChatListItem.tsx
@@ -10,10 +10,6 @@ import {
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useHaptics} from '#/lib/haptics'
-import {decrementBadgeCount} from '#/lib/notifications/notifications'
-import {logEvent} from '#/lib/statsig/statsig'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {
postUriToRelativePath,
toBskyAppUrl,
@@ -23,6 +19,10 @@ import {isNative} from '#/platform/detection'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session'
+import {useHaptics} from 'lib/haptics'
+import {decrementBadgeCount} from 'lib/notifications/notifications'
+import {logEvent} from 'lib/statsig/statsig'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
@@ -248,7 +248,6 @@ function ChatListItemReady({
numberOfLines={1}
style={[{maxWidth: '85%'}, web([a.leading_normal])]}>
refetch()}>
diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx
index 9bfe6c3fac..cd3179674c 100644
--- a/src/screens/Moderation/index.tsx
+++ b/src/screens/Moderation/index.tsx
@@ -7,7 +7,6 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {logger} from '#/logger'
@@ -23,8 +22,8 @@ import {
useProfileUpdateMutation,
} from '#/state/queries/profile'
import {useSession} from '#/state/session'
-import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
import {useSetMinimalShellMode} from '#/state/shell'
+import {useAnalytics} from 'lib/analytics/analytics'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {ScrollView} from '#/view/com/util/Views'
@@ -339,7 +338,7 @@ export function ModerationScreenInner({
a.justify_between,
disabledOnIOS && {opacity: 0.5},
]}>
-
+
Enable adult content
- {isNonConfigurableModerationAuthority(
- labeler.creator.did,
- ) && }
)}
diff --git a/src/screens/Profile/Header/DisplayName.tsx b/src/screens/Profile/Header/DisplayName.tsx
index e30162c3af..955e3d72c8 100644
--- a/src/screens/Profile/Header/DisplayName.tsx
+++ b/src/screens/Profile/Header/DisplayName.tsx
@@ -2,9 +2,9 @@ import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs, ModerationDecision} from '@atproto/api'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
import {Shadow} from '#/state/cache/types'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
@@ -19,9 +19,8 @@ export function ProfileHeaderDisplayName({
return (
+ style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '500'}]}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
diff --git a/src/screens/Profile/Header/Handle.tsx b/src/screens/Profile/Header/Handle.tsx
index ba869b6626..0344f1a234 100644
--- a/src/screens/Profile/Header/Handle.tsx
+++ b/src/screens/Profile/Header/Handle.tsx
@@ -1,12 +1,11 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
+import {Trans} from '@lingui/macro'
-import {isInvalidHandle} from '#/lib/strings/handles'
-import {isIOS} from '#/platform/detection'
import {Shadow} from '#/state/cache/types'
+import {isInvalidHandle} from 'lib/strings/handles'
+import {isIOS} from 'platform/detection'
import {atoms as a, useTheme, web} from '#/alf'
import {NewskieDialog} from '#/components/NewskieDialog'
import {Text} from '#/components/Typography'
@@ -19,7 +18,6 @@ export function ProfileHeaderHandle({
disableTaps?: boolean
}) {
const t = useTheme()
- const {_} = useLingui()
const invalidHandle = isInvalidHandle(profile.handle)
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
return (
@@ -35,7 +33,6 @@ export function ProfileHeaderHandle({
) : undefined}
- {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
+ {invalidHandle ? ⚠Invalid Handle : `@${profile.handle}`}
)
diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx
index 69e04f4af1..d675fb38ed 100644
--- a/src/screens/Settings/AppearanceSettings.tsx
+++ b/src/screens/Settings/AppearanceSettings.tsx
@@ -205,7 +205,7 @@ export function AppearanceToggleButtonGroup({
}) {
const t = useTheme()
return (
-
+
diff --git a/src/screens/Settings/components/DeactivateAccountDialog.tsx b/src/screens/Settings/components/DeactivateAccountDialog.tsx
index 6958b7a478..2be42d13e6 100644
--- a/src/screens/Settings/components/DeactivateAccountDialog.tsx
+++ b/src/screens/Settings/components/DeactivateAccountDialog.tsx
@@ -102,7 +102,7 @@ function DeactivateAccountDialogInner({
{_(msg`Yes, deactivate`)}
diff --git a/src/screens/Signup/BackNextButtons.tsx b/src/screens/Signup/BackNextButtons.tsx
index e2401bb116..73bd428c8b 100644
--- a/src/screens/Signup/BackNextButtons.tsx
+++ b/src/screens/Signup/BackNextButtons.tsx
@@ -15,7 +15,6 @@ export interface BackNextButtonsProps {
onBackPress: () => void
onNextPress?: () => void
onRetryPress?: () => void
- overrideNextText?: string
}
export function BackNextButtons({
@@ -26,7 +25,6 @@ export function BackNextButtons({
onBackPress,
onNextPress,
onRetryPress,
- overrideNextText,
}: BackNextButtonsProps) {
const {_} = useLingui()
@@ -36,7 +34,7 @@ export function BackNextButtons({
label={_(msg`Go back to previous step`)}
variant="solid"
color="secondary"
- size="large"
+ size="medium"
onPress={onBackPress}>
Back
@@ -48,7 +46,7 @@ export function BackNextButtons({
label={_(msg`Press to retry`)}
variant="solid"
color="primary"
- size="large"
+ size="medium"
onPress={onRetryPress}>
Retry
@@ -61,11 +59,11 @@ export function BackNextButtons({
label={_(msg`Continue to next step`)}
variant="solid"
color="primary"
- size="large"
+ size="medium"
disabled={isLoading || isNextDisabled}
onPress={onNextPress}>
- {overrideNextText ? overrideNextText : Next}
+ Next
{isLoading && }
diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx
index 2d4b07318d..e0a7912fd7 100644
--- a/src/screens/Signup/StepInfo/index.tsx
+++ b/src/screens/Signup/StepInfo/index.tsx
@@ -3,10 +3,8 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as EmailValidator from 'email-validator'
-import type tldts from 'tldts'
import {logEvent} from '#/lib/statsig/statsig'
-import {isEmailMaybeInvalid} from '#/lib/strings/email'
import {logger} from '#/logger'
import {ScreenTransition} from '#/screens/Login/ScreenTransition'
import {is13, is18, useSignupContext} from '#/screens/Signup/state'
@@ -48,41 +46,13 @@ export function StepInfo({
const inviteCodeValueRef = useRef(state.inviteCode)
const emailValueRef = useRef(state.email)
- const prevEmailValueRef = useRef(state.email)
const passwordValueRef = useRef(state.password)
- const [hasWarnedEmail, setHasWarnedEmail] = React.useState(false)
-
- const tldtsRef = React.useRef()
- React.useEffect(() => {
- // @ts-expect-error - valid path
- import('tldts/dist/index.cjs.min.js').then(tldts => {
- tldtsRef.current = tldts
- })
- }, [])
-
- const onNextPress = () => {
+ const onNextPress = React.useCallback(async () => {
const inviteCode = inviteCodeValueRef.current
const email = emailValueRef.current
- const emailChanged = prevEmailValueRef.current !== email
const password = passwordValueRef.current
- if (emailChanged && tldtsRef.current) {
- if (isEmailMaybeInvalid(email, tldtsRef.current)) {
- prevEmailValueRef.current = email
- setHasWarnedEmail(true)
- return dispatch({
- type: 'setError',
- value: _(
- msg`It looks like you may have entered your email address incorrectly. Are you sure it's right?`,
- ),
- })
- }
- } else if (hasWarnedEmail) {
- setHasWarnedEmail(false)
- }
- prevEmailValueRef.current = email
-
if (!is13(state.dateOfBirth)) {
return
}
@@ -119,7 +89,13 @@ export function StepInfo({
logEvent('signup:nextPressed', {
activeStep: state.activeStep,
})
- }
+ }, [
+ _,
+ dispatch,
+ state.activeStep,
+ state.dateOfBirth,
+ state.serviceDescription?.inviteCodeRequired,
+ ])
return (
@@ -172,9 +148,6 @@ export function StepInfo({
testID="emailInput"
onChangeText={value => {
emailValueRef.current = value.trim()
- if (hasWarnedEmail) {
- setHasWarnedEmail(false)
- }
}}
label={_(msg`Enter your email address`)}
defaultValue={state.email}
@@ -235,7 +208,6 @@ export function StepInfo({
onBackPress={onPressBack}
onNextPress={onNextPress}
onRetryPress={refetchServer}
- overrideNextText={hasWarnedEmail ? _(msg`It's correct`) : undefined}
/>
)
diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx
index 3209800328..0e1a2e61fa 100644
--- a/src/screens/Signup/index.tsx
+++ b/src/screens/Signup/index.tsx
@@ -8,8 +8,8 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {FEEDBACK_FORM_URL} from '#/lib/constants'
import {useServiceQuery} from '#/state/queries/service'
-import {useStarterPackQuery} from '#/state/queries/starter-packs'
-import {useActiveStarterPack} from '#/state/shell/starter-pack'
+import {useStarterPackQuery} from 'state/queries/starter-packs'
+import {useActiveStarterPack} from 'state/shell/starter-pack'
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
import {
initialState,
@@ -132,7 +132,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
!gtMobile && {paddingBottom: 100},
]}>
-
+
Step {state.activeStep + 1} of{' '}
{state.serviceDescription &&
diff --git a/src/screens/StarterPack/StarterPackLandingScreen.tsx b/src/screens/StarterPack/StarterPackLandingScreen.tsx
index 68ff3aa7bc..5f1d5e0628 100644
--- a/src/screens/StarterPack/StarterPackLandingScreen.tsx
+++ b/src/screens/StarterPack/StarterPackLandingScreen.tsx
@@ -11,22 +11,22 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {isAndroidWeb} from '#/lib/browser'
import {JOINED_THIS_WEEK} from '#/lib/constants'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {logEvent} from '#/lib/statsig/statsig'
-import {createStarterPackGooglePlayUri} from '#/lib/strings/starter-pack'
-import {isWeb} from '#/platform/detection'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useStarterPackQuery} from '#/state/queries/starter-packs'
+import {isAndroidWeb} from 'lib/browser'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {logEvent} from 'lib/statsig/statsig'
+import {createStarterPackGooglePlayUri} from 'lib/strings/starter-pack'
+import {isWeb} from 'platform/detection'
+import {useModerationOpts} from 'state/preferences/moderation-opts'
+import {useStarterPackQuery} from 'state/queries/starter-packs'
import {
useActiveStarterPack,
useSetActiveStarterPack,
-} from '#/state/shell/starter-pack'
-import {LoggedOutScreenState} from '#/view/com/auth/LoggedOut'
+} from 'state/shell/starter-pack'
import {formatCount} from '#/view/com/util/numeric/format'
-import {CenteredView} from '#/view/com/util/Views'
-import {Logo} from '#/view/icons/Logo'
+import {LoggedOutScreenState} from 'view/com/auth/LoggedOut'
+import {CenteredView} from 'view/com/util/Views'
+import {Logo} from 'view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
@@ -188,7 +188,12 @@ function LandingScreenLoaded({
{record.name}
+ style={[
+ a.text_center,
+ a.font_semibold,
+ a.text_md,
+ {color: 'white'},
+ ]}>
Starter pack by {`@${creator.handle}`}
@@ -214,7 +219,11 @@ function LandingScreenLoaded({
color={t.atoms.text_contrast_medium.color}
/>
{formatCount(i18n, JOINED_THIS_WEEK)} joined this week
@@ -299,7 +308,7 @@ function LandingScreenLoaded({
label={_(msg`Signup without a starter pack`)}
variant="solid"
color="secondary"
- size="large"
+ size="medium"
style={[a.py_lg]}
onPress={onJoinWithoutPress}>
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index e3d32a1dd5..5b267ff272 100644
--- a/src/screens/StarterPack/StarterPackScreen.tsx
+++ b/src/screens/StarterPack/StarterPackScreen.tsx
@@ -15,35 +15,35 @@ import {useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query'
-import {batchedUpdates} from '#/lib/batchedUpdates'
-import {HITSLOP_20} from '#/lib/constants'
-import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
-import {makeProfileLink, makeStarterPackLink} from '#/lib/routes/links'
-import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
-import {logEvent} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors'
-import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
-import {updateProfileShadow} from '#/state/cache/profile-shadow'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {getAllListMembers} from '#/state/queries/list-members'
-import {useResolvedStarterPackShortLink} from '#/state/queries/resolve-short-link'
-import {useResolveDidQuery} from '#/state/queries/resolve-uri'
-import {useShortenLink} from '#/state/queries/shorten-link'
import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs'
-import {useStarterPackQuery} from '#/state/queries/starter-packs'
-import {useAgent, useSession} from '#/state/session'
-import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {
ProgressGuideAction,
useProgressGuideControls,
} from '#/state/shell/progress-guide'
-import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
-import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
-import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
+import {batchedUpdates} from 'lib/batchedUpdates'
+import {HITSLOP_20} from 'lib/constants'
+import {isBlockedOrBlocking, isMuted} from 'lib/moderation/blocked-and-muted'
+import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links'
+import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
+import {logEvent} from 'lib/statsig/statsig'
+import {getStarterPackOgCard} from 'lib/strings/starter-pack'
+import {isWeb} from 'platform/detection'
+import {updateProfileShadow} from 'state/cache/profile-shadow'
+import {useModerationOpts} from 'state/preferences/moderation-opts'
+import {getAllListMembers} from 'state/queries/list-members'
+import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link'
+import {useResolveDidQuery} from 'state/queries/resolve-uri'
+import {useShortenLink} from 'state/queries/shorten-link'
+import {useStarterPackQuery} from 'state/queries/starter-packs'
+import {useAgent, useSession} from 'state/session'
+import {useLoggedOutViewControls} from 'state/shell/logged-out'
+import {useSetActiveStarterPack} from 'state/shell/starter-pack'
import * as Toast from '#/view/com/util/Toast'
-import {CenteredView} from '#/view/com/util/Views'
+import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
+import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
+import {CenteredView} from 'view/com/util/Views'
import {bulkWriteFollows} from '#/screens/Onboarding/util'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -449,7 +449,7 @@ function Header({
}}
variant="solid"
color="primary"
- size="large">
+ size="medium">
Join Bluesky
@@ -645,7 +645,7 @@ function OverflowMenu({
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index 65a3500f62..40a4a510b7 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -19,32 +19,32 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
-import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from '#/lib/constants'
-import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
-import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
-import {logEvent} from '#/lib/statsig/statsig'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {enforceLen} from '#/lib/strings/helpers'
+import {logger} from '#/logger'
+import {HITSLOP_10, STARTER_PACK_MAX_SIZE} from 'lib/constants'
+import {createSanitizedDisplayName} from 'lib/moderation/create-sanitized-display-name'
+import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
+import {logEvent} from 'lib/statsig/statsig'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {enforceLen} from 'lib/strings/helpers'
import {
getStarterPackOgCard,
parseStarterPackUri,
-} from '#/lib/strings/starter-pack'
-import {logger} from '#/logger'
-import {isAndroid, isNative, isWeb} from '#/platform/detection'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useAllListMembersQuery} from '#/state/queries/list-members'
-import {useProfileQuery} from '#/state/queries/profile'
+} from 'lib/strings/starter-pack'
+import {isAndroid, isNative, isWeb} from 'platform/detection'
+import {useModerationOpts} from 'state/preferences/moderation-opts'
+import {useAllListMembersQuery} from 'state/queries/list-members'
+import {useProfileQuery} from 'state/queries/profile'
import {
useCreateStarterPackMutation,
useEditStarterPackMutation,
useStarterPackQuery,
-} from '#/state/queries/starter-packs'
-import {useSession} from '#/state/session'
-import {useSetMinimalShellMode} from '#/state/shell'
+} from 'state/queries/starter-packs'
+import {useSession} from 'state/session'
+import {useSetMinimalShellMode} from 'state/shell'
import * as Toast from '#/view/com/util/Toast'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {CenteredView} from '#/view/com/util/Views'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {CenteredView} from 'view/com/util/Views'
import {useWizardState, WizardStep} from '#/screens/StarterPack/Wizard/State'
import {StepDetails} from '#/screens/StarterPack/Wizard/StepDetails'
import {StepFeeds} from '#/screens/StarterPack/Wizard/StepFeeds'
@@ -358,7 +358,7 @@ function Container({children}: {children: React.ReactNode}) {
label={_(msg`Next`)}
variant="solid"
color="primary"
- size="large"
+ size="medium"
style={[a.mx_xl, a.mb_lg, {marginTop: 35}]}
onPress={() => dispatch({type: 'Next'})}>
diff --git a/src/state/gallery.ts b/src/state/gallery.ts
deleted file mode 100644
index f4c8b712ef..0000000000
--- a/src/state/gallery.ts
+++ /dev/null
@@ -1,299 +0,0 @@
-import {
- cacheDirectory,
- deleteAsync,
- makeDirectoryAsync,
- moveAsync,
-} from 'expo-file-system'
-import {
- Action,
- ActionCrop,
- manipulateAsync,
- SaveFormat,
-} from 'expo-image-manipulator'
-import {nanoid} from 'nanoid/non-secure'
-
-import {POST_IMG_MAX} from '#/lib/constants'
-import {getImageDim} from '#/lib/media/manip'
-import {openCropper} from '#/lib/media/picker'
-import {getDataUriSize} from '#/lib/media/util'
-import {isIOS, isNative} from '#/platform/detection'
-
-export type ImageTransformation = {
- crop?: ActionCrop['crop']
-}
-
-export type ImageMeta = {
- path: string
- width: number
- height: number
- mime: string
-}
-
-export type ImageSource = ImageMeta & {
- id: string
-}
-
-type ComposerImageBase = {
- alt: string
- source: ImageSource
-}
-type ComposerImageWithoutTransformation = ComposerImageBase & {
- transformed?: undefined
- manips?: undefined
-}
-type ComposerImageWithTransformation = ComposerImageBase & {
- transformed: ImageMeta
- manips?: ImageTransformation
-}
-
-export type ComposerImage =
- | ComposerImageWithoutTransformation
- | ComposerImageWithTransformation
-
-let _imageCacheDirectory: string
-
-function getImageCacheDirectory(): string | null {
- if (isNative) {
- return (_imageCacheDirectory ??= joinPath(cacheDirectory!, 'bsky-composer'))
- }
-
- return null
-}
-
-export async function createComposerImage(
- raw: ImageMeta,
-): Promise {
- return {
- alt: '',
- source: {
- id: nanoid(),
- path: await moveIfNecessary(raw.path),
- width: raw.width,
- height: raw.height,
- mime: raw.mime,
- },
- }
-}
-
-export type InitialImage = {
- uri: string
- width: number
- height: number
- altText?: string
-}
-
-export function createInitialImages(
- uris: InitialImage[] = [],
-): ComposerImageWithoutTransformation[] {
- return uris.map(({uri, width, height, altText = ''}) => {
- return {
- alt: altText,
- source: {
- id: nanoid(),
- path: uri,
- width: width,
- height: height,
- mime: 'image/jpeg',
- },
- }
- })
-}
-
-export async function pasteImage(
- uri: string,
-): Promise {
- const {width, height} = await getImageDim(uri)
- const match = /^data:(.+?);/.exec(uri)
-
- return {
- alt: '',
- source: {
- id: nanoid(),
- path: uri,
- width: width,
- height: height,
- mime: match ? match[1] : 'image/jpeg',
- },
- }
-}
-
-export async function cropImage(img: ComposerImage): Promise {
- if (!isNative) {
- return img
- }
-
- // NOTE
- // on ios, react-native-image-crop-picker gives really bad quality
- // without specifying width and height. on android, however, the
- // crop stretches incorrectly if you do specify it. these are
- // both separate bugs in the library. we deal with that by
- // providing width & height for ios only
- // -prf
-
- const source = img.source
- const [w, h] = containImageRes(source.width, source.height, POST_IMG_MAX)
-
- // @todo: we're always passing the original image here, does image-cropper
- // allows for setting initial crop dimensions? -mary
- try {
- const cropped = await openCropper({
- mediaType: 'photo',
- path: source.path,
- freeStyleCropEnabled: true,
- ...(isIOS ? {width: w, height: h} : {}),
- })
-
- return {
- alt: img.alt,
- source: source,
- transformed: {
- path: await moveIfNecessary(cropped.path),
- width: cropped.width,
- height: cropped.height,
- mime: cropped.mime,
- },
- }
- } catch (e) {
- if (e instanceof Error && e.message.includes('User cancelled')) {
- return img
- }
-
- throw e
- }
-}
-
-export async function manipulateImage(
- img: ComposerImage,
- trans: ImageTransformation,
-): Promise {
- const rawActions: (Action | undefined)[] = [trans.crop && {crop: trans.crop}]
-
- const actions = rawActions.filter((a): a is Action => a !== undefined)
-
- if (actions.length === 0) {
- if (img.transformed === undefined) {
- return img
- }
-
- return {alt: img.alt, source: img.source}
- }
-
- const source = img.source
- const result = await manipulateAsync(source.path, actions, {
- format: SaveFormat.PNG,
- })
-
- return {
- alt: img.alt,
- source: img.source,
- transformed: {
- path: await moveIfNecessary(result.uri),
- width: result.width,
- height: result.height,
- mime: 'image/png',
- },
- manips: trans,
- }
-}
-
-export function resetImageManipulation(
- img: ComposerImage,
-): ComposerImageWithoutTransformation {
- if (img.transformed !== undefined) {
- return {alt: img.alt, source: img.source}
- }
-
- return img
-}
-
-export async function compressImage(img: ComposerImage): Promise {
- const source = img.transformed || img.source
-
- const [w, h] = containImageRes(source.width, source.height, POST_IMG_MAX)
- const cacheDir = isNative && getImageCacheDirectory()
-
- for (let i = 10; i > 0; i--) {
- // Float precision
- const factor = i / 10
-
- const res = await manipulateAsync(
- source.path,
- [{resize: {width: w, height: h}}],
- {
- compress: factor,
- format: SaveFormat.JPEG,
- base64: true,
- },
- )
-
- const base64 = res.base64
-
- if (base64 !== undefined && getDataUriSize(base64) <= POST_IMG_MAX.size) {
- return {
- path: await moveIfNecessary(res.uri),
- width: res.width,
- height: res.height,
- mime: 'image/jpeg',
- }
- }
-
- if (cacheDir) {
- await deleteAsync(res.uri)
- }
- }
-
- throw new Error(`Unable to compress image`)
-}
-
-async function moveIfNecessary(from: string) {
- const cacheDir = isNative && getImageCacheDirectory()
-
- if (cacheDir && from.startsWith(cacheDir)) {
- const to = joinPath(cacheDir, nanoid(36))
-
- await makeDirectoryAsync(cacheDir, {intermediates: true})
- await moveAsync({from, to})
-
- return to
- }
-
- return from
-}
-
-/** Purge files that were created to accomodate image manipulation */
-export async function purgeTemporaryImageFiles() {
- const cacheDir = isNative && getImageCacheDirectory()
-
- if (cacheDir) {
- await deleteAsync(cacheDir, {idempotent: true})
- await makeDirectoryAsync(cacheDir)
- }
-}
-
-function joinPath(a: string, b: string) {
- if (a.endsWith('/')) {
- if (b.startsWith('/')) {
- return a.slice(0, -1) + b
- }
- return a + b
- } else if (b.startsWith('/')) {
- return a + b
- }
- return a + '/' + b
-}
-
-function containImageRes(
- w: number,
- h: number,
- {width: maxW, height: maxH}: {width: number; height: number},
-): [width: number, height: number] {
- let scale = 1
-
- if (w > maxW || h > maxH) {
- scale = w > h ? maxW / w : maxH / h
- w = Math.floor(w * scale)
- h = Math.floor(h * scale)
- }
-
- return [w, h]
-}
diff --git a/src/state/geolocation.tsx b/src/state/geolocation.tsx
deleted file mode 100644
index 4d45bb574b..0000000000
--- a/src/state/geolocation.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-import React from 'react'
-import EventEmitter from 'eventemitter3'
-
-import {networkRetry} from '#/lib/async/retry'
-import {logger} from '#/logger'
-import {IS_DEV} from '#/env'
-import {Device, device} from '#/storage'
-
-const events = new EventEmitter()
-const EVENT = 'geolocation-updated'
-const emitGeolocationUpdate = (geolocation: Device['geolocation']) => {
- events.emit(EVENT, geolocation)
-}
-const onGeolocationUpdate = (
- listener: (geolocation: Device['geolocation']) => void,
-) => {
- events.on(EVENT, listener)
- return () => {
- events.off(EVENT, listener)
- }
-}
-
-/**
- * Default geolocation value. IF undefined, we fail closed and apply all
- * additional mod authorities.
- */
-export const DEFAULT_GEOLOCATION: Device['geolocation'] = {
- countryCode: undefined,
-}
-
-async function getGeolocation(): Promise {
- const res = await fetch(`https://bsky.app/ipcc`)
-
- if (!res.ok) {
- throw new Error(`geolocation: lookup failed ${res.status}`)
- }
-
- const json = await res.json()
-
- if (json.countryCode) {
- return {
- countryCode: json.countryCode,
- }
- } else {
- return undefined
- }
-}
-
-/**
- * Local promise used within this file only.
- */
-let geolocationResolution: Promise | undefined
-
-/**
- * Begin the process of resolving geolocation. This should be called once at
- * app start.
- *
- * THIS METHOD SHOULD NEVER THROW.
- *
- * This method is otherwise not used for any purpose. To ensure geolocation is
- * resolved, use {@link ensureGeolocationResolved}
- */
-export function beginResolveGeolocation() {
- /**
- * In dev, IP server is unavailable, so we just set the default geolocation
- * and fail closed.
- */
- if (IS_DEV) {
- geolocationResolution = new Promise(y => y())
- device.set(['geolocation'], DEFAULT_GEOLOCATION)
- return
- }
-
- geolocationResolution = new Promise(async resolve => {
- try {
- // Try once, fail fast
- const geolocation = await getGeolocation()
- if (geolocation) {
- device.set(['geolocation'], geolocation)
- emitGeolocationUpdate(geolocation)
- logger.debug(`geolocation: success`, {geolocation})
- } else {
- // endpoint should throw on all failures, this is insurance
- throw new Error(`geolocation: nothing returned from initial request`)
- }
- } catch (e: any) {
- logger.error(`geolocation: failed initial request`, {
- safeMessage: e.message,
- })
-
- // set to default
- device.set(['geolocation'], DEFAULT_GEOLOCATION)
-
- // retry 3 times, but don't await, proceed with default
- networkRetry(3, getGeolocation)
- .then(geolocation => {
- if (geolocation) {
- device.set(['geolocation'], geolocation)
- emitGeolocationUpdate(geolocation)
- logger.debug(`geolocation: success`, {geolocation})
- } else {
- // endpoint should throw on all failures, this is insurance
- throw new Error(`geolocation: nothing returned from retries`)
- }
- })
- .catch((e: any) => {
- // complete fail closed
- logger.error(`geolocation: failed retries`, {safeMessage: e.message})
- })
- } finally {
- resolve(undefined)
- }
- })
-}
-
-/**
- * Ensure that geolocation has been resolved, or at the very least attempted
- * once. Subsequent retries will not be captured by this `await`. Those will be
- * reported via {@link events}.
- */
-export async function ensureGeolocationResolved() {
- if (!geolocationResolution) {
- throw new Error(`geolocation: beginResolveGeolocation not called yet`)
- }
-
- const cached = device.get(['geolocation'])
- if (cached) {
- logger.debug(`geolocation: using cache`, {cached})
- } else {
- logger.debug(`geolocation: no cache`)
- await geolocationResolution
- logger.debug(`geolocation: resolved`, {
- resolved: device.get(['geolocation']),
- })
- }
-}
-
-type Context = {
- geolocation: Device['geolocation']
-}
-
-const context = React.createContext({
- geolocation: DEFAULT_GEOLOCATION,
-})
-
-export function Provider({children}: {children: React.ReactNode}) {
- const [geolocation, setGeolocation] = React.useState(() => {
- const initial = device.get(['geolocation']) || DEFAULT_GEOLOCATION
- return initial
- })
-
- React.useEffect(() => {
- return onGeolocationUpdate(geolocation => {
- setGeolocation(geolocation!)
- })
- }, [])
-
- const ctx = React.useMemo(() => {
- return {
- geolocation,
- }
- }, [geolocation])
-
- return {children}
-}
-
-export function useGeolocation() {
- return React.useContext(context)
-}
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 5be21dfd39..529dc55907 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -3,6 +3,8 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import {GalleryModel} from '#/state/models/media/gallery'
+import {ImageModel} from '#/state/models/media/image'
export interface EditProfileModal {
name: 'edit-profile'
@@ -35,15 +37,24 @@ export interface ListAddRemoveUsersModal {
) => void
}
+export interface EditImageModal {
+ name: 'edit-image'
+ image: ImageModel
+ gallery: GalleryModel
+}
+
export interface CropImageModal {
name: 'crop-image'
uri: string
dimensions?: {width: number; height: number}
- aspect?: number
- circular?: boolean
onSelect: (img?: RNImage) => void
}
+export interface AltTextImageModal {
+ name: 'alt-text-image'
+ image: ImageModel
+}
+
export interface DeleteAccountModal {
name: 'delete-account'
}
@@ -126,7 +137,9 @@ export type Modal =
| ListAddRemoveUsersModal
// Posts
+ | AltTextImageModal
| CropImageModal
+ | EditImageModal
| SelfLabelModal
// Bluesky access
diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts
new file mode 100644
index 0000000000..828905002e
--- /dev/null
+++ b/src/state/models/media/gallery.ts
@@ -0,0 +1,110 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+
+import {getImageDim} from 'lib/media/manip'
+import {openPicker} from 'lib/media/picker'
+import {ImageInitOptions, ImageModel} from './image'
+
+interface InitialImageUri {
+ uri: string
+ width: number
+ height: number
+ altText?: string
+}
+
+export class GalleryModel {
+ images: ImageModel[] = []
+
+ constructor(uris?: InitialImageUri[]) {
+ makeAutoObservable(this)
+
+ if (uris) {
+ this.addFromUris(uris)
+ }
+ }
+
+ get isEmpty() {
+ return this.size === 0
+ }
+
+ get size() {
+ return this.images.length
+ }
+
+ get needsAltText() {
+ return this.images.some(image => image.altText.trim() === '')
+ }
+
+ *add(image_: ImageInitOptions) {
+ if (this.size >= 4) {
+ return
+ }
+
+ // Temporarily enforce uniqueness but can eventually also use index
+ if (!this.images.some(i => i.path === image_.path)) {
+ const image = new ImageModel(image_)
+
+ // Initial resize
+ image.manipulate({})
+ this.images.push(image)
+ }
+ }
+
+ async paste(uri: string) {
+ if (this.size >= 4) {
+ return
+ }
+
+ const {width, height} = await getImageDim(uri)
+
+ const image = {
+ path: uri,
+ height,
+ width,
+ }
+
+ runInAction(() => {
+ this.add(image)
+ })
+ }
+
+ setAltText(image: ImageModel, altText: string) {
+ image.setAltText(altText)
+ }
+
+ crop(image: ImageModel) {
+ image.crop()
+ }
+
+ remove(image: ImageModel) {
+ const index = this.images.findIndex(image_ => image_.path === image.path)
+ this.images.splice(index, 1)
+ }
+
+ async previous(image: ImageModel) {
+ image.previous()
+ }
+
+ async pick() {
+ const images = await openPicker({
+ selectionLimit: 4 - this.size,
+ allowsMultipleSelection: true,
+ })
+
+ return await Promise.all(
+ images.map(image => {
+ this.add(image)
+ }),
+ )
+ }
+
+ async addFromUris(uris: InitialImageUri[]) {
+ for (const uriObj of uris) {
+ this.add({
+ height: uriObj.height,
+ width: uriObj.width,
+ path: uriObj.uri,
+ altText: uriObj.altText,
+ })
+ }
+ }
+}
diff --git a/src/state/models/media/image.e2e.ts b/src/state/models/media/image.e2e.ts
new file mode 100644
index 0000000000..ccabd50475
--- /dev/null
+++ b/src/state/models/media/image.e2e.ts
@@ -0,0 +1,146 @@
+import {Image as RNImage} from 'react-native-image-crop-picker'
+import {makeAutoObservable} from 'mobx'
+import {POST_IMG_MAX} from 'lib/constants'
+import {ActionCrop} from 'expo-image-manipulator'
+import {Position} from 'react-avatar-editor'
+import {Dimensions} from 'lib/media/types'
+
+export interface ImageManipulationAttributes {
+ aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
+ rotate?: number
+ scale?: number
+ position?: Position
+ flipHorizontal?: boolean
+ flipVertical?: boolean
+}
+
+export class ImageModel implements Omit {
+ path: string
+ mime = 'image/jpeg'
+ width: number
+ height: number
+ altText = ''
+ cropped?: RNImage = undefined
+ compressed?: RNImage = undefined
+
+ // Web manipulation
+ prev?: RNImage
+ attributes: ImageManipulationAttributes = {
+ aspectRatio: 'None',
+ scale: 1,
+ flipHorizontal: false,
+ flipVertical: false,
+ rotate: 0,
+ }
+ prevAttributes: ImageManipulationAttributes = {}
+
+ constructor(image: Omit) {
+ makeAutoObservable(this)
+
+ this.path = image.path
+ this.width = image.width
+ this.height = image.height
+ }
+
+ setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
+ this.attributes.aspectRatio = aspectRatio
+ }
+
+ setRotate(degrees: number) {
+ this.attributes.rotate = degrees
+ this.manipulate({})
+ }
+
+ flipVertical() {
+ this.attributes.flipVertical = !this.attributes.flipVertical
+ this.manipulate({})
+ }
+
+ flipHorizontal() {
+ this.attributes.flipHorizontal = !this.attributes.flipHorizontal
+ this.manipulate({})
+ }
+
+ get ratioMultipliers() {
+ return {
+ '4:3': 4 / 3,
+ '1:1': 1,
+ '3:4': 3 / 4,
+ None: this.width / this.height,
+ }
+ }
+
+ getUploadDimensions(
+ dimensions: Dimensions,
+ maxDimensions: Dimensions = POST_IMG_MAX,
+ as: ImageManipulationAttributes['aspectRatio'] = 'None',
+ ) {
+ const {width, height} = dimensions
+ const {width: maxWidth, height: maxHeight} = maxDimensions
+
+ return width < maxWidth && height < maxHeight
+ ? {
+ width,
+ height,
+ }
+ : this.getResizedDimensions(as, POST_IMG_MAX.width)
+ }
+
+ getResizedDimensions(
+ as: ImageManipulationAttributes['aspectRatio'] = 'None',
+ maxSide: number,
+ ) {
+ const ratioMultiplier = this.ratioMultipliers[as]
+
+ if (ratioMultiplier === 1) {
+ return {
+ height: maxSide,
+ width: maxSide,
+ }
+ }
+
+ if (ratioMultiplier < 1) {
+ return {
+ width: maxSide * ratioMultiplier,
+ height: maxSide,
+ }
+ }
+
+ return {
+ width: maxSide,
+ height: maxSide / ratioMultiplier,
+ }
+ }
+
+ setAltText(altText: string) {
+ this.altText = altText.trim()
+ }
+
+ // Only compress prior to upload
+ async compress() {
+ // do nothing
+ }
+
+ // Mobile
+ async crop() {
+ // do nothing
+ }
+
+ // Web manipulation
+ async manipulate(
+ _attributes: {
+ crop?: ActionCrop['crop']
+ } & ImageManipulationAttributes,
+ ) {
+ // do nothing
+ }
+
+ resetCropped() {
+ this.manipulate({})
+ }
+
+ previous() {
+ this.cropped = this.prev
+ this.attributes = this.prevAttributes
+ }
+}
diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts
new file mode 100644
index 0000000000..55f6364911
--- /dev/null
+++ b/src/state/models/media/image.ts
@@ -0,0 +1,310 @@
+import {Image as RNImage} from 'react-native-image-crop-picker'
+import * as ImageManipulator from 'expo-image-manipulator'
+import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
+import {makeAutoObservable, runInAction} from 'mobx'
+import {Position} from 'react-avatar-editor'
+
+import {logger} from '#/logger'
+import {POST_IMG_MAX} from 'lib/constants'
+import {openCropper} from 'lib/media/picker'
+import {Dimensions} from 'lib/media/types'
+import {getDataUriSize} from 'lib/media/util'
+import {isIOS} from 'platform/detection'
+
+export interface ImageManipulationAttributes {
+ aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
+ rotate?: number
+ scale?: number
+ position?: Position
+ flipHorizontal?: boolean
+ flipVertical?: boolean
+}
+
+export interface ImageInitOptions {
+ path: string
+ width: number
+ height: number
+ altText?: string
+}
+
+const MAX_IMAGE_SIZE_IN_BYTES = 976560
+
+export class ImageModel implements Omit {
+ path: string
+ mime = 'image/jpeg'
+ width: number
+ height: number
+ altText = ''
+ cropped?: RNImage = undefined
+ compressed?: RNImage = undefined
+
+ // Web manipulation
+ prev?: RNImage
+ attributes: ImageManipulationAttributes = {
+ aspectRatio: 'None',
+ scale: 1,
+ flipHorizontal: false,
+ flipVertical: false,
+ rotate: 0,
+ }
+ prevAttributes: ImageManipulationAttributes = {}
+
+ constructor(image: ImageInitOptions) {
+ makeAutoObservable(this)
+
+ this.path = image.path
+ this.width = image.width
+ this.height = image.height
+ if (image.altText !== undefined) {
+ this.setAltText(image.altText)
+ }
+ }
+
+ setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
+ this.attributes.aspectRatio = aspectRatio
+ }
+
+ setRotate(degrees: number) {
+ this.attributes.rotate = degrees
+ this.manipulate({})
+ }
+
+ flipVertical() {
+ this.attributes.flipVertical = !this.attributes.flipVertical
+ this.manipulate({})
+ }
+
+ flipHorizontal() {
+ this.attributes.flipHorizontal = !this.attributes.flipHorizontal
+ this.manipulate({})
+ }
+
+ get ratioMultipliers() {
+ return {
+ '4:3': 4 / 3,
+ '1:1': 1,
+ '3:4': 3 / 4,
+ None: this.width / this.height,
+ }
+ }
+
+ getUploadDimensions(
+ dimensions: Dimensions,
+ maxDimensions: Dimensions = POST_IMG_MAX,
+ as: ImageManipulationAttributes['aspectRatio'] = 'None',
+ ) {
+ const {width, height} = dimensions
+ const {width: maxWidth, height: maxHeight} = maxDimensions
+
+ return width < maxWidth && height < maxHeight
+ ? {
+ width,
+ height,
+ }
+ : this.getResizedDimensions(as, POST_IMG_MAX.width)
+ }
+
+ getResizedDimensions(
+ as: ImageManipulationAttributes['aspectRatio'] = 'None',
+ maxSide: number,
+ ) {
+ const ratioMultiplier = this.ratioMultipliers[as]
+
+ if (ratioMultiplier === 1) {
+ return {
+ height: maxSide,
+ width: maxSide,
+ }
+ }
+
+ if (ratioMultiplier < 1) {
+ return {
+ width: maxSide * ratioMultiplier,
+ height: maxSide,
+ }
+ }
+
+ return {
+ width: maxSide,
+ height: maxSide / ratioMultiplier,
+ }
+ }
+
+ setAltText(altText: string) {
+ this.altText = altText.trim()
+ }
+
+ // Only compress prior to upload
+ async compress() {
+ for (let i = 10; i > 0; i--) {
+ // Float precision
+ const factor = Math.round(i) / 10
+ const compressed = await ImageManipulator.manipulateAsync(
+ this.cropped?.path ?? this.path,
+ undefined,
+ {
+ compress: factor,
+ base64: true,
+ format: SaveFormat.JPEG,
+ },
+ )
+
+ if (compressed.base64 !== undefined) {
+ const size = getDataUriSize(compressed.base64)
+
+ if (size < MAX_IMAGE_SIZE_IN_BYTES) {
+ runInAction(() => {
+ this.compressed = {
+ mime: 'image/jpeg',
+ path: compressed.uri,
+ size,
+ ...compressed,
+ }
+ })
+ return
+ }
+ }
+ }
+
+ // Compression fails when removing redundant information is not possible.
+ // This can be tested with images that have high variance in noise.
+ throw new Error('Failed to compress image')
+ }
+
+ // Mobile
+ async crop() {
+ try {
+ // NOTE
+ // on ios, react-native-image-crop-picker gives really bad quality
+ // without specifying width and height. on android, however, the
+ // crop stretches incorrectly if you do specify it. these are
+ // both separate bugs in the library. we deal with that by
+ // providing width & height for ios only
+ // -prf
+ const {width, height} = this.getUploadDimensions({
+ width: this.width,
+ height: this.height,
+ })
+
+ const cropped = await openCropper({
+ mediaType: 'photo',
+ path: this.path,
+ freeStyleCropEnabled: true,
+ ...(isIOS ? {width, height} : {}),
+ })
+
+ runInAction(() => {
+ this.cropped = cropped
+ })
+ } catch (err) {
+ logger.error('Failed to crop photo', {message: err})
+ }
+ }
+
+ // Web manipulation
+ async manipulate(
+ attributes: {
+ crop?: ActionCrop['crop']
+ } & ImageManipulationAttributes,
+ ) {
+ let uploadWidth: number | undefined
+ let uploadHeight: number | undefined
+
+ const {aspectRatio, crop, position, scale} = attributes
+ const modifiers = []
+
+ if (this.attributes.flipHorizontal) {
+ modifiers.push({flip: FlipType.Horizontal})
+ }
+
+ if (this.attributes.flipVertical) {
+ modifiers.push({flip: FlipType.Vertical})
+ }
+
+ if (this.attributes.rotate !== undefined) {
+ modifiers.push({rotate: this.attributes.rotate})
+ }
+
+ if (crop !== undefined) {
+ const croppedHeight = crop.height * this.height
+ const croppedWidth = crop.width * this.width
+ modifiers.push({
+ crop: {
+ originX: crop.originX * this.width,
+ originY: crop.originY * this.height,
+ height: croppedHeight,
+ width: croppedWidth,
+ },
+ })
+
+ const uploadDimensions = this.getUploadDimensions(
+ {width: croppedWidth, height: croppedHeight},
+ POST_IMG_MAX,
+ aspectRatio,
+ )
+
+ uploadWidth = uploadDimensions.width
+ uploadHeight = uploadDimensions.height
+ } else {
+ const uploadDimensions = this.getUploadDimensions(
+ {width: this.width, height: this.height},
+ POST_IMG_MAX,
+ aspectRatio,
+ )
+
+ uploadWidth = uploadDimensions.width
+ uploadHeight = uploadDimensions.height
+ }
+
+ if (scale !== undefined) {
+ this.attributes.scale = scale
+ }
+
+ if (position !== undefined) {
+ this.attributes.position = position
+ }
+
+ if (aspectRatio !== undefined) {
+ this.attributes.aspectRatio = aspectRatio
+ }
+
+ const ratioMultiplier =
+ this.ratioMultipliers[this.attributes.aspectRatio ?? '1:1']
+
+ const result = await ImageManipulator.manipulateAsync(
+ this.path,
+ [
+ ...modifiers,
+ {
+ resize:
+ ratioMultiplier > 1 ? {width: uploadWidth} : {height: uploadHeight},
+ },
+ ],
+ {
+ base64: true,
+ format: SaveFormat.JPEG,
+ },
+ )
+
+ runInAction(() => {
+ this.cropped = {
+ mime: 'image/jpeg',
+ path: result.uri,
+ size:
+ result.base64 !== undefined
+ ? getDataUriSize(result.base64)
+ : MAX_IMAGE_SIZE_IN_BYTES + 999, // shouldn't hit this unless manipulation fails
+ ...result,
+ }
+ })
+ }
+
+ resetCropped() {
+ this.manipulate({})
+ }
+
+ previous() {
+ this.cropped = this.prev
+ this.attributes = this.prevAttributes
+ }
+}
diff --git a/src/state/persisted/index.ts b/src/state/persisted/index.ts
index 51d757ad8b..6f4beae2ca 100644
--- a/src/state/persisted/index.ts
+++ b/src/state/persisted/index.ts
@@ -8,7 +8,6 @@ import {
tryStringify,
} from '#/state/persisted/schema'
import {PersistedApi} from './types'
-import {normalizeData} from './util'
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
export {defaults} from '#/state/persisted/schema'
@@ -34,10 +33,10 @@ export async function write(
key: K,
value: Schema[K],
): Promise {
- _state = normalizeData({
+ _state = {
..._state,
[key]: value,
- })
+ }
await writeToStorage(_state)
}
write satisfies PersistedApi['write']
@@ -82,9 +81,6 @@ async function readFromStorage(): Promise {
})
}
if (rawData) {
- const parsed = tryParse(rawData)
- if (parsed) {
- return normalizeData(parsed)
- }
+ return tryParse(rawData)
}
}
diff --git a/src/state/persisted/index.web.ts b/src/state/persisted/index.web.ts
index 4cfc87cdb1..7521776bc0 100644
--- a/src/state/persisted/index.web.ts
+++ b/src/state/persisted/index.web.ts
@@ -9,7 +9,6 @@ import {
tryStringify,
} from '#/state/persisted/schema'
import {PersistedApi} from './types'
-import {normalizeData} from './util'
export type {PersistedAccount, Schema} from '#/state/persisted/schema'
export {defaults} from '#/state/persisted/schema'
@@ -57,10 +56,10 @@ export async function write(
} catch (e) {
// Ignore and go through the normal path.
}
- _state = normalizeData({
+ _state = {
..._state,
[key]: value,
- })
+ }
writeToStorage(_state)
broadcast.postMessage({event: {type: UPDATE_EVENT, key}})
broadcast.postMessage({event: UPDATE_EVENT}) // Backcompat while upgrading
@@ -141,11 +140,9 @@ function readFromStorage(): Schema | undefined {
return lastResult
} else {
const result = tryParse(rawData)
- if (result) {
- lastRawData = rawData
- lastResult = normalizeData(result)
- return lastResult
- }
+ lastRawData = rawData
+ lastResult = result
+ return result
}
}
}
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index 8040179496..331a111a2e 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -1,8 +1,7 @@
import {z} from 'zod'
-import {deviceLanguageCodes, deviceLocales} from '#/locale/deviceLocales'
-import {findSupportedAppLanguage} from '#/locale/helpers'
import {logger} from '#/logger'
+import {deviceLocales} from '#/platform/detection'
import {PlatformInfo} from '../../../modules/expo-bluesky-swiss-army'
const externalEmbedOptions = ['show', 'hide'] as const
@@ -56,39 +55,10 @@ const schema = z.object({
lastEmailConfirm: z.string().optional(),
}),
languagePrefs: z.object({
- /**
- * The target language for translating posts.
- *
- * BCP-47 2-letter language code without region.
- */
- primaryLanguage: z.string(),
- /**
- * The languages the user can read, passed to feeds.
- *
- * BCP-47 2-letter language codes without region.
- */
- contentLanguages: z.array(z.string()),
- /**
- * The language(s) the user is currently posting in, configured within the
- * composer. Multiple languages are psearate by commas.
- *
- * BCP-47 2-letter language code without region.
- */
- postLanguage: z.string(),
- /**
- * The user's post language history, used to pre-populate the post language
- * selector in the composer. Within each value, multiple languages are
- * separated by values.
- *
- * BCP-47 2-letter language codes without region.
- */
+ primaryLanguage: z.string(), // should move to server
+ contentLanguages: z.array(z.string()), // should move to server
+ postLanguage: z.string(), // should move to server
postLanguageHistory: z.array(z.string()),
- /**
- * The language for UI translations in the app.
- *
- * BCP-47 2-letter language code with or without region,
- * to match with {@link AppLanguage}.
- */
appLanguage: z.string(),
}),
requireAltTextEnabled: z.boolean(), // should move to server
@@ -138,17 +108,13 @@ export const defaults: Schema = {
lastEmailConfirm: undefined,
},
languagePrefs: {
- primaryLanguage: deviceLanguageCodes[0] || 'en',
- contentLanguages: deviceLanguageCodes || [],
- postLanguage: deviceLanguageCodes[0] || 'en',
- postLanguageHistory: (deviceLanguageCodes || [])
+ primaryLanguage: deviceLocales[0] || 'en',
+ contentLanguages: deviceLocales || [],
+ postLanguage: deviceLocales[0] || 'en',
+ postLanguageHistory: (deviceLocales || [])
.concat(['en', 'ja', 'pt', 'de'])
.slice(0, 6),
- // try full language tag first, then fallback to language code
- appLanguage: findSupportedAppLanguage([
- deviceLocales.at(0)?.languageTag,
- deviceLanguageCodes[0],
- ]),
+ appLanguage: deviceLocales[0] || 'en',
},
requireAltTextEnabled: false,
largeAltBadgeEnabled: false,
diff --git a/src/state/persisted/util.ts b/src/state/persisted/util.ts
deleted file mode 100644
index 64a8bf9459..0000000000
--- a/src/state/persisted/util.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import {parse} from 'bcp-47'
-
-import {dedupArray} from '#/lib/functions'
-import {logger} from '#/logger'
-import {Schema} from '#/state/persisted/schema'
-
-export function normalizeData(data: Schema) {
- const next = {...data}
-
- /**
- * Normalize language prefs to ensure that these values only contain 2-letter
- * country codes without region.
- */
- try {
- const langPrefs = {...next.languagePrefs}
- langPrefs.primaryLanguage = normalizeLanguageTagToTwoLetterCode(
- langPrefs.primaryLanguage,
- )
- langPrefs.contentLanguages = dedupArray(
- langPrefs.contentLanguages.map(lang =>
- normalizeLanguageTagToTwoLetterCode(lang),
- ),
- )
- langPrefs.postLanguage = langPrefs.postLanguage
- .split(',')
- .map(lang => normalizeLanguageTagToTwoLetterCode(lang))
- .filter(Boolean)
- .join(',')
- langPrefs.postLanguageHistory = dedupArray(
- langPrefs.postLanguageHistory.map(postLanguage => {
- return postLanguage
- .split(',')
- .map(lang => normalizeLanguageTagToTwoLetterCode(lang))
- .filter(Boolean)
- .join(',')
- }),
- )
- next.languagePrefs = langPrefs
- } catch (e: any) {
- logger.error(`persisted state: failed to normalize language prefs`, {
- safeMessage: e.message,
- })
- }
-
- return next
-}
-
-export function normalizeLanguageTagToTwoLetterCode(lang: string) {
- const result = parse(lang).language
- return result ?? lang
-}
diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts
index acc0467715..abf78da3ce 100644
--- a/src/state/queries/actor-autocomplete.ts
+++ b/src/state/queries/actor-autocomplete.ts
@@ -2,7 +2,7 @@ import React from 'react'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query'
-import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
+import {isJustAMute} from '#/lib/moderation'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
@@ -113,10 +113,6 @@ function computeSuggestions({
return items.filter(profile => {
const modui = moderateProfile(profile, moderationOpts).ui('profileList')
const isExactMatch = q && profile.handle.toLowerCase() === q
- return (
- (isExactMatch && !moduiContainsHideableOffense(modui)) ||
- !modui.filter ||
- isJustAMute(modui)
- )
+ return isExactMatch || !modui.filter || isJustAMute(modui)
})
}
diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts
index a251d170ec..e0ee02294e 100644
--- a/src/state/queries/notifications/util.ts
+++ b/src/state/queries/notifications/util.ts
@@ -13,7 +13,6 @@ import {
import {QueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk'
-import {labelIsHideableOffense} from '#/lib/moderation'
import {precacheProfile} from '../profile'
import {FeedNotification, FeedPage, NotificationType} from './types'
@@ -105,10 +104,6 @@ export function shouldFilterNotif(
notif: AppBskyNotificationListNotifications.Notification,
moderationOpts: ModerationOpts | undefined,
): boolean {
- const containsImperative = !!notif.author.labels?.some(labelIsHideableOffense)
- if (containsImperative) {
- return true
- }
if (!moderationOpts) {
return false
}
diff --git a/src/state/session/__tests__/session-test.ts b/src/state/session/__tests__/session-test.ts
index 44c5cf9343..3e22c262cb 100644
--- a/src/state/session/__tests__/session-test.ts
+++ b/src/state/session/__tests__/session-test.ts
@@ -10,10 +10,6 @@ jest.mock('jwt-decode', () => ({
},
}))
-jest.mock('expo-localization', () => ({
- getLocales: () => [],
-}))
-
describe('session', () => {
it('can log in and out', () => {
let state = getInitialState([])
diff --git a/src/state/session/additional-moderation-authorities.ts b/src/state/session/additional-moderation-authorities.ts
deleted file mode 100644
index c594294b2a..0000000000
--- a/src/state/session/additional-moderation-authorities.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import {BskyAgent} from '@atproto/api'
-
-import {logger} from '#/logger'
-import {device} from '#/storage'
-
-export const BR_LABELER = 'did:plc:ekitcvx7uwnauoqy5oest3hm'
-export const ADDITIONAL_LABELERS_MAP: {
- [countryCode: string]: string[]
-} = {
- BR: [BR_LABELER],
-}
-export const ALL_ADDITIONAL_LABELERS = Object.values(
- ADDITIONAL_LABELERS_MAP,
-).flat()
-export const NON_CONFIGURABLE_LABELERS = [BR_LABELER]
-
-export function isNonConfigurableModerationAuthority(did: string) {
- return NON_CONFIGURABLE_LABELERS.includes(did)
-}
-
-export function configureAdditionalModerationAuthorities() {
- const geolocation = device.get(['geolocation'])
- let additionalLabelers: string[] = ALL_ADDITIONAL_LABELERS
-
- if (geolocation?.countryCode) {
- additionalLabelers = ADDITIONAL_LABELERS_MAP[geolocation.countryCode] ?? []
- } else {
- logger.info(`no geolocation, cannot apply mod authorities`)
- }
-
- const appLabelers = Array.from(
- new Set([...BskyAgent.appLabelers, ...additionalLabelers]),
- )
-
- logger.info(`applying mod authorities`, {
- additionalLabelers,
- appLabelers,
- })
-
- BskyAgent.configure({appLabelers})
-}
diff --git a/src/state/session/moderation.ts b/src/state/session/moderation.ts
index 01684fe0ba..d8ded90f69 100644
--- a/src/state/session/moderation.ts
+++ b/src/state/session/moderation.ts
@@ -1,7 +1,6 @@
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
import {IS_TEST_USER} from '#/lib/constants'
-import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities'
import {readLabelers} from './agent-config'
import {SessionAccount} from './types'
@@ -9,7 +8,6 @@ export function configureModerationForGuest() {
// This global mutation is *only* OK because this code is only relevant for testing.
// Don't add any other global behavior here!
switchToBskyAppLabeler()
- configureAdditionalModerationAuthorities()
}
export async function configureModerationForAccount(
@@ -33,8 +31,6 @@ export async function configureModerationForAccount(
// If there are no headers in the storage, we'll not send them on the initial requests.
// If we wanted to fix this, we could block on the preferences query here.
}
-
- configureAdditionalModerationAuthorities()
}
function switchToBskyAppLabeler() {
diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx
index 8e12386bd3..6755ec9a66 100644
--- a/src/state/shell/composer/index.tsx
+++ b/src/state/shell/composer/index.tsx
@@ -9,7 +9,6 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
-import {purgeTemporaryImageFiles} from '#/state/gallery'
import * as Toast from '#/view/com/util/Toast'
export interface ComposerOptsPostRef {
@@ -78,11 +77,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const closeComposer = useNonReactiveCallback(() => {
let wasOpen = !!state
- if (wasOpen) {
- setState(undefined)
- purgeTemporaryImageFiles()
- }
-
+ setState(undefined)
return wasOpen
})
diff --git a/src/storage/index.ts b/src/storage/index.ts
index 7ef226d3aa..4be08170dd 100644
--- a/src/storage/index.ts
+++ b/src/storage/index.ts
@@ -1,6 +1,5 @@
import {MMKV} from 'react-native-mmkv'
-import {IS_DEV} from '#/env'
import {Device} from '#/storage/schema'
export * from '#/storage/schema'
@@ -72,11 +71,4 @@ export class Storage {
*
* `device.set([key], true)`
*/
-export const device = new Storage<[], Device>({id: 'bsky_device'})
-
-if (IS_DEV && typeof window !== 'undefined') {
- // @ts-ignore
- window.bsky_storage = {
- device,
- }
-}
+export const device = new Storage<[], Device>({id: 'device'})
diff --git a/src/storage/schema.ts b/src/storage/schema.ts
index cf410c77de..1a9656fede 100644
--- a/src/storage/schema.ts
+++ b/src/storage/schema.ts
@@ -5,7 +5,4 @@ export type Device = {
fontScale: '-2' | '-1' | '0' | '1' | '2'
fontFamily: 'system' | 'theme'
lastNuxDialog: string | undefined
- geolocation?: {
- countryCode: string | undefined
- }
}
diff --git a/src/style.css b/src/style.css
deleted file mode 100644
index 980d92ef77..0000000000
--- a/src/style.css
+++ /dev/null
@@ -1,355 +0,0 @@
-/**
- * IMPORTANT
- *
- * Some of these styles are duplicated in the `web/index.html` and
- * `bskyweb/templates/base.html` files. Depending on what you're updating, you
- * may need to touch all three. Ask Eric if you aren't sure.
- */
-
-@font-face {
- font-family: 'Inter-Regular';
- src: local('Inter-Regular'),
- url(/assets/fonts/inter/Inter-Regular.otf) format('font/otf');
- font-weight: 400;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: 'Inter-Italic';
- src: local('Inter-Italic'),
- url(/assets/fonts/inter/Inter-Italic.otf) format('font/otf');
- font-weight: 400;
- font-style: italic;
- font-display: swap;
-}
-/*
-@font-face {
- font-family: "Inter-Medium";
- src: local("Inter-Medium"), url(/assets/fonts/inter/Inter-Medium.otf) format("font/otf");
- font-weight: 500;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: "Inter-MediumItalic";
- src: local("Inter-MediumItalic"), url(/assets/fonts/inter/Inter-MediumItalic.otf) format("font/otf");
- font-weight: 500;
- font-style: italic;
- font-display: swap;
-}
-*/
-@font-face {
- font-family: 'Inter-SemiBold';
- src: local('Inter-SemiBold'),
- url(/assets/fonts/inter/Inter-SemiBold.otf) format('font/otf');
- font-weight: 600;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: 'Inter-SemiBoldItalic';
- src: local('Inter-SemiBoldItalic'),
- url(/assets/fonts/inter/Inter-SemiBoldItalic.otf) format('font/otf');
- font-weight: 600;
- font-style: italic;
- font-display: swap;
-}
-/*
-@font-face {
- font-family: "Inter-Bold";
- src: local("Inter-Bold"), url(/assets/fonts/inter/Inter-Bold.otf) format("font/otf");
- font-weight: 700;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: "Inter-BoldItalic";
- src: local("Inter-BoldItalic"), url(/assets/fonts/inter/Inter-BoldItalic.otf) format("font/otf");
- font-weight: 700;
- font-style: italic;
- font-display: swap;
-}
-*/
-@font-face {
- font-family: 'Inter-ExtraBold';
- src: local('Inter-ExtraBold'),
- url(/assets/fonts/inter/Inter-ExtraBold.otf) format('font/otf');
- font-weight: 800;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: 'Inter-ExtraBoldItalic';
- src: local('Inter-ExtraBoldItalic'),
- url(/assets/fonts/inter/Inter-ExtraBoldItalic.otf) format('font/otf');
- font-weight: 800;
- font-style: italic;
- font-display: swap;
-}
-/*
-@font-face {
- font-family: "Inter-Black";
- src: local("Inter-Black"), url(/assets/fonts/inter/Inter-Black.otf) format("font/otf");
- font-weight: 900;
- font-style: normal;
- font-display: swap;
-}
-@font-face {
- font-family: "Inter-BlackItalic";
- src: local("Inter-BlackItalic"), url(/assets/fonts/inter/Inter-BlackItalic.otf) format("font/otf");
- font-weight: 900;
- font-style: italic;
- font-display: swap;
-}
-*/
-
-/**
- * BEGIN STYLES
- *
- * HTML & BODY STYLES IN `web/index.html` and `bskyweb/templates/base.html`
- */
-:root {
- --text: black;
- --background: white;
- --backgroundLight: hsl(211, 20%, 95%);
-}
-@media (prefers-color-scheme: dark) {
- :root {
- color-scheme: dark;
- --text: white;
- --background: black;
- --backgroundLight: hsl(211, 20%, 20%);
- }
-}
-
-html.theme--light {
- --text: black;
- --background: white;
- --backgroundLight: hsl(211, 20%, 95%);
- background-color: white;
-}
-html.theme--dark {
- color-scheme: dark;
- background-color: black;
- --text: white;
- --background: black;
- --backgroundLight: hsl(211, 20%, 20%);
-}
-html.theme--dim {
- color-scheme: dark;
- background-color: hsl(211, 28%, 12%);
- --text: white;
- --background: hsl(211, 20%, 4%);
- --backgroundLight: hsl(211, 20%, 10%);
-}
-
-/* Buttons and inputs have a font set by UA, so we'll have to reset that */
-button,
-input,
-textarea {
- font: inherit;
- line-height: inherit;
-}
-
-/* Remove autofill styles on Webkit */
-input:autofill,
-input:-webkit-autofill,
-input:-webkit-autofill:hover,
-input:-webkit-autofill:focus,
-input:-webkit-autofill:active {
- -webkit-background-clip: text;
- -webkit-text-fill-color: var(--text);
- transition: background-color 5000s ease-in-out 0s;
- box-shadow: inset 0 0 20px 20px var(--background);
- background: var(--background);
- color: var(--text);
-}
-/* Force left-align date/time inputs on iOS mobile */
-input::-webkit-date-and-time-value {
- text-align: left;
-}
-
-/* Remove default link styling */
-a {
- color: inherit;
-}
-a[role='link']:hover {
- text-decoration: underline;
-}
-a[role='link'][data-no-underline='1']:hover {
- text-decoration: none;
-}
-
-/* Styling hacks */
-*[data-word-wrap] {
- word-break: break-word;
-}
-*[data-stable-gutters] {
- scrollbar-gutter: stable both-edges;
-}
-
-/* ProseMirror */
-.ProseMirror {
- font: 18px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
- 'Liberation Sans', Helvetica, Arial, sans-serif;
- min-height: 140px;
-}
-.ProseMirror-dark {
- color: white;
-}
-.ProseMirror p {
- margin: 0;
-}
-.ProseMirror p.is-editor-empty:first-child::before {
- color: #8d8e96;
- content: attr(data-placeholder);
- float: left;
- height: 0;
- pointer-events: none;
-}
-.ProseMirror .mention {
- color: #0085ff;
-}
-.ProseMirror a,
-.ProseMirror .autolink {
- color: #0085ff;
-}
-/* OLLIE: TODO -- this is not accessible */
-/* Remove focus state on inputs */
-.ProseMirror-focused {
- outline: 0;
-}
-textarea:focus,
-input:focus {
- outline: 0;
-}
-.tippy-content .items {
- width: fit-content;
-}
-
-/* Tooltips */
-[data-tooltip] {
- position: relative;
- z-index: 10;
-}
-[data-tooltip]::after {
- content: attr(data-tooltip);
- display: none;
- position: absolute;
- bottom: 0;
- left: 50%;
- transform: translateY(100%) translateY(8px) translateX(-50%);
- padding: 4px 10px;
- border-radius: 10px;
- background: var(--backgroundLight);
- color: var(--text);
- text-align: center;
- white-space: nowrap;
- font-size: 12px;
- z-index: 10;
-}
-[data-tooltip]::before {
- content: '';
- display: none;
- position: absolute;
- border-bottom: 6px solid var(--backgroundLight);
- border-left: 6px solid transparent;
- border-right: 6px solid transparent;
- bottom: 0;
- left: 50%;
- transform: translateY(100%) translateY(2px) translateX(-50%);
- z-index: 10;
-}
-[data-tooltip]:hover::after,
-[data-tooltip]:hover::before {
- display: block;
-}
-
-/* NativeDropdown component */
-.radix-dropdown-item:focus,
-.nativeDropdown-item:focus {
- outline: none;
-}
-
-/* Spinner component */
-@keyframes rotate {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
-}
-.rotate-500ms {
- position: absolute;
- inset: 0;
- animation: rotate 500ms linear infinite;
-}
-
-@keyframes avatarHoverFadeIn {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
-
-@keyframes avatarHoverFadeOut {
- from {
- opacity: 1;
- }
- to {
- opacity: 0;
- }
-}
-
-.force-no-clicks > *,
-.force-no-clicks * {
- pointer-events: none !important;
-}
-
-input[type='range'][orient='vertical'] {
- writing-mode: vertical-lr;
- direction: rtl;
- appearance: slider-vertical;
- width: 16px;
- vertical-align: bottom;
- -webkit-appearance: none;
- appearance: none;
- background: transparent;
- cursor: pointer;
-}
-
-input[type='range'][orient='vertical']::-webkit-slider-runnable-track {
- background: white;
- height: 100%;
- width: 4px;
- border-radius: 4px;
-}
-
-input[type='range'][orient='vertical']::-moz-range-track {
- background: white;
- height: 100%;
- width: 4px;
- border-radius: 4px;
-}
-
-input[type='range']::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- border-radius: 50%;
- background-color: white;
- height: 16px;
- width: 16px;
- margin-left: -6px;
-}
-
-input[type='range'][orient='vertical']::-moz-range-thumb {
- border: none;
- border-radius: 50%;
- background-color: white;
- height: 16px;
- width: 16px;
- margin-left: -6px;
-}
diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
index a18f17612e..8eac1ab82f 100644
--- a/src/view/com/auth/SplashScreen.tsx
+++ b/src/view/com/auth/SplashScreen.tsx
@@ -4,9 +4,9 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
+import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button'
@@ -35,7 +35,8 @@ export const SplashScreen = ({
-
+
What's up?
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
index 1fd62e1d3f..9ffcbfb9df 100644
--- a/src/view/com/auth/SplashScreen.web.tsx
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -4,11 +4,11 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useKawaiiMode} from '#/state/preferences/kawaii'
-import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
+import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button'
@@ -78,7 +78,11 @@ export const SplashScreen = ({
)}
+ style={[
+ a.text_md,
+ a.font_semibold,
+ t.atoms.text_contrast_medium,
+ ]}>
What's up?
diff --git a/src/view/com/auth/server-input/index.tsx b/src/view/com/auth/server-input/index.tsx
index fb69e1d9c7..0d64650ddb 100644
--- a/src/view/com/auth/server-input/index.tsx
+++ b/src/view/com/auth/server-input/index.tsx
@@ -3,15 +3,14 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {BSKY_SERVICE} from '#/lib/constants'
import * as persisted from '#/state/persisted'
+import {BSKY_SERVICE} from 'lib/constants'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
-import {InlineLinkText} from '#/components/Link'
import {P, Text} from '#/components/Typography'
export function ServerInputDialog({
@@ -154,13 +153,9 @@ export function ServerInputDialog({
]}>
Bluesky is an open network where you can choose your hosting
- provider. If you're a developer, you can host your own server.
- {' '}
-
- Learn more.
-
+ provider. Custom hosting is now available in beta for
+ developers.
+
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 3b7cf13851..dfdfb3ebdf 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -44,6 +44,7 @@ import {RichText} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {observer} from 'mobx-react-lite'
import {useAnalytics} from '#/lib/analytics/analytics'
import * as apilib from '#/lib/api/index'
@@ -67,9 +68,9 @@ import {logger} from '#/logger'
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
import {useDialogStateControlContext} from '#/state/dialogs'
import {emitPostCreated} from '#/state/events'
-import {ComposerImage, createInitialImages, pasteImage} from '#/state/gallery'
import {useModalControls} from '#/state/modals'
import {useModals} from '#/state/modals'
+import {GalleryModel} from '#/state/models/media/gallery'
import {useRequireAltTextEnabled} from '#/state/preferences'
import {
toPostLanguages,
@@ -121,14 +122,12 @@ import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import * as Prompt from '#/components/Prompt'
import {Text as NewText} from '#/components/Typography'
-const MAX_IMAGES = 4
-
type CancelRef = {
onPressCancel: () => void
}
type Props = ComposerOpts
-export const ComposePost = ({
+export const ComposePost = observer(function ComposePost({
replyTo,
onPost,
quote: initQuote,
@@ -140,7 +139,7 @@ export const ComposePost = ({
cancelRef,
}: Props & {
cancelRef?: React.RefObject
-}) => {
+}) {
const {currentAccount} = useSession()
const agent = useAgent()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
@@ -213,8 +212,9 @@ export const ComposePost = ({
)
const [postgate, setPostgate] = useState(createPostgateRecord({post: ''}))
- const [images, setImages] = useState(() =>
- createInitialImages(initImageUris),
+ const gallery = useMemo(
+ () => new GalleryModel(initImageUris),
+ [initImageUris],
)
const onClose = useCallback(() => {
closeComposer()
@@ -233,7 +233,7 @@ export const ComposePost = ({
const onPressCancel = useCallback(() => {
if (
graphemeLength > 0 ||
- images.length !== 0 ||
+ !gallery.isEmpty ||
extGif ||
videoUploadState.status !== 'idle'
) {
@@ -246,7 +246,7 @@ export const ComposePost = ({
}, [
extGif,
graphemeLength,
- images.length,
+ gallery.isEmpty,
closeAllDialogs,
discardPromptControl,
onClose,
@@ -299,31 +299,22 @@ export const ComposePost = ({
[extLink, setExtLink],
)
- const onImageAdd = useCallback(
- (next: ComposerImage[]) => {
- setImages(prev => prev.concat(next.slice(0, MAX_IMAGES - prev.length)))
- },
- [setImages],
- )
-
const onPhotoPasted = useCallback(
async (uri: string) => {
track('Composer:PastedPhotos')
if (uri.startsWith('data:video/')) {
selectVideo({uri, type: 'video', height: 0, width: 0})
} else {
- const res = await pasteImage(uri)
- onImageAdd([res])
+ await gallery.paste(uri)
}
},
- [track, selectVideo, onImageAdd],
+ [gallery, track, selectVideo],
)
const isAltTextRequiredAndMissing = useMemo(() => {
if (!requireAltTextEnabled) return false
- if (images.some(img => img.alt === '')) return true
-
+ if (gallery.needsAltText) return true
if (extGif) {
if (!extLink?.meta?.description) return true
@@ -331,7 +322,7 @@ export const ComposePost = ({
if (!parsedAlt.isPreferred) return true
}
return false
- }, [images, extLink, extGif, requireAltTextEnabled])
+ }, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled])
const onPressPublish = React.useCallback(
async (finishedUploading?: boolean) => {
@@ -356,7 +347,7 @@ export const ComposePost = ({
if (
richtext.text.trim().length === 0 &&
- images.length === 0 &&
+ gallery.isEmpty &&
!extLink &&
!quote &&
videoUploadState.status === 'idle'
@@ -377,7 +368,7 @@ export const ComposePost = ({
await apilib.post(agent, {
rawText: richtext.text,
replyTo: replyTo?.uri,
- images,
+ images: gallery.images,
quote,
extLink,
labels,
@@ -414,7 +405,7 @@ export const ComposePost = ({
} catch (e: any) {
logger.error(e, {
message: `Composer: create post failed`,
- hasImages: images.length > 0,
+ hasImages: gallery.size > 0,
})
if (extLink) {
@@ -436,7 +427,7 @@ export const ComposePost = ({
} finally {
if (postUri) {
logEvent('post:create', {
- imageCount: images.length,
+ imageCount: gallery.size,
isReply: replyTo != null,
hasLink: extLink != null,
hasQuote: quote != null,
@@ -445,7 +436,7 @@ export const ComposePost = ({
})
}
track('Create Post', {
- imageCount: images.length,
+ imageCount: gallery.size,
})
if (replyTo && replyTo.uri) track('Post:Reply')
}
@@ -481,7 +472,9 @@ export const ComposePost = ({
agent,
captions,
extLink,
- images,
+ gallery.images,
+ gallery.isEmpty,
+ gallery.size,
graphemeLength,
isAltTextRequiredAndMissing,
isProcessing,
@@ -523,12 +516,12 @@ export const ComposePost = ({
: _(msg`What's up?`)
const canSelectImages =
- images.length < MAX_IMAGES &&
+ gallery.size < 4 &&
!extLink &&
videoUploadState.status === 'idle' &&
!videoUploadState.video
const hasMedia =
- images.length > 0 || Boolean(extLink) || Boolean(videoUploadState.video)
+ gallery.size > 0 || Boolean(extLink) || Boolean(videoUploadState.video)
const onEmojiButtonPress = useCallback(() => {
openEmojiPicker?.(textInput.current?.getCursorPosition())
@@ -723,8 +716,8 @@ export const ComposePost = ({
/>
-
- {images.length === 0 && extLink && (
+
+ {gallery.isEmpty && extLink && (
) : (
-
+
-
+
)
-}
+})
export function useComposerCancelRef() {
return useRef(null)
diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx
index cf4d8c5600..d4ba1f3a86 100644
--- a/src/view/com/composer/ComposerReplyTo.tsx
+++ b/src/view/com/composer/ComposerReplyTo.tsx
@@ -10,13 +10,13 @@ import {
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {ComposerOptsPostRef} from '#/state/shell/composer'
-import {QuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed'
-import {Text} from '#/view/com/util/text/Text'
-import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, useTheme} from '#/alf'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {ComposerOptsPostRef} from 'state/shell/composer'
+import {QuoteEmbed} from 'view/com/util/post-embeds/QuoteEmbed'
+import {Text} from 'view/com/util/text/Text'
+import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
+import {useTheme} from '#/alf'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const t = useTheme()
@@ -122,87 +122,94 @@ function ComposerReplyToImages({
showFull: boolean
}) {
return (
-
- {(images.length === 1 && (
-
- )) ||
- (images.length === 2 && (
-
-
-
-
+
+
+ {(images.length === 1 && (
+
)) ||
- (images.length === 3 && (
-
-
-
-
-
-
-
- )) ||
- (images.length === 4 && (
-
-
+ (images.length === 2 && (
+
-
+ )) ||
+ (images.length === 3 && (
+
-
+
+
+
+
-
- ))}
+ )) ||
+ (images.length === 4 && (
+
+
+
+
+
+
+
+
+
+
+ ))}
+
)
}
@@ -233,7 +240,23 @@ const styles = StyleSheet.create({
borderRadius: 6,
overflow: 'hidden',
marginTop: 2,
- height: 64,
- width: 64,
+ },
+ imagesInner: {
+ gap: 2,
+ },
+ imagesRow: {
+ flexDirection: 'row',
+ },
+ singleImage: {
+ width: 65,
+ height: 65,
+ },
+ doubleImageTall: {
+ width: 32.5,
+ height: 65,
+ },
+ doubleImage: {
+ width: 32.5,
+ height: 32.5,
},
})
diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx
index f48e50cfd7..4801ca0abf 100644
--- a/src/view/com/composer/ExternalEmbed.tsx
+++ b/src/view/com/composer/ExternalEmbed.tsx
@@ -1,10 +1,10 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
-import {ExternalEmbedDraft} from '#/lib/api/index'
-import {Gif} from '#/state/queries/tenor'
-import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
-import {ExternalLinkEmbed} from '#/view/com/util/post-embeds/ExternalLinkEmbed'
+import {ExternalEmbedDraft} from 'lib/api/index'
+import {Gif} from 'state/queries/tenor'
+import {ExternalEmbedRemoveBtn} from 'view/com/composer/ExternalEmbedRemoveBtn'
+import {ExternalLinkEmbed} from 'view/com/util/post-embeds/ExternalLinkEmbed'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
@@ -26,7 +26,7 @@ export const ExternalEmbed = ({
title: link.meta?.title ?? link.uri,
uri: link.uri,
description: link.meta?.description ?? '',
- thumb: link.localThumb?.source.path,
+ thumb: link.localThumb?.path,
},
[link],
)
diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx
index a05607c76c..b7690e1023 100644
--- a/src/view/com/composer/GifAltText.tsx
+++ b/src/view/com/composer/GifAltText.tsx
@@ -43,7 +43,7 @@ export function GifAltText({
title: linkProp.meta?.title ?? linkProp.uri,
uri: linkProp.uri,
description: linkProp.meta?.description ?? '',
- thumb: linkProp.localThumb?.source.path,
+ thumb: linkProp.localThumb?.path,
},
params: parseEmbedPlayerFromUrl(linkProp.uri),
}
@@ -160,7 +160,7 @@ function AltTextInner({
diff --git a/src/view/com/composer/photos/EditImageDialog.tsx b/src/view/com/composer/photos/EditImageDialog.tsx
deleted file mode 100644
index 4263587fd4..0000000000
--- a/src/view/com/composer/photos/EditImageDialog.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react'
-
-import {ComposerImage} from '#/state/gallery'
-import * as Dialog from '#/components/Dialog'
-
-export type EditImageDialogProps = {
- control: Dialog.DialogOuterProps['control']
- image: ComposerImage
- onChange: (next: ComposerImage) => void
-}
-
-export const EditImageDialog = ({}: EditImageDialogProps): React.ReactNode => {
- return null
-}
diff --git a/src/view/com/composer/photos/EditImageDialog.web.tsx b/src/view/com/composer/photos/EditImageDialog.web.tsx
deleted file mode 100644
index 0afb83ed96..0000000000
--- a/src/view/com/composer/photos/EditImageDialog.web.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import 'react-image-crop/dist/ReactCrop.css'
-
-import React from 'react'
-import {View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import ReactCrop, {PercentCrop} from 'react-image-crop'
-
-import {
- ImageSource,
- ImageTransformation,
- manipulateImage,
-} from '#/state/gallery'
-import {atoms as a} from '#/alf'
-import {Button, ButtonText} from '#/components/Button'
-import * as Dialog from '#/components/Dialog'
-import {Text} from '#/components/Typography'
-import {EditImageDialogProps} from './EditImageDialog'
-
-export const EditImageDialog = (props: EditImageDialogProps) => {
- return (
-
-
-
- )
-}
-
-const EditImageInner = ({control, image, onChange}: EditImageDialogProps) => {
- const {_} = useLingui()
-
- const source = image.source
-
- const initialCrop = getInitialCrop(source, image.manips)
- const [crop, setCrop] = React.useState(initialCrop)
-
- const isEmpty = !crop || (crop.width || crop.height) === 0
- const isNew = initialCrop ? true : !isEmpty
-
- const onPressSubmit = React.useCallback(async () => {
- const result = await manipulateImage(image, {
- crop:
- crop && (crop.width || crop.height) !== 0
- ? {
- originX: (crop.x * source.width) / 100,
- originY: (crop.y * source.height) / 100,
- width: (crop.width * source.width) / 100,
- height: (crop.height * source.height) / 100,
- }
- : undefined,
- })
-
- onChange(result)
- control.close()
- }, [crop, image, source, control, onChange])
-
- return (
-
-
-
-
- Edit image
-
-
-
- setCrop(percentCrop)}
- className="ReactCrop--no-animate">
-
-
-
-
-
-
-
- Save
-
-
-
-
- )
-}
-
-const getInitialCrop = (
- source: ImageSource,
- manips: ImageTransformation | undefined,
-): PercentCrop | undefined => {
- const initialArea = manips?.crop
-
- if (initialArea) {
- return {
- unit: '%',
- x: (initialArea.originX / source.width) * 100,
- y: (initialArea.originY / source.height) * 100,
- width: (initialArea.width / source.width) * 100,
- height: (initialArea.height / source.height) * 100,
- }
- }
-}
diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx
index 369f08d745..7ff1b7b9ab 100644
--- a/src/view/com/composer/photos/Gallery.tsx
+++ b/src/view/com/composer/photos/Gallery.tsx
@@ -1,38 +1,29 @@
-import React from 'react'
-import {
- ImageStyle,
- Keyboard,
- LayoutChangeEvent,
- StyleSheet,
- TouchableOpacity,
- View,
- ViewStyle,
-} from 'react-native'
+import React, {useState} from 'react'
+import {ImageStyle, Keyboard, LayoutChangeEvent} from 'react-native'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {Image} from 'expo-image'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {observer} from 'mobx-react-lite'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {Dimensions} from '#/lib/media/types'
-import {colors, s} from '#/lib/styles'
-import {isNative} from '#/platform/detection'
-import {ComposerImage, cropImage} from '#/state/gallery'
-import {Text} from '#/view/com/util/text/Text'
+import {useModalControls} from '#/state/modals'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {Dimensions} from 'lib/media/types'
+import {colors, s} from 'lib/styles'
+import {isNative} from 'platform/detection'
+import {GalleryModel} from 'state/models/media/gallery'
+import {Text} from 'view/com/util/text/Text'
import {useTheme} from '#/alf'
-import * as Dialog from '#/components/Dialog'
-import {EditImageDialog} from './EditImageDialog'
-import {ImageAltTextDialog} from './ImageAltTextDialog'
const IMAGE_GAP = 8
interface GalleryProps {
- images: ComposerImage[]
- onChange: (next: ComposerImage[]) => void
+ gallery: GalleryModel
}
-export let Gallery = (props: GalleryProps): React.ReactNode => {
- const [containerInfo, setContainerInfo] = React.useState()
+export const Gallery = (props: GalleryProps) => {
+ const [containerInfo, setContainerInfo] = useState()
const onLayout = (evt: LayoutChangeEvent) => {
const {width, height} = evt.nativeEvent.layout
@@ -50,200 +41,177 @@ export let Gallery = (props: GalleryProps): React.ReactNode => {
)
}
-Gallery = React.memo(Gallery)
interface GalleryInnerProps extends GalleryProps {
containerInfo: Dimensions
}
-const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => {
+const GalleryInner = observer(function GalleryImpl({
+ gallery,
+ containerInfo,
+}: GalleryInnerProps) {
+ const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
+ const {openModal} = useModalControls()
+ const t = useTheme()
- const {altTextControlStyle, imageControlsStyle, imageStyle} =
- React.useMemo(() => {
- const side =
- images.length === 1
- ? 250
- : (containerInfo.width - IMAGE_GAP * (images.length - 1)) /
- images.length
+ let side: number
- const isOverflow = isMobile && images.length > 2
+ if (gallery.size === 1) {
+ side = 250
+ } else {
+ side = (containerInfo.width - IMAGE_GAP * (gallery.size - 1)) / gallery.size
+ }
- return {
- altTextControlStyle: isOverflow
- ? {left: 4, bottom: 4}
- : !isMobile && images.length < 3
- ? {left: 8, top: 8}
- : {left: 4, top: 4},
- imageControlsStyle: {
- display: 'flex' as const,
- flexDirection: 'row' as const,
- position: 'absolute' as const,
- ...(isOverflow
- ? {top: 4, right: 4, gap: 4}
- : !isMobile && images.length < 3
- ? {top: 8, right: 8, gap: 8}
- : {top: 4, right: 4, gap: 4}),
- zIndex: 1,
- },
- imageStyle: {
- height: side,
- width: side,
- },
+ const imageStyle = {
+ height: side,
+ width: side,
+ }
+
+ const isOverflow = isMobile && gallery.size > 2
+
+ const altTextControlStyle = isOverflow
+ ? {
+ left: 4,
+ bottom: 4,
+ }
+ : !isMobile && gallery.size < 3
+ ? {
+ left: 8,
+ top: 8,
+ }
+ : {
+ left: 4,
+ top: 4,
}
- }, [images.length, containerInfo, isMobile])
- return images.length !== 0 ? (
+ const imageControlsStyle = {
+ display: 'flex' as const,
+ flexDirection: 'row' as const,
+ position: 'absolute' as const,
+ ...(isOverflow
+ ? {
+ top: 4,
+ right: 4,
+ gap: 4,
+ }
+ : !isMobile && gallery.size < 3
+ ? {
+ top: 8,
+ right: 8,
+ gap: 8,
+ }
+ : {
+ top: 4,
+ right: 4,
+ gap: 4,
+ }),
+ zIndex: 1,
+ }
+
+ return !gallery.isEmpty ? (
<>
- {images.map((image, index) => {
- return (
- {
- onChange(
- images.map(i => (i.source === image.source ? next : i)),
- )
+ {gallery.images.map(image => (
+
+ {
+ Keyboard.dismiss()
+ openModal({
+ name: 'alt-text-image',
+ image,
+ })
}}
- onRemove={() => {
- const next = images.slice()
- next.splice(index, 1)
-
- onChange(next)
+ style={[styles.altTextControl, altTextControlStyle]}>
+ {image.altText.length > 0 ? (
+
+ ) : (
+
+ )}
+
+ ALT
+
+
+
+ {
+ if (isNative) {
+ gallery.crop(image)
+ } else {
+ openModal({
+ name: 'edit-image',
+ image,
+ gallery,
+ })
+ }
+ }}
+ style={styles.imageControl}>
+
+
+ gallery.remove(image)}
+ style={styles.imageControl}>
+
+
+
+ {
+ Keyboard.dismiss()
+ openModal({
+ name: 'alt-text-image',
+ image,
+ })
}}
+ style={styles.altTextHiddenRegion}
/>
- )
- })}
+
+
+
+ ))}
>
) : null
-}
-
-type GalleryItemProps = {
- image: ComposerImage
- altTextControlStyle?: ViewStyle
- imageControlsStyle?: ViewStyle
- imageStyle?: ViewStyle
- onChange: (next: ComposerImage) => void
- onRemove: () => void
-}
-
-const GalleryItem = ({
- image,
- altTextControlStyle,
- imageControlsStyle,
- imageStyle,
- onChange,
- onRemove,
-}: GalleryItemProps): React.ReactNode => {
- const {_} = useLingui()
- const t = useTheme()
-
- const altTextControl = Dialog.useDialogControl()
- const editControl = Dialog.useDialogControl()
-
- const onImageEdit = () => {
- if (isNative) {
- cropImage(image).then(next => {
- onChange(next)
- })
- } else {
- editControl.open()
- }
- }
-
- const onAltTextEdit = () => {
- Keyboard.dismiss()
- altTextControl.open()
- }
-
- return (
-
-
- {image.alt.length !== 0 ? (
-
- ) : (
-
- )}
-
- ALT
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
+})
export function AltTextReminder() {
const t = useTheme()
@@ -295,7 +263,7 @@ const styles = StyleSheet.create({
altTextControlLabel: {
color: 'white',
fontSize: 12,
- fontWeight: '600',
+ fontWeight: 'bold',
letterSpacing: 1,
},
altTextHiddenRegion: {
diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx
deleted file mode 100644
index 123e1066a5..0000000000
--- a/src/view/com/composer/photos/ImageAltTextDialog.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import React from 'react'
-import {ImageStyle, useWindowDimensions, View} from 'react-native'
-import {Image} from 'expo-image'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {MAX_ALT_TEXT} from '#/lib/constants'
-import {isWeb} from '#/platform/detection'
-import {ComposerImage} from '#/state/gallery'
-import {atoms as a, useTheme} from '#/alf'
-import {Button, ButtonText} from '#/components/Button'
-import * as Dialog from '#/components/Dialog'
-import * as TextField from '#/components/forms/TextField'
-import {Text} from '#/components/Typography'
-
-type Props = {
- control: Dialog.DialogOuterProps['control']
- image: ComposerImage
- onChange: (next: ComposerImage) => void
-}
-
-export const ImageAltTextDialog = (props: Props): React.ReactNode => {
- return (
-
-
-
-
-
- )
-}
-
-const ImageAltTextInner = ({
- control,
- image,
- onChange,
-}: Props): React.ReactNode => {
- const {_} = useLingui()
- const t = useTheme()
-
- const windim = useWindowDimensions()
-
- const [altText, setAltText] = React.useState(image.alt)
-
- const onPressSubmit = React.useCallback(() => {
- control.close()
- onChange({...image, alt: altText.trim()})
- }, [control, image, altText, onChange])
-
- const imageStyle = React.useMemo(() => {
- const maxWidth = isWeb ? 450 : windim.width
- const source = image.transformed ?? image.source
-
- if (source.height > source.width) {
- return {
- resizeMode: 'contain',
- width: '100%',
- aspectRatio: 1,
- borderRadius: 8,
- }
- }
- return {
- width: '100%',
- height: (maxWidth / source.width) * source.height,
- borderRadius: 8,
- }
- }, [image, windim])
-
- return (
-
-
-
-
-
- Add alt text
-
-
-
-
-
-
-
-
-
-
- Descriptive alt text
-
-
- setAltText(text)}
- value={altText}
- multiline
- numberOfLines={3}
- autoFocus
- />
-
-
- MAX_ALT_TEXT || altText === image.alt}
- size="large"
- color="primary"
- variant="solid"
- onPress={onPressSubmit}>
-
- Save
-
-
-
-
- )
-}
diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx
index 2183ca7902..f1f984103e 100644
--- a/src/view/com/composer/photos/OpenCameraBtn.tsx
+++ b/src/view/com/composer/photos/OpenCameraBtn.tsx
@@ -9,17 +9,17 @@ import {useCameraPermission} from '#/lib/hooks/usePermissions'
import {openCamera} from '#/lib/media/picker'
import {logger} from '#/logger'
import {isMobileWeb, isNative} from '#/platform/detection'
-import {ComposerImage, createComposerImage} from '#/state/gallery'
+import {GalleryModel} from '#/state/models/media/gallery'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera'
type Props = {
+ gallery: GalleryModel
disabled?: boolean
- onAdd: (next: ComposerImage[]) => void
}
-export function OpenCameraBtn({disabled, onAdd}: Props) {
+export function OpenCameraBtn({gallery, disabled}: Props) {
const {track} = useAnalytics()
const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission()
@@ -48,16 +48,13 @@ export function OpenCameraBtn({disabled, onAdd}: Props) {
if (mediaPermissionRes) {
await MediaLibrary.createAssetAsync(img.path)
}
-
- const res = await createComposerImage(img)
-
- onAdd([res])
+ gallery.add(img)
} catch (err: any) {
// ignore
logger.warn('Error using camera', {error: err})
}
}, [
- onAdd,
+ gallery,
track,
requestCameraAccessIfNeeded,
mediaPermissionRes,
diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx
index 95d2df022c..747653fc8d 100644
--- a/src/view/com/composer/photos/SelectPhotoBtn.tsx
+++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx
@@ -5,20 +5,18 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {usePhotoLibraryPermission} from '#/lib/hooks/usePermissions'
-import {openPicker} from '#/lib/media/picker'
import {isNative} from '#/platform/detection'
-import {ComposerImage, createComposerImage} from '#/state/gallery'
+import {GalleryModel} from '#/state/models/media/gallery'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Image_Stroke2_Corner0_Rounded as Image} from '#/components/icons/Image'
type Props = {
- size: number
+ gallery: GalleryModel
disabled?: boolean
- onAdd: (next: ComposerImage[]) => void
}
-export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
+export function SelectPhotoBtn({gallery, disabled}: Props) {
const {track} = useAnalytics()
const {_} = useLingui()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
@@ -31,17 +29,8 @@ export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
return
}
- const images = await openPicker({
- selectionLimit: 4 - size,
- allowsMultipleSelection: true,
- })
-
- const results = await Promise.all(
- images.map(img => createComposerImage(img)),
- )
-
- onAdd(results)
- }, [track, requestPhotoAccessIfNeeded, size, onAdd])
+ gallery.pick()
+ }, [track, requestPhotoAccessIfNeeded, gallery])
return (
(null)
const textInputSelection = useRef({start: 0, end: 0})
const theme = useTheme()
@@ -180,57 +180,25 @@ export const TextInput = forwardRef(function TextInputImpl(
[onChangeText, richtext, setAutocompletePrefix],
)
- const inputTextStyle = React.useMemo(() => {
- const style = normalizeTextStyles(
- [a.text_xl, a.leading_snug, t.atoms.text],
- {
- fontScale: fonts.scaleMultiplier,
- fontFamily: fonts.family,
- flags: {},
- },
- )
-
- /**
- * PasteInput doesn't like `lineHeight`, results in jumpiness
- */
- if (isNative) {
- style.lineHeight = undefined
- }
-
- /*
- * Android impl of `PasteInput` doesn't support the array syntax for `fontVariant`
- */
- if (isAndroid) {
- // @ts-ignore
- style.fontVariant = style.fontVariant
- ? style.fontVariant.join(' ')
- : undefined
- }
- return style
- }, [t, fonts])
-
const textDecorated = useMemo(() => {
let i = 0
return Array.from(richtext.segments()).map(segment => {
return (
-
{segment.text}
-
+
)
})
- }, [t, richtext, inputTextStyle])
+ }, [richtext, pal.link, pal.text])
return (
-
+
{textDecorated}
@@ -256,3 +229,24 @@ export const TextInput = forwardRef(function TextInputImpl(
)
})
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ textInput: {
+ flex: 1,
+ width: '100%',
+ padding: 5,
+ paddingBottom: 20,
+ marginLeft: 8,
+ alignSelf: 'flex-start',
+ },
+ textInputFormatting: {
+ fontSize: 18,
+ letterSpacing: 0.2,
+ fontWeight: '400',
+ // This is broken on ios right now, so don't set it there.
+ lineHeight: isIOS ? undefined : 23.4, // 1.3*16
+ },
+})
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 77f69fa890..3db25746f3 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -13,18 +13,16 @@ import {Text as TiptapText} from '@tiptap/extension-text'
import {generateJSON} from '@tiptap/html'
import {EditorContent, JSONContent, useEditor} from '@tiptap/react'
-import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {usePalette} from '#/lib/hooks/usePalette'
-import {blobToDataUri, isUriImage} from '#/lib/media/util'
import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
+import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
+import {blobToDataUri, isUriImage} from 'lib/media/util'
+import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
import {
LinkFacetMatch,
suggestLinkCardUri,
-} from '#/view/com/composer/text-input/text-input-util'
-import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
-import {atoms as a, useAlf} from '#/alf'
+} from 'view/com/composer/text-input/text-input-util'
import {Portal} from '#/components/Portal'
-import {normalizeTextStyles} from '#/components/Typography'
import {Text} from '../../util/text/Text'
import {createSuggestion} from './web/Autocomplete'
import {Emoji} from './web/EmojiPicker.web'
@@ -60,7 +58,6 @@ export const TextInput = React.forwardRef(function TextInputImpl(
TextInputProps,
ref,
) {
- const {theme: t, fonts} = useAlf()
const autocomplete = useActorAutocompleteFn()
const pal = usePalette('default')
const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
@@ -250,32 +247,13 @@ export const TextInput = React.forwardRef(function TextInputImpl(
},
}))
- const inputStyle = React.useMemo(() => {
- const style = normalizeTextStyles(
- [a.text_lg, a.leading_snug, t.atoms.text],
- {
- fontScale: fonts.scaleMultiplier,
- fontFamily: fonts.family,
- flags: {},
- },
- )
- /*
- * TipTap component isn't a RN View and while it seems to convert
- * `fontSize` to `px`, it doesn't convert `lineHeight`.
- *
- * `lineHeight` should always be defined here, this is defensive.
- */
- style.lineHeight = style.lineHeight
- ? ((style.lineHeight + 'px') as unknown as number)
- : undefined
- return style
- }, [t, fonts])
-
return (
<>
- {/* @ts-ignore inputStyle is fine */}
-
+
{isDropping && (
diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx
index a43e67c044..29b8f0bc65 100644
--- a/src/view/com/composer/text-input/web/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/web/Autocomplete.tsx
@@ -5,20 +5,19 @@ import React, {
useState,
} from 'react'
import {Pressable, StyleSheet, View} from 'react-native'
-import {Trans} from '@lingui/macro'
import {ReactRenderer} from '@tiptap/react'
+import tippy, {Instance as TippyInstance} from 'tippy.js'
import {
- SuggestionKeyDownProps,
SuggestionOptions,
SuggestionProps,
+ SuggestionKeyDownProps,
} from '@tiptap/suggestion'
-import tippy, {Instance as TippyInstance} from 'tippy.js'
-
-import {usePalette} from '#/lib/hooks/usePalette'
import {ActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
-import {Text} from '#/view/com/util/text/Text'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {usePalette} from 'lib/hooks/usePalette'
+import {Text} from 'view/com/util/text/Text'
+import {UserAvatar} from 'view/com/util/UserAvatar'
import {useGrapheme} from '../hooks/useGrapheme'
+import {Trans} from '@lingui/macro'
interface MentionListRef {
onKeyDown: (props: SuggestionKeyDownProps) => boolean
@@ -181,7 +180,7 @@ const MentionList = forwardRef(
size={26}
type={item.associated?.labeler ? 'labeler' : 'user'}
/>
-
+
{displayName}
diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
index 33d4dbc6c1..666473afd9 100644
--- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx
+++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx
@@ -5,9 +5,9 @@ import {AppBskyFeedPostgate} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
import {isNative} from '#/platform/detection'
import {ThreadgateAllowUISetting} from '#/state/queries/threadgate'
+import {useAnalytics} from 'lib/analytics/analytics'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -60,7 +60,7 @@ export function ThreadgateBtn({
undefined)
- .then(thumb => (thumb ? createComposerImage(thumb) : undefined))
- .then(thumb => {
+ .then(localThumb => {
if (aborted) {
return
}
setExtLink({
...extLink,
isLoading: false, // done
- localThumb: thumb,
+ localThumb: localThumb ? new ImageModel(localThumb) : undefined,
})
})
return cleanup
diff --git a/src/view/com/composer/videos/SubtitleDialog.tsx b/src/view/com/composer/videos/SubtitleDialog.tsx
index c07fdfc562..10c2d75642 100644
--- a/src/view/com/composer/videos/SubtitleDialog.tsx
+++ b/src/view/com/composer/videos/SubtitleDialog.tsx
@@ -44,7 +44,7 @@ export function SubtitleDialogBtn(props: Props) {
? _('Opens captions and alt text dialog')
: _('Opens alt text dialog')
}
- size="small"
+ size="xsmall"
color="secondary"
variant="ghost"
onPress={() => {
@@ -169,7 +169,7 @@ function SubtitleDialogInner({
{
diff --git a/src/view/com/composer/videos/SubtitleFilePicker.tsx b/src/view/com/composer/videos/SubtitleFilePicker.tsx
index 44a6b53b6f..856a0eb4fc 100644
--- a/src/view/com/composer/videos/SubtitleFilePicker.tsx
+++ b/src/view/com/composer/videos/SubtitleFilePicker.tsx
@@ -57,7 +57,7 @@ export function SubtitleFilePicker({
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index 3276cf8821..68437c37a0 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -12,10 +12,6 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {s} from '#/lib/styles'
import {logger} from '#/logger'
import {shouldClickOpenNewTab} from '#/platform/urls'
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
@@ -25,8 +21,12 @@ import {
UsePreferencesQueryResponse,
useRemoveFeedMutation,
} from '#/state/queries/preferences'
+import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
+import {usePalette} from 'lib/hooks/usePalette'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {s} from 'lib/styles'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
-import * as Toast from '#/view/com/util/Toast'
+import * as Toast from 'view/com/util/Toast'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import * as Prompt from '#/components/Prompt'
@@ -242,7 +242,7 @@ export function FeedSourceCardLoaded({
-
+
{feed.displayName}
diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx
new file mode 100644
index 0000000000..ba489cde7b
--- /dev/null
+++ b/src/view/com/modals/AltImage.tsx
@@ -0,0 +1,186 @@
+import React, {useCallback, useMemo, useState} from 'react'
+import {
+ ImageStyle,
+ ScrollView as RNScrollView,
+ StyleSheet,
+ TextInput as RNTextInput,
+ TouchableOpacity,
+ useWindowDimensions,
+ View,
+} from 'react-native'
+import {Image} from 'expo-image'
+import {LinearGradient} from 'expo-linear-gradient'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useModalControls} from '#/state/modals'
+import {MAX_ALT_TEXT} from 'lib/constants'
+import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
+import {usePalette} from 'lib/hooks/usePalette'
+import {enforceLen} from 'lib/strings/helpers'
+import {gradients, s} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
+import {isAndroid, isWeb} from 'platform/detection'
+import {ImageModel} from 'state/models/media/image'
+import {Text} from '../util/text/Text'
+import {ScrollView, TextInput} from './util'
+
+export const snapPoints = ['100%']
+
+interface Props {
+ image: ImageModel
+}
+
+export function Component({image}: Props) {
+ const pal = usePalette('default')
+ const theme = useTheme()
+ const {_} = useLingui()
+ const [altText, setAltText] = useState(image.altText)
+ const windim = useWindowDimensions()
+ const {closeModal} = useModalControls()
+ const inputRef = React.useRef(null)
+ const scrollViewRef = React.useRef(null)
+ const keyboardShown = useIsKeyboardVisible()
+
+ // Autofocus hack when we open the modal. We have to wait for the animation to complete first
+ React.useEffect(() => {
+ if (isAndroid) return
+ setTimeout(() => {
+ inputRef.current?.focus()
+ }, 500)
+ }, [])
+
+ // We'd rather be at the bottom here so that we can easily dismiss the modal instead of having to scroll
+ // (especially on android, it acts weird)
+ React.useEffect(() => {
+ if (keyboardShown[0]) {
+ scrollViewRef.current?.scrollToEnd()
+ }
+ }, [keyboardShown])
+
+ const imageStyles = useMemo(() => {
+ const maxWidth = isWeb ? 450 : windim.width
+ if (image.height > image.width) {
+ return {
+ resizeMode: 'contain',
+ width: '100%',
+ aspectRatio: 1,
+ borderRadius: 8,
+ }
+ }
+ return {
+ width: '100%',
+ height: (maxWidth / image.width) * image.height,
+ borderRadius: 8,
+ }
+ }, [image, windim])
+
+ const onUpdate = useCallback(
+ (v: string) => {
+ v = enforceLen(v, MAX_ALT_TEXT)
+ setAltText(v)
+ image.setAltText(v)
+ },
+ [setAltText, image],
+ )
+
+ const onPressSave = useCallback(() => {
+ image.setAltText(altText)
+ closeModal()
+ }, [closeModal, image, altText])
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Done
+
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ scrollContainer: {
+ flex: 1,
+ height: '100%',
+ paddingHorizontal: isWeb ? 0 : 12,
+ paddingVertical: isWeb ? 0 : 24,
+ },
+ scrollInner: {
+ gap: 12,
+ paddingTop: isWeb ? 0 : 12,
+ },
+ imageContainer: {
+ borderRadius: 8,
+ },
+ textArea: {
+ borderWidth: 1,
+ borderRadius: 6,
+ paddingTop: 10,
+ paddingHorizontal: 12,
+ fontSize: 16,
+ height: 100,
+ textAlignVertical: 'top',
+ },
+ button: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ borderRadius: 32,
+ padding: 10,
+ },
+ buttonControls: {
+ gap: 8,
+ paddingBottom: isWeb ? 0 : 50,
+ },
+})
diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx
index 7717f597dd..3088c92a1d 100644
--- a/src/view/com/modals/CreateOrEditList.tsx
+++ b/src/view/com/modals/CreateOrEditList.tsx
@@ -14,22 +14,22 @@ import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {compressIfNeeded} from '#/lib/media/manip'
-import {cleanError, isNetworkError} from '#/lib/strings/errors'
-import {enforceLen} from '#/lib/strings/helpers'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
-import {colors, gradients, s} from '#/lib/styles'
-import {useTheme} from '#/lib/ThemeContext'
import {useModalControls} from '#/state/modals'
import {
useListCreateMutation,
useListMetadataMutation,
} from '#/state/queries/list'
import {useAgent} from '#/state/session'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {compressIfNeeded} from 'lib/media/manip'
+import {cleanError, isNetworkError} from 'lib/strings/errors'
+import {enforceLen} from 'lib/strings/helpers'
+import {colors, gradients, s} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
@@ -359,7 +359,7 @@ export function Component({
const styles = StyleSheet.create({
title: {
textAlign: 'center',
- fontWeight: '600',
+ fontWeight: 'bold',
fontSize: 24,
marginBottom: 18,
},
@@ -373,7 +373,7 @@ const styles = StyleSheet.create({
marginTop: 20,
},
label: {
- fontWeight: '600',
+ fontWeight: 'bold',
},
form: {
paddingHorizontal: 6,
diff --git a/src/view/com/modals/CropImage.web.tsx b/src/view/com/modals/CropImage.web.tsx
deleted file mode 100644
index 41ca306573..0000000000
--- a/src/view/com/modals/CropImage.web.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {Image as RNImage} from 'react-native-image-crop-picker'
-import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
-import {LinearGradient} from 'expo-linear-gradient'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import ReactCrop, {PercentCrop} from 'react-image-crop'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {getDataUriSize} from '#/lib/media/util'
-import {gradients, s} from '#/lib/styles'
-import {useModalControls} from '#/state/modals'
-import {Text} from '#/view/com/util/text/Text'
-
-export const snapPoints = ['0%']
-
-export function Component({
- uri,
- aspect,
- circular,
- onSelect,
-}: {
- uri: string
- aspect?: number
- circular?: boolean
- onSelect: (img?: RNImage) => void
-}) {
- const pal = usePalette('default')
- const {_} = useLingui()
-
- const {closeModal} = useModalControls()
- const {isMobile} = useWebMediaQueries()
-
- const imageRef = React.useRef(null)
- const [crop, setCrop] = React.useState()
-
- const isEmpty = !crop || (crop.width || crop.height) === 0
-
- const onPressCancel = () => {
- onSelect(undefined)
- closeModal()
- }
- const onPressDone = async () => {
- const img = imageRef.current!
-
- const result = await manipulateAsync(
- uri,
- isEmpty
- ? []
- : [
- {
- crop: {
- originX: (crop.x * img.naturalWidth) / 100,
- originY: (crop.y * img.naturalHeight) / 100,
- width: (crop.width * img.naturalWidth) / 100,
- height: (crop.height * img.naturalHeight) / 100,
- },
- },
- ],
- {
- base64: true,
- format: SaveFormat.JPEG,
- },
- )
-
- onSelect({
- path: result.uri,
- mime: 'image/jpeg',
- size: result.base64 !== undefined ? getDataUriSize(result.base64) : 0,
- width: result.width,
- height: result.height,
- })
-
- closeModal()
- }
-
- return (
-
-
- setCrop(percentCrop)}
- circularCrop={circular}>
-
-
-
-
-
-
- Cancel
-
-
-
-
-
-
- Done
-
-
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- cropper: {
- marginLeft: 'auto',
- marginRight: 'auto',
- borderWidth: 1,
- borderRadius: 4,
- overflow: 'hidden',
- alignItems: 'center',
- },
- ctrls: {
- flexDirection: 'row',
- alignItems: 'center',
- marginTop: 10,
- },
- btns: {
- flexDirection: 'row',
- alignItems: 'center',
- marginTop: 10,
- },
- btn: {
- borderRadius: 4,
- paddingVertical: 8,
- paddingHorizontal: 24,
- },
-})
diff --git a/src/view/com/modals/EditImage.tsx b/src/view/com/modals/EditImage.tsx
new file mode 100644
index 0000000000..b39dcd9364
--- /dev/null
+++ b/src/view/com/modals/EditImage.tsx
@@ -0,0 +1,402 @@
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
+import {Pressable, StyleSheet, View} from 'react-native'
+import {useWindowDimensions} from 'react-native'
+import {LinearGradient} from 'expo-linear-gradient'
+import {MaterialIcons} from '@expo/vector-icons'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {Slider} from '@miblanchard/react-native-slider'
+import {observer} from 'mobx-react-lite'
+import ImageEditor, {Position} from 'react-avatar-editor'
+
+import {useModalControls} from '#/state/modals'
+import {MAX_ALT_TEXT} from 'lib/constants'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {RectTallIcon, RectWideIcon, SquareIcon} from 'lib/icons'
+import {enforceLen} from 'lib/strings/helpers'
+import {gradients, s} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
+import {getKeys} from 'lib/type-assertions'
+import {GalleryModel} from 'state/models/media/gallery'
+import {ImageModel} from 'state/models/media/image'
+import {Text} from '../util/text/Text'
+import {TextInput} from './util'
+
+export const snapPoints = ['80%']
+
+const RATIOS = {
+ '4:3': {
+ Icon: RectWideIcon,
+ },
+ '1:1': {
+ Icon: SquareIcon,
+ },
+ '3:4': {
+ Icon: RectTallIcon,
+ },
+ None: {
+ label: 'None',
+ Icon: MaterialIcons,
+ name: 'do-not-disturb-alt',
+ },
+} as const
+
+type AspectRatio = keyof typeof RATIOS
+
+interface Props {
+ image: ImageModel
+ gallery: GalleryModel
+}
+
+export const Component = observer(function EditImageImpl({
+ image,
+ gallery,
+}: Props) {
+ const pal = usePalette('default')
+ const theme = useTheme()
+ const {_} = useLingui()
+ const windowDimensions = useWindowDimensions()
+ const {isMobile} = useWebMediaQueries()
+ const {closeModal} = useModalControls()
+
+ const {
+ aspectRatio,
+ // rotate = 0
+ } = image.attributes
+
+ const editorRef = useRef(null)
+ const [scale, setScale] = useState(image.attributes.scale ?? 1)
+ const [position, setPosition] = useState(
+ image.attributes.position,
+ )
+ const [altText, setAltText] = useState(image?.altText ?? '')
+
+ const onFlipHorizontal = useCallback(() => {
+ image.flipHorizontal()
+ }, [image])
+
+ const onFlipVertical = useCallback(() => {
+ image.flipVertical()
+ }, [image])
+
+ // const onSetRotate = useCallback(
+ // (direction: 'left' | 'right') => {
+ // const rotation = (rotate + 90 * (direction === 'left' ? -1 : 1)) % 360
+ // image.setRotate(rotation)
+ // },
+ // [rotate, image],
+ // )
+
+ const onSetRatio = useCallback(
+ (ratio: AspectRatio) => {
+ image.setRatio(ratio)
+ },
+ [image],
+ )
+
+ const adjustments = useMemo(
+ () => [
+ // {
+ // name: 'rotate-left' as const,
+ // label: 'Rotate left',
+ // onPress: () => {
+ // onSetRotate('left')
+ // },
+ // },
+ // {
+ // name: 'rotate-right' as const,
+ // label: 'Rotate right',
+ // onPress: () => {
+ // onSetRotate('right')
+ // },
+ // },
+ {
+ name: 'flip' as const,
+ label: _(msg`Flip horizontal`),
+ onPress: onFlipHorizontal,
+ },
+ {
+ name: 'flip' as const,
+ label: _(msg`Flip vertically`),
+ onPress: onFlipVertical,
+ },
+ ],
+ [onFlipHorizontal, onFlipVertical, _],
+ )
+
+ useEffect(() => {
+ image.prev = image.cropped
+ image.prevAttributes = image.attributes
+ image.resetCropped()
+ }, [image])
+
+ const onCloseModal = useCallback(() => {
+ closeModal()
+ }, [closeModal])
+
+ const onPressCancel = useCallback(async () => {
+ await gallery.previous(image)
+ onCloseModal()
+ }, [onCloseModal, gallery, image])
+
+ const onPressSave = useCallback(async () => {
+ image.setAltText(altText)
+
+ const crop = editorRef.current?.getCroppingRect()
+
+ await image.manipulate({
+ ...(crop !== undefined
+ ? {
+ crop: {
+ originX: crop.x,
+ originY: crop.y,
+ width: crop.width,
+ height: crop.height,
+ },
+ ...(scale !== 1 ? {scale} : {}),
+ ...(position !== undefined ? {position} : {}),
+ }
+ : {}),
+ })
+
+ image.prev = image.cropped
+ image.prevAttributes = image.attributes
+ onCloseModal()
+ }, [altText, image, position, scale, onCloseModal])
+
+ const getLabelIconSize = useCallback((as: AspectRatio) => {
+ switch (as) {
+ case 'None':
+ return 22
+ case '1:1':
+ return 32
+ default:
+ return 26
+ }
+ }, [])
+
+ if (image.cropped === undefined) {
+ return null
+ }
+
+ const computedWidth =
+ windowDimensions.width > 500 ? 410 : windowDimensions.width - 80
+ const sideLength = isMobile ? computedWidth : 300
+
+ const dimensions = image.getResizedDimensions(aspectRatio, sideLength)
+ const imgContainerStyles = {width: sideLength, height: sideLength}
+
+ const imgControlStyles = {
+ alignItems: 'center' as const,
+ flexDirection: isMobile ? ('column' as const) : ('row' as const),
+ gap: isMobile ? 0 : 5,
+ }
+
+ return (
+
+
+ Edit image
+
+
+
+
+
+
+
+ setScale(Array.isArray(v) ? v[0] : v)
+ }
+ minimumValue={1}
+ maximumValue={3}
+ />
+
+
+ {!isMobile ? (
+
+ Ratios
+
+ ) : null}
+
+ {getKeys(RATIOS).map(ratio => {
+ const {Icon, ...props} = RATIOS[ratio]
+ const labelIconSize = getLabelIconSize(ratio)
+ const isSelected = aspectRatio === ratio
+
+ return (
+ {
+ onSetRatio(ratio)
+ }}
+ accessibilityLabel={ratio}
+ accessibilityHint="">
+
+
+
+ {ratio}
+
+
+ )
+ })}
+
+ {!isMobile ? (
+
+ Transformations
+
+ ) : null}
+
+ {adjustments.map(({label, name, onPress}) => (
+
+
+
+ ))}
+
+
+
+
+
+ Accessibility
+
+ setAltText(enforceLen(text, MAX_ALT_TEXT))}
+ accessibilityLabel={_(msg`Alt text`)}
+ accessibilityHint=""
+ accessibilityLabelledBy="alt-text"
+ />
+
+
+
+
+ Cancel
+
+
+
+
+
+ Done
+
+
+
+
+
+ )
+})
+
+const styles = StyleSheet.create({
+ container: {
+ gap: 18,
+ height: '100%',
+ width: '100%',
+ },
+ subsection: {marginTop: 12},
+ gap18: {gap: 18},
+ title: {
+ fontWeight: 'bold',
+ fontSize: 24,
+ },
+ btns: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ btn: {
+ borderRadius: 4,
+ paddingVertical: 8,
+ paddingHorizontal: 24,
+ },
+ imgControl: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: 40,
+ },
+ imgEditor: {
+ maxWidth: '100%',
+ },
+ imgContainer: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderWidth: 1,
+ borderStyle: 'solid',
+ marginBottom: 4,
+ },
+ flipVertical: {
+ transform: [{rotate: '90deg'}],
+ },
+ flipBtn: {
+ paddingHorizontal: 4,
+ paddingVertical: 8,
+ },
+ textArea: {
+ borderWidth: 1,
+ borderRadius: 6,
+ paddingTop: 10,
+ paddingHorizontal: 12,
+ fontSize: 16,
+ height: 100,
+ textAlignVertical: 'top',
+ },
+ bottomSection: {
+ borderTopWidth: 1,
+ paddingTop: 18,
+ },
+})
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index beea3ca1a8..4b94aeb42f 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -15,18 +15,18 @@ import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {MAX_DESCRIPTION, MAX_DISPLAY_NAME} from '#/lib/constants'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {compressIfNeeded} from '#/lib/media/manip'
-import {cleanError} from '#/lib/strings/errors'
-import {enforceLen} from '#/lib/strings/helpers'
-import {colors, gradients, s} from '#/lib/styles'
-import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useProfileUpdateMutation} from '#/state/queries/profile'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {MAX_DESCRIPTION, MAX_DISPLAY_NAME} from 'lib/constants'
+import {usePalette} from 'lib/hooks/usePalette'
+import {compressIfNeeded} from 'lib/media/manip'
+import {cleanError} from 'lib/strings/errors'
+import {enforceLen} from 'lib/strings/helpers'
+import {colors, gradients, s} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
+import {isWeb} from 'platform/detection'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
@@ -261,12 +261,12 @@ export function Component({
const styles = StyleSheet.create({
title: {
textAlign: 'center',
- fontWeight: '600',
+ fontWeight: 'bold',
fontSize: 24,
marginBottom: 18,
},
label: {
- fontWeight: '600',
+ fontWeight: 'bold',
paddingHorizontal: 4,
paddingBottom: 4,
marginTop: 20,
diff --git a/src/view/com/modals/InAppBrowserConsent.tsx b/src/view/com/modals/InAppBrowserConsent.tsx
index 37b039c605..3fa5159346 100644
--- a/src/view/com/modals/InAppBrowserConsent.tsx
+++ b/src/view/com/modals/InAppBrowserConsent.tsx
@@ -1,18 +1,19 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
+
+import {s} from 'lib/styles'
+import {Text} from '../util/text/Text'
+import {Button} from '../util/forms/Button'
+import {ScrollView} from './util'
+import {usePalette} from 'lib/hooks/usePalette'
+
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {s} from '#/lib/styles'
import {useModalControls} from '#/state/modals'
import {
useOpenLink,
useSetInAppBrowser,
} from '#/state/preferences/in-app-browser'
-import {Button} from '../util/forms/Button'
-import {Text} from '../util/text/Text'
-import {ScrollView} from './util'
export const snapPoints = [350]
@@ -88,7 +89,7 @@ export function Component({href}: {href: string}) {
const styles = StyleSheet.create({
title: {
textAlign: 'center',
- fontWeight: '600',
+ fontWeight: 'bold',
fontSize: 24,
marginBottom: 12,
},
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 90e93821c5..3455e1cdf8 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -3,11 +3,13 @@ import {StyleSheet} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context'
import BottomSheet from '@discord/bottom-sheet/src'
-import {usePalette} from '#/lib/hooks/usePalette'
import {useModalControls, useModals} from '#/state/modals'
+import {usePalette} from 'lib/hooks/usePalette'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as AddAppPassword from './AddAppPasswords'
+import * as AltImageModal from './AltImage'
+import * as EditImageModal from './AltImage'
import * as ChangeEmailModal from './ChangeEmail'
import * as ChangeHandleModal from './ChangeHandle'
import * as ChangePasswordModal from './ChangePassword'
@@ -73,6 +75,12 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'self-label') {
snapPoints = SelfLabelModal.snapPoints
element =
+ } else if (activeModal?.name === 'alt-text-image') {
+ snapPoints = AltImageModal.snapPoints
+ element =
+ } else if (activeModal?.name === 'edit-image') {
+ snapPoints = AltImageModal.snapPoints
+ element =
} else if (activeModal?.name === 'change-handle') {
snapPoints = ChangeHandleModal.snapPoints
element =
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index a2acc23bb9..c4bab6fb18 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -2,18 +2,20 @@ import React from 'react'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
-import {usePalette} from '#/lib/hooks/usePalette'
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import type {Modal as ModalIface} from '#/state/modals'
import {useModalControls, useModals} from '#/state/modals'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import * as AddAppPassword from './AddAppPasswords'
+import * as AltTextImageModal from './AltImage'
import * as ChangeEmailModal from './ChangeEmail'
import * as ChangeHandleModal from './ChangeHandle'
import * as ChangePasswordModal from './ChangePassword'
import * as CreateOrEditListModal from './CreateOrEditList'
-import * as CropImageModal from './CropImage.web'
+import * as CropImageModal from './crop-image/CropImage.web'
import * as DeleteAccountModal from './DeleteAccount'
+import * as EditImageModal from './EditImage'
import * as EditProfileModal from './EditProfile'
import * as InviteCodesModal from './InviteCodes'
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
@@ -52,7 +54,11 @@ function Modal({modal}: {modal: ModalIface}) {
}
const onPressMask = () => {
- if (modal.name === 'crop-image') {
+ if (
+ modal.name === 'crop-image' ||
+ modal.name === 'edit-image' ||
+ modal.name === 'alt-text-image'
+ ) {
return // dont close on mask presses during crop
}
closeModal()
@@ -87,6 +93,10 @@ function Modal({modal}: {modal: ModalIface}) {
element =
} else if (modal.name === 'post-languages-settings') {
element =
+ } else if (modal.name === 'alt-text-image') {
+ element =
+ } else if (modal.name === 'edit-image') {
+ element =
} else if (modal.name === 'verify-email') {
element =
} else if (modal.name === 'change-email') {
diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx
index b0b76644f0..f6db94ed85 100644
--- a/src/view/com/modals/UserAddRemoveLists.tsx
+++ b/src/view/com/modals/UserAddRemoveLists.tsx
@@ -9,12 +9,7 @@ import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {cleanError} from '#/lib/strings/errors'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {s} from '#/lib/styles'
-import {isAndroid, isMobileWeb, isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {
getMembership,
@@ -24,6 +19,11 @@ import {
useListMembershipRemoveMutation,
} from '#/state/queries/list-memberships'
import {useSession} from '#/state/session'
+import {usePalette} from 'lib/hooks/usePalette'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {s} from 'lib/styles'
+import {isAndroid, isMobileWeb, isWeb} from 'platform/detection'
import {MyLists} from '../lists/MyLists'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
@@ -65,27 +65,21 @@ export function Component({
return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}]
}, [pal.border, screenHeight])
- const headerStyles = [
- {
- textAlign: 'center',
- fontWeight: '600',
- fontSize: 20,
- marginBottom: 12,
- paddingHorizontal: 12,
- } as const,
- pal.text,
- ]
-
return (
-
-
- Update{' '}
-
- {displayName}
- {' '}
- in Lists
-
+
+ Update {displayName} in Lists
= {
+ [AspectRatio.Square]: {width: 1000, height: 1000},
+ [AspectRatio.Wide]: {width: 1000, height: 750},
+ [AspectRatio.Tall]: {width: 750, height: 1000},
+}
+
+export const snapPoints = ['0%']
+
+export function Component({
+ uri,
+ dimensions,
+ onSelect,
+}: {
+ uri: string
+ dimensions?: Dimensions
+ onSelect: (img?: RNImage) => void
+}) {
+ const {closeModal} = useModalControls()
+ const pal = usePalette('default')
+ const {_} = useLingui()
+ const defaultAspectStyle = dimensions
+ ? AspectRatio.Custom
+ : AspectRatio.Square
+ const [as, setAs] = React.useState(defaultAspectStyle)
+ const [scale, setScale] = React.useState(1)
+ const editorRef = React.useRef(null)
+ const imageEditorWidth = dimensions ? dimensions.width : DIMS[as].width
+ const imageEditorHeight = dimensions ? dimensions.height : DIMS[as].height
+
+ const doSetAs = (v: AspectRatio) => () => setAs(v)
+
+ const onPressCancel = () => {
+ onSelect(undefined)
+ closeModal()
+ }
+ const onPressDone = () => {
+ const canvas = editorRef.current?.getImageScaledToCanvas()
+ if (canvas) {
+ const dataUri = canvas.toDataURL('image/jpeg')
+ onSelect({
+ path: dataUri,
+ mime: 'image/jpeg',
+ size: getDataUriSize(dataUri),
+ width: imageEditorWidth,
+ height: imageEditorHeight,
+ })
+ } else {
+ onSelect(undefined)
+ }
+ closeModal()
+ }
+
+ let cropperStyle
+ if (as === AspectRatio.Square) {
+ cropperStyle = styles.cropperSquare
+ } else if (as === AspectRatio.Wide) {
+ cropperStyle = styles.cropperWide
+ } else if (as === AspectRatio.Tall) {
+ cropperStyle = styles.cropperTall
+ } else if (as === AspectRatio.Custom) {
+ const cropperDimensions = calculateDimensions(
+ 550,
+ imageEditorHeight,
+ imageEditorWidth,
+ )
+ cropperStyle = {
+ width: cropperDimensions.width,
+ height: cropperDimensions.height,
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ setScale(Array.isArray(v) ? v[0] : v)
+ }
+ minimumValue={1}
+ maximumValue={3}
+ containerStyle={styles.slider}
+ />
+ {as === AspectRatio.Custom ? null : (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ Cancel
+
+
+
+
+
+
+ Done
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ cropper: {
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ borderWidth: 1,
+ borderRadius: 4,
+ overflow: 'hidden',
+ },
+ cropperSquare: {
+ width: 400,
+ height: 400,
+ },
+ cropperWide: {
+ width: 400,
+ height: 300,
+ },
+ cropperTall: {
+ width: 300,
+ height: 400,
+ },
+ imageEditor: {
+ maxWidth: '100%',
+ },
+ ctrls: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 10,
+ },
+ slider: {
+ flex: 1,
+ marginRight: 10,
+ },
+ btns: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 10,
+ },
+ btn: {
+ borderRadius: 4,
+ paddingVertical: 8,
+ paddingHorizontal: 24,
+ },
+})
diff --git a/src/view/com/modals/crop-image/cropImageUtil.ts b/src/view/com/modals/crop-image/cropImageUtil.ts
new file mode 100644
index 0000000000..303d15ba5b
--- /dev/null
+++ b/src/view/com/modals/crop-image/cropImageUtil.ts
@@ -0,0 +1,13 @@
+export const calculateDimensions = (
+ maxWidth: number,
+ originalHeight: number,
+ originalWidth: number,
+) => {
+ const aspectRatio = originalWidth / originalHeight
+ const newHeight = maxWidth / aspectRatio
+ const newWidth = maxWidth
+ return {
+ width: newWidth,
+ height: newHeight,
+ }
+}
diff --git a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
index 360cc0e404..b8c125b65c 100644
--- a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
+++ b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
@@ -1,20 +1,19 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
+import {ScrollView} from '../util'
+import {Text} from '../../util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {deviceLocales} from 'platform/detection'
+import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
+import {LanguageToggle} from './LanguageToggle'
+import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {Trans} from '@lingui/macro'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {deviceLanguageCodes} from '#/locale/deviceLocales'
import {useModalControls} from '#/state/modals'
import {
useLanguagePrefs,
useLanguagePrefsApi,
} from '#/state/preferences/languages'
-import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
-import {Text} from '../../util/text/Text'
-import {ScrollView} from '../util'
-import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
-import {LanguageToggle} from './LanguageToggle'
export const snapPoints = ['100%']
@@ -38,10 +37,10 @@ export function Component({}: {}) {
langs.sort((a, b) => {
const hasA =
langPrefs.contentLanguages.includes(a.code2) ||
- deviceLanguageCodes.includes(a.code2)
+ deviceLocales.includes(a.code2)
const hasB =
langPrefs.contentLanguages.includes(b.code2) ||
- deviceLanguageCodes.includes(b.code2)
+ deviceLocales.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1
return 1
@@ -111,7 +110,7 @@ const styles = StyleSheet.create({
},
title: {
textAlign: 'center',
- fontWeight: '600',
+ fontWeight: 'bold',
fontSize: 24,
marginBottom: 12,
},
diff --git a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
index 2b0eb8cf24..05cfb81156 100644
--- a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
+++ b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
@@ -1,21 +1,20 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
+import {ScrollView} from '../util'
+import {Text} from '../../util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {deviceLocales} from 'platform/detection'
+import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
+import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
+import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {Trans} from '@lingui/macro'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {deviceLanguageCodes} from '#/locale/deviceLocales'
import {useModalControls} from '#/state/modals'
import {
- hasPostLanguage,
useLanguagePrefs,
useLanguagePrefsApi,
+ hasPostLanguage,
} from '#/state/preferences/languages'
-import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
-import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
-import {Text} from '../../util/text/Text'
-import {ScrollView} from '../util'
-import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
export const snapPoints = ['100%']
@@ -39,10 +38,10 @@ export function Component() {
langs.sort((a, b) => {
const hasA =
hasPostLanguage(langPrefs.postLanguage, a.code2) ||
- deviceLanguageCodes.includes(a.code2)
+ deviceLocales.includes(a.code2)
const hasB =
hasPostLanguage(langPrefs.postLanguage, b.code2) ||
- deviceLanguageCodes.includes(b.code2)
+ deviceLocales.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1
return 1
@@ -119,7 +118,7 @@ const styles = StyleSheet.create({
},
title: {
textAlign: 'center',
- fontWeight: '600',
+ fontWeight: 'bold',
fontSize: 24,
marginBottom: 12,
},
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 669fd9bdee..f5ab2608a8 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -28,21 +28,21 @@ import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
-import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {makeProfileLink} from '#/lib/routes/links'
-import {NavigationProp} from '#/lib/routes/types'
-import {forceLTR} from '#/lib/strings/bidi'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {niceDate} from '#/lib/strings/time'
-import {colors, s} from '#/lib/styles'
import {logger} from '#/logger'
-import {isWeb} from '#/platform/detection'
-import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
import {FeedNotification} from '#/state/queries/notifications/feed'
-import {precacheProfile} from '#/state/queries/profile'
-import {useAgent} from '#/state/session'
+import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {usePalette} from 'lib/hooks/usePalette'
+import {makeProfileLink} from 'lib/routes/links'
+import {NavigationProp} from 'lib/routes/types'
+import {forceLTR} from 'lib/strings/bidi'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {niceDate} from 'lib/strings/time'
+import {colors, s} from 'lib/styles'
+import {isWeb} from 'platform/detection'
+import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
+import {precacheProfile} from 'state/queries/profile'
+import {useAgent} from 'state/session'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {
@@ -183,11 +183,7 @@ let FeedItem = ({
key={authors[0].href}
style={[pal.text, s.bold]}
href={authors[0].href}
- text={
-
- {forceLTR(firstAuthorName)}
-
- }
+ text={forceLTR(firstAuthorName)}
disableMismatchWarning
/>
)
@@ -551,7 +547,7 @@ function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileViewBasic}) {
label={_(msg`Say hello!`)}
variant="ghost"
color="primary"
- size="small"
+ size="xsmall"
style={[a.self_center, {marginLeft: 'auto'}]}
disabled={isLoading}
onPress={async () => {
@@ -709,13 +705,12 @@ function ExpandedAuthorsList({
numberOfLines={1}
style={pal.text}
lineHeight={1.2}>
-
- {sanitizeDisplayName(
- author.profile.displayName || author.profile.handle,
- )}
- {' '}
+ {sanitizeDisplayName(
+ author.profile.displayName || author.profile.handle,
+ )}
+
- {sanitizeHandle(author.profile.handle, '@')}
+ {sanitizeHandle(author.profile.handle)}
@@ -732,11 +727,7 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
return (
<>
- {text?.length > 0 && (
-
- {text}
-
- )}
+ {text?.length > 0 && {text}}
(itemRefs.current[i] = node as any)}
+ ref={node => (itemRefs.current[i] = node)}
onLayout={e => onItemLayout(e, i)}
style={styles.item}
hoverStyle={pal.viewLight}
onPress={() => onPressItem(i)}>
-
+
{sanitizeHandle(post.author.handle, '@')}
@@ -558,14 +553,18 @@ let PostThreadItemLoaded = ({
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index ec730a5e16..9033fb96f7 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -163,7 +163,7 @@ function PostInner({
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index fb9cdb065e..7537a46448 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -245,7 +245,7 @@ let FeedItemInner = ({
onBeforePress={onBeforePress}
dataSet={{feedContext}}>
-
+
{isThreadChild && (
- {sanitizeDisplayName(
- reason.by.displayName ||
- sanitizeHandle(reason.by.handle),
- moderation.ui('displayName'),
- )}
-
- }
+ text={sanitizeDisplayName(
+ reason.by.displayName ||
+ sanitizeHandle(reason.by.handle),
+ moderation.ui('displayName'),
+ )}
href={makeProfileLink(reason.by)}
onBeforePress={onOpenReposter}
/>
@@ -345,7 +337,7 @@ let FeedItemInner = ({
- {profile.displayName
- ? sanitizeDisplayName(profile.displayName)
- : sanitizeHandle(profile.handle)}
-
+ profile.displayName
+ ? sanitizeDisplayName(profile.displayName)
+ : sanitizeHandle(profile.handle)
}
/>
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index dc68ee7a17..0920026f60 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -4,9 +4,9 @@ import Svg, {Circle, Line} from 'react-native-svg'
import {AtUri} from '@atproto/api'
import {Trans} from '@lingui/macro'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {makeProfileLink} from '#/lib/routes/links'
import {FeedPostSlice} from '#/state/queries/post-feed'
+import {usePalette} from 'lib/hooks/usePalette'
+import {makeProfileLink} from 'lib/routes/links'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {FeedItem} from './FeedItem'
@@ -146,7 +146,7 @@ const styles = StyleSheet.create({
paddingLeft: 18,
},
viewFullThreadDots: {
- width: 42,
+ width: 52,
alignItems: 'center',
},
})
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index eab8611dd4..fd32e37a42 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -7,17 +7,17 @@ import {
} from '@atproto/api'
import {useQueryClient} from '@tanstack/react-query'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {getModerationCauseKey, isJustAMute} from '#/lib/moderation'
-import {makeProfileLink} from '#/lib/routes/links'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {s} from '#/lib/styles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {Shadow} from '#/state/cache/types'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {precacheProfile} from '#/state/queries/profile'
import {useSession} from '#/state/session'
+import {usePalette} from 'lib/hooks/usePalette'
+import {getModerationCauseKey, isJustAMute} from 'lib/moderation'
+import {makeProfileLink} from 'lib/routes/links'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {s} from 'lib/styles'
+import {precacheProfile} from 'state/queries/profile'
import {atoms as a} from '#/alf'
import {
KnownFollowers,
@@ -103,7 +103,6 @@ export function ProfileCard({
-
+
{sanitizeHandle(profile.handle, '@')}
{profile.description ? (
-
+
{profile.description as string}
) : null}
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index d6995749bf..e07acef281 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -5,16 +5,16 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
-import {BACK_HITSLOP} from '#/lib/constants'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {makeProfileLink} from '#/lib/routes/links'
-import {NavigationProp} from '#/lib/routes/types'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {isNative} from '#/platform/detection'
import {emitSoftReset} from '#/state/events'
import {ImagesLightbox, useLightboxControls} from '#/state/lightbox'
import {useSetDrawerOpen} from '#/state/shell'
+import {BACK_HITSLOP} from 'lib/constants'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {makeProfileLink} from 'lib/routes/links'
+import {NavigationProp} from 'lib/routes/types'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {isNative} from 'platform/detection'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {StarterPack} from '#/components/icons/StarterPack'
import {TextLink} from '../util/Link'
@@ -145,7 +145,7 @@ export function ProfileSubpageHeader({
testID="headerTitle"
type="title-xl"
href={href}
- style={[pal.text, {fontWeight: '600'}]}
+ style={[pal.text, {fontWeight: 'bold'}]}
text={title || ''}
onPress={emitSoftReset}
numberOfLines={4}
diff --git a/src/view/com/util/Html.tsx b/src/view/com/util/Html.tsx
index f77fb16034..2e47194811 100644
--- a/src/view/com/util/Html.tsx
+++ b/src/view/com/util/Html.tsx
@@ -1,17 +1,16 @@
import * as React from 'react'
import {StyleSheet, View} from 'react-native'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useTheme} from 'lib/ThemeContext'
+import {Text} from './text/Text'
+import {TextLink} from './Link'
import {
H1 as ExpoH1,
H2 as ExpoH2,
H3 as ExpoH3,
H4 as ExpoH4,
} from '@expo/html-elements'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {useTheme} from '#/lib/ThemeContext'
-import {TextLink} from './Link'
-import {Text} from './text/Text'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
/**
* These utilities are used to define long documents in an html-like
@@ -167,7 +166,7 @@ const useStyles = () => {
h4: {
marginTop: 0,
marginBottom: 10,
- fontWeight: '600',
+ fontWeight: 'bold',
},
p: {
marginBottom: 10,
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index 6620eb8e28..6e75e88ca6 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -7,9 +7,9 @@ import {
ViewStyle,
} from 'react-native'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {s} from '#/lib/styles'
-import {useTheme} from '#/lib/ThemeContext'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
import {atoms as a, useTheme as useTheme_NEW} from '#/alf'
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
import {
@@ -53,8 +53,8 @@ export function PostLoadingPlaceholder({
return (
onOpenAuthor?: () => void
style?: StyleProp
}
let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
- const t = useTheme()
- const {i18n, _} = useLingui()
+ const {i18n} = useLingui()
+ const pal = usePalette('default')
const displayName = opts.author.displayName || opts.author.handle
const handle = opts.author.handle
const profileLink = makeProfileLink(opts.author)
@@ -49,18 +53,9 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
}, [queryClient, opts.author])
return (
-
+
{opts.showAvatar && (
-
+
{
)}
-
-
+
-
- {forceLTR(
- sanitizeDisplayName(
- displayName,
- opts.moderation?.ui('displayName'),
- ),
- )}
-
-
-
+
-
- {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
-
-
+ style={[pal.textLight, {flexShrink: 4}]}
+ text={NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
+ href={profileLink}
+ onBeforePress={onBeforePressAuthor}
+ anchorNoUnderline
+ />
-
-
- ·
-
-
+ {!isAndroid && (
+
+ ·
+
+ )}
{({timeElapsed}) => (
-
- {timeElapsed}
-
+ accessibilityHint=""
+ href={opts.postHref}
+ onBeforePress={onBeforePressPost}
+ />
)}
@@ -138,3 +117,21 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
}
PostMeta = memo(PostMeta)
export {PostMeta}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'flex-end',
+ paddingBottom: 2,
+ gap: 4,
+ zIndex: 1,
+ flex: 1,
+ },
+ avatar: {
+ alignSelf: 'center',
+ },
+ maxWidth: {
+ flex: isAndroid ? 1 : undefined,
+ flexShrink: isAndroid ? undefined : 1,
+ },
+})
diff --git a/src/view/com/util/PressableWithHover.tsx b/src/view/com/util/PressableWithHover.tsx
index 48659e2295..77276f1843 100644
--- a/src/view/com/util/PressableWithHover.tsx
+++ b/src/view/com/util/PressableWithHover.tsx
@@ -1,35 +1,39 @@
-import React, {forwardRef, PropsWithChildren} from 'react'
+import React, {
+ useState,
+ useCallback,
+ PropsWithChildren,
+ forwardRef,
+ Ref,
+} from 'react'
import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
-import {View} from 'react-native'
-
-import {addStyle} from '#/lib/styles'
-import {useInteractionState} from '#/components/hooks/useInteractionState'
+import {addStyle} from 'lib/styles'
interface PressableWithHover extends PressableProps {
hoverStyle: StyleProp
}
-export const PressableWithHover = forwardRef<
- View,
- PropsWithChildren
->(function PressableWithHoverImpl(
- {children, style, hoverStyle, ...props},
- ref,
+export const PressableWithHover = forwardRef(function PressableWithHoverImpl(
+ {
+ children,
+ style,
+ hoverStyle,
+ ...props
+ }: PropsWithChildren,
+ ref: Ref,
) {
- const {
- state: hovered,
- onIn: onHoverIn,
- onOut: onHoverOut,
- } = useInteractionState()
+ const [isHovering, setIsHovering] = useState(false)
+
+ const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering])
+ const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering])
+ style =
+ typeof style !== 'function' && isHovering
+ ? addStyle(style, hoverStyle)
+ : style
return (
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 76d9d1503e..b2f56c1385 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -8,17 +8,17 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
-import {usePalette} from '#/lib/hooks/usePalette'
+import {logger} from '#/logger'
+import {usePalette} from 'lib/hooks/usePalette'
import {
useCameraPermission,
usePhotoLibraryPermission,
-} from '#/lib/hooks/usePermissions'
-import {makeProfileLink} from '#/lib/routes/links'
-import {colors} from '#/lib/styles'
-import {logger} from '#/logger'
-import {isAndroid, isNative, isWeb} from '#/platform/detection'
-import {precacheProfile} from '#/state/queries/profile'
-import {HighPriorityImage} from '#/view/com/util/images/Image'
+} from 'lib/hooks/usePermissions'
+import {makeProfileLink} from 'lib/routes/links'
+import {colors} from 'lib/styles'
+import {isAndroid, isNative, isWeb} from 'platform/detection'
+import {precacheProfile} from 'state/queries/profile'
+import {HighPriorityImage} from 'view/com/util/images/Image'
import {tokens, useTheme} from '#/alf'
import {
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
@@ -321,8 +321,6 @@ let EditableUserAvatar = ({
height: 1000,
width: 1000,
path: item.path,
- webAspectRatio: 1,
- webCircularCrop: true,
})
onSelectNewAvatar(croppedImage)
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index 13f4081fce..93ea32750d 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -6,16 +6,16 @@ import {ModerationUI} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {usePalette} from '#/lib/hooks/usePalette'
+import {logger} from '#/logger'
+import {usePalette} from 'lib/hooks/usePalette'
import {
useCameraPermission,
usePhotoLibraryPermission,
-} from '#/lib/hooks/usePermissions'
-import {colors} from '#/lib/styles'
-import {useTheme} from '#/lib/ThemeContext'
-import {logger} from '#/logger'
-import {isAndroid, isNative} from '#/platform/detection'
-import {EventStopper} from '#/view/com/util/EventStopper'
+} from 'lib/hooks/usePermissions'
+import {colors} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
+import {isAndroid, isNative} from 'platform/detection'
+import {EventStopper} from 'view/com/util/EventStopper'
import {tokens, useTheme as useAlfTheme} from '#/alf'
import {
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
@@ -72,7 +72,6 @@ export function UserBanner({
path: items[0].path,
width: 3000,
height: 1000,
- webAspectRatio: 3,
}),
)
} catch (e: any) {
diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx
index 8a444d5901..9cb9997f60 100644
--- a/src/view/com/util/UserInfoText.tsx
+++ b/src/view/com/util/UserInfoText.tsx
@@ -1,16 +1,15 @@
import React from 'react'
-import {StyleProp, StyleSheet, TextStyle} from 'react-native'
import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
-
-import {makeProfileLink} from '#/lib/routes/links'
-import {sanitizeDisplayName} from '#/lib/strings/display-names'
-import {sanitizeHandle} from '#/lib/strings/handles'
-import {TypographyVariant} from '#/lib/ThemeContext'
-import {STALE} from '#/state/queries'
-import {useProfileQuery} from '#/state/queries/profile'
+import {StyleProp, StyleSheet, TextStyle} from 'react-native'
import {TextLinkOnWebOnly} from './Link'
-import {LoadingPlaceholder} from './LoadingPlaceholder'
import {Text} from './text/Text'
+import {LoadingPlaceholder} from './LoadingPlaceholder'
+import {TypographyVariant} from 'lib/ThemeContext'
+import {sanitizeDisplayName} from 'lib/strings/display-names'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {makeProfileLink} from 'lib/routes/links'
+import {useProfileQuery} from '#/state/queries/profile'
+import {STALE} from '#/state/queries'
export function UserInfoText({
type = 'md',
@@ -51,15 +50,11 @@ export function UserInfoText({
lineHeight={1.2}
numberOfLines={1}
href={makeProfileLink(profile)}
- text={
-
- {`${prefix || ''}${sanitizeDisplayName(
- typeof profile[attr] === 'string' && profile[attr]
- ? (profile[attr] as string)
- : sanitizeHandle(profile.handle),
- )}`}
-
- }
+ text={`${prefix || ''}${sanitizeDisplayName(
+ typeof profile[attr] === 'string' && profile[attr]
+ ? (profile[attr] as string)
+ : sanitizeHandle(profile.handle),
+ )}`}
/>
)
} else {
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index e5121b350a..ca417034db 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -6,12 +6,12 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {NavigationProp} from '#/lib/routes/types'
import {useSetDrawerOpen} from '#/state/shell'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {NavigationProp} from 'lib/routes/types'
import {useTheme} from '#/alf'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {Text} from './text/Text'
@@ -250,7 +250,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
title: {
- fontWeight: '600',
+ fontWeight: 'bold',
},
subtitle: {
fontSize: 13,
diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx
index 5d8aac81af..ee8e1f47a2 100644
--- a/src/view/com/util/fab/FABInner.tsx
+++ b/src/view/com/util/fab/FABInner.tsx
@@ -4,13 +4,11 @@ import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {LinearGradient} from 'expo-linear-gradient'
-import {useHaptics} from '#/lib/haptics'
import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {clamp} from '#/lib/numbers'
import {gradients} from '#/lib/styles'
import {isWeb} from '#/platform/detection'
-import {useHapticsDisabled} from '#/state/preferences'
import {useInteractionState} from '#/components/hooks/useInteractionState'
export interface FABProps
@@ -19,11 +17,9 @@ export interface FABProps
icon: JSX.Element
}
-export function FABInner({testID, icon, onPress, ...props}: FABProps) {
+export function FABInner({testID, icon, ...props}: FABProps) {
const insets = useSafeAreaInsets()
const {isMobile, isTablet} = useWebMediaQueries()
- const playHaptic = useHaptics()
- const isHapticsDisabled = useHapticsDisabled()
const fabMinimalShellTransform = useMinimalShellFabTransform()
const {
state: pressed,
@@ -46,15 +42,6 @@ export function FABInner({testID, icon, onPress, ...props}: FABProps) {
testID={testID}
onPressIn={onPressIn}
onPressOut={onPressOut}
- onPress={e => {
- playHaptic('Light')
- setTimeout(
- () => {
- onPress?.(e)
- },
- isHapticsDisabled ? 0 : 75,
- )
- }}
{...props}>
>(type, {
primary: {
color: theme.palette.primary.text,
- fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
secondary: {
color: theme.palette.secondary.text,
- fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
inverted: {
color: theme.palette.inverted.text,
- fontWeight: theme.palette.inverted.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
},
'primary-outline': {
color: theme.palette.primary.textInverted,
- fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
'secondary-outline': {
color: theme.palette.secondary.textInverted,
- fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
'primary-light': {
color: theme.palette.primary.textInverted,
- fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
'secondary-light': {
color: theme.palette.secondary.textInverted,
- fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
default: {
color: theme.palette.default.text,
- fontWeight: theme.palette.default.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
},
'default-light': {
color: theme.palette.default.text,
- fontWeight: theme.palette.default.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
},
})
return (
diff --git a/src/view/com/util/forms/ToggleButton.tsx b/src/view/com/util/forms/ToggleButton.tsx
index 706796fc40..c98e846cd3 100644
--- a/src/view/com/util/forms/ToggleButton.tsx
+++ b/src/view/com/util/forms/ToggleButton.tsx
@@ -1,12 +1,11 @@
import React from 'react'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
-
-import {choose} from '#/lib/functions'
-import {colors} from '#/lib/styles'
-import {useTheme} from '#/lib/ThemeContext'
-import {TypographyVariant} from '#/lib/ThemeContext'
import {Text} from '../text/Text'
import {Button, ButtonType} from './Button'
+import {useTheme} from 'lib/ThemeContext'
+import {choose} from 'lib/functions'
+import {colors} from 'lib/styles'
+import {TypographyVariant} from 'lib/ThemeContext'
export function ToggleButton({
testID,
@@ -101,39 +100,39 @@ export function ToggleButton({
const labelStyle = choose>(type, {
primary: {
color: theme.palette.primary.text,
- fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
secondary: {
color: theme.palette.secondary.text,
- fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
inverted: {
color: theme.palette.inverted.text,
- fontWeight: theme.palette.inverted.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
},
'primary-outline': {
color: theme.palette.primary.textInverted,
- fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
'secondary-outline': {
color: theme.palette.secondary.textInverted,
- fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
'primary-light': {
color: theme.palette.primary.textInverted,
- fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
},
'secondary-light': {
color: theme.palette.secondary.textInverted,
- fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
},
default: {
color: theme.palette.default.text,
- fontWeight: theme.palette.default.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
},
'default-light': {
color: theme.palette.default.text,
- fontWeight: theme.palette.default.isLowContrast ? '600' : undefined,
+ fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
},
})
return (
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index a9bfc1c966..9abbe2875f 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -88,7 +88,7 @@ export function ConstrainedImage({
void
-interface Props {
+interface GalleryItemProps {
images: AppBskyEmbedImages.ViewImage[]
index: number
onPress?: EventFunction
onLongPress?: EventFunction
onPressIn?: EventFunction
- imageStyle?: StyleProp
+ imageStyle?: ComponentProps['style']
viewContext?: PostEmbedViewContext
- insetBorderStyle?: StyleProp
}
-export function GalleryItem({
+export const GalleryItem: FC = ({
images,
index,
imageStyle,
@@ -32,8 +31,7 @@ export function GalleryItem({
onPressIn,
onLongPress,
viewContext,
- insetBorderStyle,
-}: Props) {
+}) => {
const t = useTheme()
const {_} = useLingui()
const largeAltBadge = useLargeAltBadgeEnabled()
@@ -49,6 +47,7 @@ export function GalleryItem({
onLongPress={onLongPress ? () => onLongPress(index) : undefined}
style={[
a.flex_1,
+ a.rounded_sm,
a.overflow_hidden,
t.atoms.bg_contrast_25,
imageStyle,
@@ -64,7 +63,7 @@ export function GalleryItem({
accessibilityHint=""
accessibilityIgnoresInvertColors
/>
-
+
{hasAlt && !hideBadges ? (
-
+
@@ -63,18 +54,10 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
return (
-
+
-
+
)
@@ -82,35 +65,15 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
case 3:
return (
-
-
+
+
-
-
+
+
-
-
+
+
@@ -120,51 +83,19 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
return (
<>
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
>
@@ -174,22 +105,3 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
return null
}
}
-
-function noCorners(
- corners: ('topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight')[],
-) {
- const styles: StyleProp[] = []
- if (corners.includes('topLeft')) {
- styles.push({borderTopLeftRadius: 0})
- }
- if (corners.includes('topRight')) {
- styles.push({borderTopRightRadius: 0})
- }
- if (corners.includes('bottomLeft')) {
- styles.push({borderBottomLeftRadius: 0})
- }
- if (corners.includes('bottomRight')) {
- styles.push({borderBottomRightRadius: 0})
- }
- return StyleSheet.flatten(styles)
-}
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
index 0ecdf25b93..8c4928dfcd 100644
--- a/src/view/com/util/post-ctrls/RepostButton.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -157,7 +157,7 @@ let RepostButton = ({
label={_(msg`Cancel quote post`)}
onAccessibilityEscape={close}
onPress={close}
- size="large"
+ size="medium"
variant="solid"
color="primary">
{_(msg`Cancel`)}
diff --git a/src/view/com/util/post-embeds/ExternalGifEmbed.tsx b/src/view/com/util/post-embeds/ExternalGifEmbed.tsx
index 6f1c88dcdf..1f966d7107 100644
--- a/src/view/com/util/post-embeds/ExternalGifEmbed.tsx
+++ b/src/view/com/util/post-embeds/ExternalGifEmbed.tsx
@@ -117,7 +117,7 @@ export function ExternalGifEmbed({
style={[
{height: imageDims.height},
styles.gifContainer,
- a.rounded_md,
+ a.rounded_sm,
a.overflow_hidden,
{
borderBottomLeftRadius: 0,
diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
index 98332c33b0..e6ab86f9c5 100644
--- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
+++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
@@ -5,21 +5,21 @@ import {AppBskyEmbedExternal} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {shareUrl} from '#/lib/sharing'
-import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {shareUrl} from 'lib/sharing'
+import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
import {
getStarterPackOgCard,
parseStarterPackUri,
-} from '#/lib/strings/starter-pack'
-import {toNiceDomain} from '#/lib/strings/url-helpers'
-import {isNative} from '#/platform/detection'
-import {useExternalEmbedsPrefs} from '#/state/preferences'
-import {Link} from '#/view/com/util/Link'
-import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed'
-import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed'
-import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed'
+} from 'lib/strings/starter-pack'
+import {toNiceDomain} from 'lib/strings/url-helpers'
+import {isNative} from 'platform/detection'
+import {useExternalEmbedsPrefs} from 'state/preferences'
+import {Link} from 'view/com/util/Link'
+import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed'
+import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed'
+import {GifEmbed} from 'view/com/util/post-embeds/GifEmbed'
import {atoms as a, useTheme} from '#/alf'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {Text} from '../text/Text'
@@ -59,15 +59,15 @@ export const ExternalLinkEmbed = ({
}
return (
-
+
{imageUri && !embedPlayerParams ? (
{!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && (
-
+
{link.title || link.uri}
)}
{link.description ? (
diff --git a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx
index 6d5eacd1a0..64ea0029fa 100644
--- a/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx
+++ b/src/view/com/util/post-embeds/ExternalPlayerEmbed.tsx
@@ -229,7 +229,7 @@ export function ExternalPlayer({
collapsable={false}
style={[
aspect,
- a.rounded_md,
+ a.rounded_sm,
a.overflow_hidden,
{
borderBottomLeftRadius: 0,
@@ -245,7 +245,7 @@ export function ExternalPlayer({
/>
+
@@ -132,7 +132,7 @@ export function GifEmbed({
@@ -299,6 +293,13 @@ function viewRecordToPostView(
}
const styles = StyleSheet.create({
+ container: {
+ borderRadius: 8,
+ marginTop: 8,
+ paddingVertical: 12,
+ paddingHorizontal: 12,
+ borderWidth: StyleSheet.hairlineWidth,
+ },
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx
index 24802d1882..267b5d1843 100644
--- a/src/view/com/util/post-embeds/VideoEmbed.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbed.tsx
@@ -40,11 +40,11 @@ export function VideoEmbed({embed}: Props) {
diff --git a/src/view/com/util/post-embeds/VideoEmbed.web.tsx b/src/view/com/util/post-embeds/VideoEmbed.web.tsx
index 3180dd99eb..908c06e221 100644
--- a/src/view/com/util/post-embeds/VideoEmbed.web.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbed.web.tsx
@@ -66,8 +66,8 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
{aspectRatio},
{backgroundColor: 'black'},
a.relative,
- a.rounded_md,
- a.mt_xs,
+ a.rounded_sm,
+ a.my_xs,
]}>
(null)
- const videoRef = useRef
(null)
+ const ref = useRef(null)
const [focused, setFocused] = useState(false)
const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false)
const figId = useId()
@@ -31,24 +30,64 @@ export function VideoEmbedInnerWeb({
throw error
}
- const hlsRef = useHLS({
- focused,
- playlist: embed.playlist,
- setHasSubtitleTrack,
- setError,
- videoRef,
- })
+ const hlsRef = useRef(undefined)
+
+ useEffect(() => {
+ if (!ref.current) return
+ if (!Hls.isSupported()) throw new HLSUnsupportedError()
+
+ const hls = new Hls({
+ capLevelToPlayerSize: true,
+ maxMaxBufferLength: 10, // only load 10s ahead
+ // note: the amount buffered is affected by both maxBufferLength and maxBufferSize
+ // it will buffer until it it's greater than *both* of those values
+ // so we use maxMaxBufferLength to set the actual maximum amount of buffering instead
+ })
+ hlsRef.current = hls
+
+ hls.attachMedia(ref.current)
+ hls.loadSource(embed.playlist)
+
+ // initial value, later on it's managed by Controls
+ hls.autoLevelCapping = 0
+
+ hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (_event, data) => {
+ if (data.subtitleTracks.length > 0) {
+ setHasSubtitleTrack(true)
+ }
+ })
+
+ hls.on(Hls.Events.ERROR, (_event, data) => {
+ if (data.fatal) {
+ if (
+ data.details === 'manifestLoadError' &&
+ data.response?.code === 404
+ ) {
+ setError(new VideoNotFoundError())
+ } else {
+ setError(data.error)
+ }
+ }
+ })
+
+ return () => {
+ hlsRef.current = undefined
+ hls.detachMedia()
+ hls.destroy()
+ }
+ }, [embed.playlist])
return (
-
+
@@ -71,7 +110,7 @@ export function VideoEmbedInnerWeb({
)}
void
- setError: (v: Error | null) => void
- videoRef: React.RefObject
-}) {
- const hlsRef = useRef(undefined)
- const [lowQualityFragments, setLowQualityFragments] = useState([])
-
- // purge low quality segments from buffer on next frag change
- const handleFragChange = useNonReactiveCallback(
- (_event: Events.FRAG_CHANGED, {frag}: FragChangedData) => {
- if (!hlsRef.current) return
- const hls = hlsRef.current
-
- if (focused && hls.nextAutoLevel > 0) {
- // if the current quality level goes above 0, flush the low quality segments
- const flushed: Fragment[] = []
-
- for (const lowQualFrag of lowQualityFragments) {
- // avoid if close to the current fragment
- if (Math.abs(frag.start - lowQualFrag.start) < 0.1) {
- continue
- }
-
- hls.trigger(Hls.Events.BUFFER_FLUSHING, {
- startOffset: lowQualFrag.start,
- endOffset: lowQualFrag.end,
- type: 'video',
- })
-
- flushed.push(lowQualFrag)
- }
-
- setLowQualityFragments(prev => prev.filter(f => !flushed.includes(f)))
- }
- },
- )
-
- useEffect(() => {
- if (!videoRef.current) return
- if (!Hls.isSupported()) throw new HLSUnsupportedError()
-
- const hls = new Hls({
- maxMaxBufferLength: 10, // only load 10s ahead
- // note: the amount buffered is affected by both maxBufferLength and maxBufferSize
- // it will buffer until it it's greater than *both* of those values
- // so we use maxMaxBufferLength to set the actual maximum amount of buffering instead
- })
- hlsRef.current = hls
-
- hls.attachMedia(videoRef.current)
- hls.loadSource(playlist)
-
- // initial value, later on it's managed by Controls
- hls.autoLevelCapping = 0
-
- // manually loop, so if we've flushed the first buffer it doesn't get confused
- const abortController = new AbortController()
- const {signal} = abortController
- const videoNode = videoRef.current
- videoNode.addEventListener(
- 'ended',
- function () {
- videoNode.currentTime = 0
- videoNode.play()
- },
- {signal},
- )
-
- hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, (_event, data) => {
- if (data.subtitleTracks.length > 0) {
- setHasSubtitleTrack(true)
- }
- })
-
- hls.on(Hls.Events.FRAG_BUFFERED, (_event, {frag}) => {
- if (frag.level === 0) {
- setLowQualityFragments(prev => [...prev, frag])
- }
- })
-
- hls.on(Hls.Events.ERROR, (_event, data) => {
- if (data.fatal) {
- if (
- data.details === 'manifestLoadError' &&
- data.response?.code === 404
- ) {
- setError(new VideoNotFoundError())
- } else {
- setError(data.error)
- }
- } else {
- console.error(data.error)
- }
- })
-
- hls.on(Hls.Events.FRAG_CHANGED, handleFragChange)
-
- return () => {
- hlsRef.current = undefined
- hls.detachMedia()
- hls.destroy()
- abortController.abort()
- }
- }, [playlist, setError, setHasSubtitleTrack, videoRef, handleFragChange])
-
- return hlsRef
-}
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx
index 8ffe482a8f..36b32a0725 100644
--- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/ControlButton.tsx
@@ -1,8 +1,8 @@
import React from 'react'
import {SvgProps} from 'react-native-svg'
-import {atoms as a, useTheme, web} from '#/alf'
-import {PressableWithHover} from '../../../PressableWithHover'
+import {atoms as a, useTheme} from '#/alf'
+import {Button} from '#/components/Button'
export function ControlButton({
active,
@@ -21,21 +21,19 @@ export function ControlButton({
}) {
const t = useTheme()
return (
-
+ variant="ghost"
+ shape="round"
+ size="medium"
+ style={a.p_2xs}
+ hoverStyle={{backgroundColor: 'rgba(255, 255, 255, 0.1)'}}>
{active ? (
) : (
)}
-
+
)
}
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx
index 2d1427347d..5bd7e0d179 100644
--- a/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/web-controls/VideoControls.tsx
@@ -358,8 +358,9 @@ export function Controls({
style={[
a.flex_1,
a.px_xs,
- a.pb_sm,
- a.gap_sm,
+ a.pt_2xs,
+ a.pb_md,
+ a.gap_md,
a.flex_row,
a.align_center,
]}>
@@ -372,11 +373,7 @@ export function Controls({
onPress={onPressPlayPause}
/>
-
+
{formatTime(currentTime)} / {formatTime(duration)}
{hasSubtitleTrack && (
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index d4982b0e27..b4a6cf8251 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -20,10 +20,10 @@ import {
ModerationDecision,
} from '@atproto/api'
-import {usePalette} from '#/lib/hooks/usePalette'
import {ImagesLightbox, useLightboxControls} from '#/state/lightbox'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard'
+import {usePalette} from 'lib/hooks/usePalette'
+import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {atoms as a, useTheme} from '#/alf'
import * as ListCard from '#/components/ListCard'
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
@@ -138,7 +138,7 @@ export function PostEmbeds({
const image = images[0]
return (
-
+
-
+
)
@@ -247,6 +247,9 @@ function MaybeListCard({view}: {view: AppBskyGraphDefs.ListView}) {
}
const styles = StyleSheet.create({
+ container: {
+ marginTop: 8,
+ },
altContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,
@@ -259,7 +262,7 @@ const styles = StyleSheet.create({
alt: {
color: 'white',
fontSize: 7,
- fontWeight: '600',
+ fontWeight: 'bold',
},
customFeedOuter: {
borderWidth: StyleSheet.hairlineWidth,
diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx
index 3d885480cc..52a45b0e2e 100644
--- a/src/view/com/util/text/Text.tsx
+++ b/src/view/com/util/text/Text.tsx
@@ -2,40 +2,27 @@ import React from 'react'
import {StyleSheet, Text as RNText, TextProps} from 'react-native'
import {UITextView} from 'react-native-uitextview'
-import {lh, s} from '#/lib/styles'
-import {TypographyVariant, useTheme} from '#/lib/ThemeContext'
-import {logger} from '#/logger'
-import {isIOS} from '#/platform/detection'
+import {lh, s} from 'lib/styles'
+import {TypographyVariant, useTheme} from 'lib/ThemeContext'
+import {isIOS, isWeb} from 'platform/detection'
import {applyFonts, useAlf} from '#/alf'
-import {
- childHasEmoji,
- childIsString,
- renderChildrenWithEmoji,
- StringChild,
-} from '#/components/Typography'
-import {IS_DEV} from '#/env'
-export type CustomTextProps = Omit & {
+export type CustomTextProps = TextProps & {
type?: TypographyVariant
lineHeight?: number
title?: string
dataSet?: Record
selectable?: boolean
-} & (
- | {
- emoji: true
- children: StringChild
- }
- | {
- emoji?: false
- children: TextProps['children']
- }
- )
+}
+
+const fontFamilyStyle = {
+ fontFamily:
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif',
+}
export function Text({
type = 'md',
children,
- emoji,
lineHeight,
style,
title,
@@ -48,18 +35,6 @@ export function Text({
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
const {fonts} = useAlf()
- if (IS_DEV) {
- if (!emoji && childHasEmoji(children)) {
- logger.warn(
- `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add '`,
- )
- }
-
- if (emoji && !childIsString(children)) {
- logger.error('Text: when , children can only be strings.')
- }
- }
-
if (selectable && isIOS) {
const flattened = StyleSheet.flatten([
s.black,
@@ -83,7 +58,7 @@ export function Text({
selectable={selectable}
uiTextView
{...props}>
- {isIOS && emoji ? renderChildrenWithEmoji(children) : children}
+ {children}
)
}
@@ -91,6 +66,7 @@ export function Text({
const flattened = StyleSheet.flatten([
s.black,
typography,
+ isWeb && fontFamilyStyle,
lineHeightStyle,
style,
])
@@ -111,7 +87,7 @@ export function Text({
dataSet={Object.assign({tooltip: title}, dataSet || {})}
selectable={selectable}
{...props}>
- {isIOS && emoji ? renderChildrenWithEmoji(children) : children}
+ {children}
)
}
diff --git a/src/view/com/util/text/ThemedText.tsx b/src/view/com/util/text/ThemedText.tsx
new file mode 100644
index 0000000000..2844d273c2
--- /dev/null
+++ b/src/view/com/util/text/ThemedText.tsx
@@ -0,0 +1,80 @@
+import React from 'react'
+import {CustomTextProps, Text} from './Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {addStyle} from 'lib/styles'
+
+export type ThemedTextProps = CustomTextProps & {
+ fg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light'
+ bg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light'
+ border?: 'default' | 'dark' | 'error' | 'inverted' | 'inverted-dark'
+ lineHeight?: number
+}
+
+export function ThemedText({
+ fg,
+ bg,
+ border,
+ style,
+ children,
+ ...props
+}: React.PropsWithChildren) {
+ const pal = usePalette('default')
+ const palInverted = usePalette('inverted')
+ const palError = usePalette('error')
+ switch (fg) {
+ case 'default':
+ style = addStyle(style, pal.text)
+ break
+ case 'light':
+ style = addStyle(style, pal.textLight)
+ break
+ case 'error':
+ style = addStyle(style, {color: palError.colors.background})
+ break
+ case 'inverted':
+ style = addStyle(style, palInverted.text)
+ break
+ case 'inverted-light':
+ style = addStyle(style, palInverted.textLight)
+ break
+ }
+ switch (bg) {
+ case 'default':
+ style = addStyle(style, pal.view)
+ break
+ case 'light':
+ style = addStyle(style, pal.viewLight)
+ break
+ case 'error':
+ style = addStyle(style, palError.view)
+ break
+ case 'inverted':
+ style = addStyle(style, palInverted.view)
+ break
+ case 'inverted-light':
+ style = addStyle(style, palInverted.viewLight)
+ break
+ }
+ switch (border) {
+ case 'default':
+ style = addStyle(style, pal.border)
+ break
+ case 'dark':
+ style = addStyle(style, pal.borderDark)
+ break
+ case 'error':
+ style = addStyle(style, palError.border)
+ break
+ case 'inverted':
+ style = addStyle(style, palInverted.border)
+ break
+ case 'inverted-dark':
+ style = addStyle(style, palInverted.borderDark)
+ break
+ }
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/view/screens/AccessibilitySettings.tsx b/src/view/screens/AccessibilitySettings.tsx
index 158dc8b8da..2992e5c7e9 100644
--- a/src/view/screens/AccessibilitySettings.tsx
+++ b/src/view/screens/AccessibilitySettings.tsx
@@ -69,7 +69,7 @@ export function AccessibilitySettingsScreen({}: Props) {
},
]}>
-
+
Accessibility Settings
diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx
index bd69d7a550..0f27db5229 100644
--- a/src/view/screens/LanguageSettings.tsx
+++ b/src/view/screens/LanguageSettings.tsx
@@ -9,19 +9,19 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
-import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
-import {s} from '#/lib/styles'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {useModalControls} from '#/state/modals'
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
import {useSetMinimalShellMode} from '#/state/shell'
-import {Button} from '#/view/com/util/forms/Button'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {CenteredView} from '#/view/com/util/Views'
+import {APP_LANGUAGES, LANGUAGES} from 'lib/../locale/languages'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {s} from 'lib/styles'
+import {Button} from 'view/com/util/forms/Button'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {CenteredView} from 'view/com/util/Views'
import {Text} from '../com/util/text/Text'
type Props = NativeStackScreenProps
@@ -118,7 +118,7 @@ export function LanguageSettingsScreen(_props: Props) {
color: pal.text.color,
fontSize: 14,
letterSpacing: 0.5,
- fontWeight: '600',
+ fontWeight: '500',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
@@ -128,7 +128,7 @@ export function LanguageSettingsScreen(_props: Props) {
color: pal.text.color,
fontSize: 14,
letterSpacing: 0.5,
- fontWeight: '600',
+ fontWeight: '500',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
@@ -147,7 +147,7 @@ export function LanguageSettingsScreen(_props: Props) {
fontSize: 14,
fontFamily: 'inherit',
letterSpacing: 0.5,
- fontWeight: '600',
+ fontWeight: '500',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
@@ -211,7 +211,7 @@ export function LanguageSettingsScreen(_props: Props) {
color: pal.text.color,
fontSize: 14,
letterSpacing: 0.5,
- fontWeight: '600',
+ fontWeight: '500',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
@@ -221,7 +221,7 @@ export function LanguageSettingsScreen(_props: Props) {
color: pal.text.color,
fontSize: 14,
letterSpacing: 0.5,
- fontWeight: '600',
+ fontWeight: '500',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
@@ -239,7 +239,7 @@ export function LanguageSettingsScreen(_props: Props) {
fontSize: 14,
fontFamily: 'inherit',
letterSpacing: 0.5,
- fontWeight: '600',
+ fontWeight: '500',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx
index d6a86e5143..9daeaba187 100644
--- a/src/view/screens/Lists.tsx
+++ b/src/view/screens/Lists.tsx
@@ -5,17 +5,17 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Trans} from '@lingui/macro'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
-import {NavigationProp} from '#/lib/routes/types'
-import {s} from '#/lib/styles'
import {useModalControls} from '#/state/modals'
import {useSetMinimalShellMode} from '#/state/shell'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {NavigationProp} from 'lib/routes/types'
+import {s} from 'lib/styles'
import {MyLists} from '#/view/com/lists/MyLists'
-import {Button} from '#/view/com/util/forms/Button'
-import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
-import {Text} from '#/view/com/util/text/Text'
+import {Button} from 'view/com/util/forms/Button'
+import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
+import {Text} from 'view/com/util/text/Text'
type Props = NativeStackScreenProps
export function ListsScreen({}: Props) {
@@ -61,7 +61,7 @@ export function ListsScreen({}: Props) {
},
]}>
-
+
User Lists
diff --git a/src/view/screens/ModerationModlists.tsx b/src/view/screens/ModerationModlists.tsx
index 39ba540b49..b7d993acc7 100644
--- a/src/view/screens/ModerationModlists.tsx
+++ b/src/view/screens/ModerationModlists.tsx
@@ -1,21 +1,20 @@
import React from 'react'
import {View} from 'react-native'
-import {AtUri} from '@atproto/api'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Trans} from '@lingui/macro'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
-import {NavigationProp} from '#/lib/routes/types'
-import {s} from '#/lib/styles'
-import {useModalControls} from '#/state/modals'
-import {useSetMinimalShellMode} from '#/state/shell'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {AtUri} from '@atproto/api'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {MyLists} from '#/view/com/lists/MyLists'
-import {Button} from '#/view/com/util/forms/Button'
-import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
-import {Text} from '#/view/com/util/text/Text'
+import {Text} from 'view/com/util/text/Text'
+import {Button} from 'view/com/util/forms/Button'
+import {NavigationProp} from 'lib/routes/types'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
+import {s} from 'lib/styles'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {useModalControls} from '#/state/modals'
+import {Trans} from '@lingui/macro'
type Props = NativeStackScreenProps
export function ModerationModlistsScreen({}: Props) {
@@ -55,7 +54,7 @@ export function ModerationModlistsScreen({}: Props) {
!isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
}>
-
+
Moderation Lists
diff --git a/src/view/screens/PreferencesExternalEmbeds.tsx b/src/view/screens/PreferencesExternalEmbeds.tsx
index 8b3550d6b3..ade7a53d90 100644
--- a/src/view/screens/PreferencesExternalEmbeds.tsx
+++ b/src/view/screens/PreferencesExternalEmbeds.tsx
@@ -3,21 +3,21 @@ import {StyleSheet, View} from 'react-native'
import {Trans} from '@lingui/macro'
import {useFocusEffect} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {
EmbedPlayerSource,
externalEmbedLabels,
} from '#/lib/strings/embed-player'
-import {s} from '#/lib/styles'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {s} from 'lib/styles'
import {
useExternalEmbedsPrefs,
useSetExternalEmbedPref,
-} from '#/state/preferences'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
+} from 'state/preferences'
+import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {atoms as a} from '#/alf'
import {SimpleViewHeader} from '../com/util/SimpleViewHeader'
import {Text} from '../com/util/text/Text'
@@ -50,7 +50,7 @@ export function PreferencesExternalEmbeds({}: Props) {
showBackButton={isTabletOrMobile}
style={[pal.border, a.border_b]}>
-
+
External Media Preferences
diff --git a/src/view/screens/PreferencesFollowingFeed.tsx b/src/view/screens/PreferencesFollowingFeed.tsx
index 085250e3bd..8aa4221e6c 100644
--- a/src/view/screens/PreferencesFollowingFeed.tsx
+++ b/src/view/screens/PreferencesFollowingFeed.tsx
@@ -44,7 +44,7 @@ export function PreferencesFollowingFeed({}: Props) {
showBackButton={isTabletOrMobile}
style={[pal.border, a.border_b]}>
-
+
Following Feed Preferences
diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx
index 7a5a88869d..4a311f91ce 100644
--- a/src/view/screens/PreferencesThreads.tsx
+++ b/src/view/screens/PreferencesThreads.tsx
@@ -47,7 +47,7 @@ export function PreferencesThreads({}: Props) {
showBackButton={isTabletOrMobile}
style={[pal.border, a.border_b]}>
-
+
Thread Preferences
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 810bbff889..5ef6459810 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -16,18 +16,9 @@ import {
useQueryClient,
} from '@tanstack/react-query'
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {useSetTitle} from '#/lib/hooks/useSetTitle'
-import {ComposeIcon2} from '#/lib/icons'
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
-import {combinedDisplayName} from '#/lib/strings/display-names'
import {cleanError} from '#/lib/strings/errors'
-import {isInvalidHandle} from '#/lib/strings/handles'
-import {colors, s} from '#/lib/styles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {listenSoftReset} from '#/state/events'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs'
import {useLabelerInfoQuery} from '#/state/queries/labeler'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {useProfileQuery} from '#/state/queries/profile'
@@ -35,21 +26,29 @@ import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useAgent, useSession} from '#/state/session'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
-import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens'
-import {ProfileLists} from '#/view/com/lists/ProfileLists'
-import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
-import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
-import {FAB} from '#/view/com/util/fab/FAB'
-import {ListRef} from '#/view/com/util/List'
-import {CenteredView} from '#/view/com/util/Views'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {useSetTitle} from 'lib/hooks/useSetTitle'
+import {ComposeIcon2} from 'lib/icons'
+import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {combinedDisplayName} from 'lib/strings/display-names'
+import {isInvalidHandle} from 'lib/strings/handles'
+import {colors, s} from 'lib/styles'
+import {listenSoftReset} from 'state/events'
+import {useActorStarterPacksQuery} from 'state/queries/actor-starter-packs'
+import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileHeader, ProfileHeaderLoading} from '#/screens/Profile/Header'
import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
-import {web} from '#/alf'
import {ScreenHider} from '#/components/moderation/ScreenHider'
import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
import {navigate} from '#/Navigation'
import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder'
+import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
+import {ProfileLists} from '../com/lists/ProfileLists'
+import {ErrorScreen} from '../com/util/error/ErrorScreen'
+import {FAB} from '../com/util/fab/FAB'
+import {ListRef} from '../com/util/List'
+import {CenteredView} from '../com/util/Views'
interface SectionRef {
scrollToTop: () => void
@@ -108,7 +107,7 @@ export function ProfileScreen({route}: Props) {
// Most pushes will happen here, since we will have only placeholder data
if (isLoadingDid || isLoadingProfile || starterPacksQuery.isLoading) {
return (
-
+
)
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index 07d762c0fe..30d16506e0 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -24,18 +24,11 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useAnalytics} from '#/lib/analytics/analytics'
import {createHitslop} from '#/lib/constants'
import {HITSLOP_10} from '#/lib/constants'
-import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {MagnifyingGlassIcon} from '#/lib/icons'
import {makeProfileLink} from '#/lib/routes/links'
import {NavigationProp} from '#/lib/routes/types'
-import {
- NativeStackScreenProps,
- SearchTabNavigatorParams,
-} from '#/lib/routes/types'
import {augmentSearchQuery} from '#/lib/strings/helpers'
-import {useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger'
import {isNative, isWeb} from '#/platform/detection'
import {listenSoftReset} from '#/state/events'
@@ -47,6 +40,13 @@ import {useSearchPostsQuery} from '#/state/queries/search-posts'
import {useSession} from '#/state/session'
import {useSetDrawerOpen} from '#/state/shell'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
+import {useNonReactiveCallback} from 'lib/hooks/useNonReactiveCallback'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {
+ NativeStackScreenProps,
+ SearchTabNavigatorParams,
+} from 'lib/routes/types'
+import {useTheme} from 'lib/ThemeContext'
import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar'
import {Post} from '#/view/com/post/Post'
@@ -414,7 +414,7 @@ let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => {
display: 'flex',
paddingVertical: 12,
paddingHorizontal: 18,
- fontWeight: '600',
+ fontWeight: 'bold',
borderBottomWidth: 1,
},
]}>
@@ -959,7 +959,6 @@ function SearchHistory({
accessibilityIgnoresInvertColors
/>
{profile.displayName || profile.handle}
@@ -1135,7 +1134,7 @@ const styles = StyleSheet.create({
borderRadius: 8,
},
searchHistoryTitle: {
- fontWeight: '600',
+ fontWeight: 'bold',
paddingVertical: 12,
paddingHorizontal: 10,
},
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index 737ca2d28a..fe449fcdbc 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -18,18 +18,6 @@ import {useLingui} from '@lingui/react'
import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info'
-import {STATUS_PAGE_URL} from '#/lib/constants'
-import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
-import {useCustomPalette} from '#/lib/hooks/useCustomPalette'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {HandIcon, HashtagIcon} from '#/lib/icons'
-import {makeProfileLink} from '#/lib/routes/links'
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
-import {NavigationProp} from '#/lib/routes/types'
-import {colors, s} from '#/lib/styles'
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {clearStorage} from '#/state/persisted'
@@ -45,14 +33,26 @@ import {SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useOnboardingDispatch, useSetMinimalShellMode} from '#/state/shell'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
-import {AccountDropdownBtn} from '#/view/com/util/AccountDropdownBtn'
-import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
-import {Link, TextLink} from '#/view/com/util/Link'
-import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
-import {Text} from '#/view/com/util/text/Text'
-import * as Toast from '#/view/com/util/Toast'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {ScrollView} from '#/view/com/util/Views'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {appVersion, BUNDLE_DATE, bundleInfo} from 'lib/app-info'
+import {STATUS_PAGE_URL} from 'lib/constants'
+import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
+import {useCustomPalette} from 'lib/hooks/useCustomPalette'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {HandIcon, HashtagIcon} from 'lib/icons'
+import {makeProfileLink} from 'lib/routes/links'
+import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {NavigationProp} from 'lib/routes/types'
+import {colors, s} from 'lib/styles'
+import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
+import {ToggleButton} from 'view/com/util/forms/ToggleButton'
+import {Link, TextLink} from 'view/com/util/Link'
+import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
+import {Text} from 'view/com/util/text/Text'
+import * as Toast from 'view/com/util/Toast'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {ScrollView} from 'view/com/util/Views'
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
import {atoms as a, useTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
@@ -298,7 +298,7 @@ export function SettingsScreen({}: Props) {
!isMobile && {borderLeftWidth: 1, borderRightWidth: 1},
]}>
-
+
Settings
diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx
index 66040c2e3d..2935103dfb 100644
--- a/src/view/screens/Storybook/Buttons.tsx
+++ b/src/view/screens/Storybook/Buttons.tsx
@@ -9,6 +9,7 @@ import {
ButtonText,
ButtonVariant,
} from '#/components/Button'
+import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/Arrow'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
import {H1} from '#/components/Typography'
@@ -69,115 +70,81 @@ export function Buttons() {
),
)}
+ {/*
+
+ {['gradient_sunset', 'gradient_nordic', 'gradient_bonfire'].map(
+ name => (
+
+
+ Button
+
+
+ Button
+
+
+ ),
+ )}
+
+ */}
-
- Button
-
-
- Button
-
+
+ Link out
+
-
- Button
-
-
- Button
-
+
+ Link out
+
-
+
+ Link xxxxxx
+
+
+
- Button
+ Link out
-
-
-
- Button
-
-
- Button
-
-
-
- Button
-
-
-
-
-
-
-
-
-
-
-
- Button
-
-
- Button
-
-
-
-
-
-
-
-
-
-
-
-
- Button
-
-
- Button
-
-
-
-
-
-
-
+
+ Link out
-
+
-
-
-
-
-
-
-
-
- Submit
-
-
-
-
-
+
@@ -92,17 +91,16 @@ function StorybookInner() {
-
+
-
)
-}
+})
function Providers({
children,
diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx
index 049f35d35d..1c97df9c39 100644
--- a/src/view/shell/Composer.tsx
+++ b/src/view/shell/Composer.tsx
@@ -1,12 +1,17 @@
import React, {useEffect} from 'react'
import {Animated, Easing, StyleSheet, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
-import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useComposerState} from '#/state/shell/composer'
+import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useComposerState} from 'state/shell/composer'
import {ComposePost} from '../com/composer/Composer'
-export function Composer({winHeight}: {winHeight: number}) {
+export const Composer = observer(function ComposerImpl({
+ winHeight,
+}: {
+ winHeight: number
+}) {
const state = useComposerState()
const pal = usePalette('default')
const initInterp = useAnimatedValue(0)
@@ -57,7 +62,7 @@ export function Composer({winHeight}: {winHeight: number}) {
/>
)
-}
+})
const styles = StyleSheet.create({
wrapper: {
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 226fe24966..facead2c1e 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -14,25 +14,25 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native'
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {FEEDBACK_FORM_URL, HELP_DESK_URL} from '#/lib/constants'
-import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {getTabState, TabState} from '#/lib/routes/helpers'
-import {NavigationProp} from '#/lib/routes/types'
-import {colors, s} from '#/lib/styles'
-import {useTheme} from '#/lib/ThemeContext'
-import {isWeb} from '#/platform/detection'
import {emitSoftReset} from '#/state/events'
import {useKawaiiMode} from '#/state/preferences/kawaii'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useProfileQuery} from '#/state/queries/profile'
import {SessionAccount, useSession} from '#/state/session'
import {useSetDrawerOpen} from '#/state/shell'
-import {formatCount} from '#/view/com/util/numeric/format'
-import {Text} from '#/view/com/util/text/Text'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants'
+import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
+import {usePalette} from 'lib/hooks/usePalette'
+import {getTabState, TabState} from 'lib/routes/helpers'
+import {NavigationProp} from 'lib/routes/types'
+import {colors, s} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
+import {isWeb} from 'platform/detection'
import {NavSignupCard} from '#/view/shell/NavSignupCard'
+import {formatCount} from 'view/com/util/numeric/format'
+import {Text} from 'view/com/util/text/Text'
+import {UserAvatar} from 'view/com/util/UserAvatar'
import {atoms as a} from '#/alf'
import {useTheme as useAlfTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -674,7 +674,7 @@ const styles = StyleSheet.create({
},
menuItemCountLabel: {
fontSize: 12,
- fontWeight: '600',
+ fontWeight: 'bold',
fontVariant: ['tabular-nums'],
color: colors.white,
},
diff --git a/src/view/shell/bottom-bar/BottomBarStyles.tsx b/src/view/shell/bottom-bar/BottomBarStyles.tsx
index 9255957cb4..c575e3d9b1 100644
--- a/src/view/shell/bottom-bar/BottomBarStyles.tsx
+++ b/src/view/shell/bottom-bar/BottomBarStyles.tsx
@@ -1,6 +1,6 @@
import {StyleSheet} from 'react-native'
-import {colors} from '#/lib/styles'
+import {colors} from 'lib/styles'
export const styles = StyleSheet.create({
bottomBar: {
@@ -40,7 +40,7 @@ export const styles = StyleSheet.create({
},
notificationCountLabel: {
fontSize: 12,
- fontWeight: '600',
+ fontWeight: 'bold',
color: colors.white,
fontVariant: ['tabular-nums'],
},
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index 2f5f954274..72e34ac469 100644
--- a/src/view/shell/desktop/Feeds.tsx
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -4,13 +4,13 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation, useNavigationState} from '@react-navigation/native'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {getCurrentRoute} from '#/lib/routes/helpers'
-import {NavigationProp} from '#/lib/routes/types'
import {emitSoftReset} from '#/state/events'
import {usePinnedFeedsInfos} from '#/state/queries/feed'
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
-import {TextLink} from '#/view/com/util/Link'
+import {usePalette} from 'lib/hooks/usePalette'
+import {getCurrentRoute} from 'lib/routes/helpers'
+import {NavigationProp} from 'lib/routes/types'
+import {TextLink} from 'view/com/util/Link'
export function DesktopFeeds() {
const pal = usePalette('default')
@@ -81,7 +81,7 @@ function FeedItem({
onPress={onPress}
style={[
current ? pal.text : pal.textLight,
- {letterSpacing: 0.15, fontWeight: current ? '600' : '400'},
+ {letterSpacing: 0.15, fontWeight: current ? '500' : 'normal'},
]}
/>
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 6cceaccd92..ca8073f573 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -12,13 +12,7 @@ import {
useNavigationState,
} from '@react-navigation/native'
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {getCurrentRoute, isStateAtTabRoot, isTab} from '#/lib/routes/helpers'
-import {makeProfileLink} from '#/lib/routes/links'
-import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {isInvalidHandle} from '#/lib/strings/handles'
-import {colors, s} from '#/lib/styles'
import {emitSoftReset} from '#/state/events'
import {useFetchHandle} from '#/state/queries/handle'
import {useUnreadMessageCount} from '#/state/queries/messages/list-converations'
@@ -26,12 +20,18 @@ import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
-import {Link} from '#/view/com/util/Link'
-import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
-import {PressableWithHover} from '#/view/com/util/PressableWithHover'
-import {Text} from '#/view/com/util/text/Text'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {getCurrentRoute, isStateAtTabRoot, isTab} from 'lib/routes/helpers'
+import {makeProfileLink} from 'lib/routes/links'
+import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
+import {colors, s} from 'lib/styles'
import {NavSignupCard} from '#/view/shell/NavSignupCard'
+import {Link} from 'view/com/util/Link'
+import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
+import {PressableWithHover} from 'view/com/util/PressableWithHover'
+import {Text} from 'view/com/util/text/Text'
+import {UserAvatar} from 'view/com/util/UserAvatar'
import {
Bell_Filled_Corner0_Rounded as BellFilled,
Bell_Stroke2_Corner0_Rounded as Bell,
@@ -468,7 +468,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.blue3,
color: colors.white,
fontSize: 12,
- fontWeight: '600',
+ fontWeight: 'bold',
paddingHorizontal: 4,
borderRadius: 6,
},
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index b43dbcce32..1ba2d3f3db 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -16,19 +16,19 @@ import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
-import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links'
-import {NavigationProp} from '#/lib/routes/types'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {s} from '#/lib/styles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
-import {precacheProfile} from '#/state/queries/profile'
-import {SearchInput} from '#/view/com/util/forms/SearchInput'
+import {usePalette} from 'lib/hooks/usePalette'
+import {NavigationProp} from 'lib/routes/types'
+import {precacheProfile} from 'state/queries/profile'
import {Link} from '#/view/com/util/Link'
-import {Text} from '#/view/com/util/text/Text'
import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {SearchInput} from 'view/com/util/forms/SearchInput'
+import {Text} from 'view/com/util/text/Text'
import {atoms as a} from '#/alf'
let SearchLinkCard = ({
@@ -126,7 +126,6 @@ let SearchProfileCard = ({
/>
({
- name,
- params: {
- floatPrecision: 3,
- transformPrecision: 5,
- // minimise diff in ouput from svgomg
- // maybe remove in future? will produce smaller output
- convertToZ: false,
- removeUseless: false,
- }
- })),
- {
- name: 'addTrailingWhitespace',
- fn() {
- return {
- root: {
- exit (root) {
- root.children.push({ type: 'text', value: '\n' })
- return root
- }
- }
- }
- }
- }]
-};
diff --git a/web/index.html b/web/index.html
index 71e5ac0892..8902f7b6e0 100644
--- a/web/index.html
+++ b/web/index.html
@@ -17,70 +17,295 @@
%WEB_TITLE%
-
-
-
-
-
-
-
-
-
-
-
@@ -137,7 +362,7 @@
-
+
diff --git a/yarn.lock b/yarn.lock
index 17fe862372..98479ba44c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2570,7 +2570,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.22.5"
-"@babel/plugin-transform-runtime@^7.0.0", "@babel/plugin-transform-runtime@^7.16.4":
+"@babel/plugin-transform-runtime@^7.0.0", "@babel/plugin-transform-runtime@^7.12.1", "@babel/plugin-transform-runtime@^7.16.4":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.10.tgz#89eda6daf1d3af6f36fb368766553054c8d7cd46"
integrity sha512-RchI7HePu1eu0CYNKHHHQdfenZcM4nz8rew5B1VWqeRKdcwW5aQ5HeG9eTUbWiAS1UrmHVLmoxTWHt3iLD/NhA==
@@ -2983,6 +2983,11 @@
"@babel/helper-validator-identifier" "^7.24.6"
to-fast-properties "^2.0.0"
+"@bam.tech/react-native-image-resizer@^3.0.4":
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/@bam.tech/react-native-image-resizer/-/react-native-image-resizer-3.0.5.tgz#6661ba020de156268f73bdc92fbb93ef86f88a13"
+ integrity sha512-u5QGUQGGVZiVCJ786k9/kd7pPRZ6eYfJCYO18myVCH8FbVI7J8b5GT2Svjj2x808DlWeqfaZOOzxPqo27XYvrQ==
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -8257,6 +8262,13 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
+"@types/react-avatar-editor@^13.0.0":
+ version "13.0.0"
+ resolved "https://registry.yarnpkg.com/@types/react-avatar-editor/-/react-avatar-editor-13.0.0.tgz#5963e16c931746c47e478d669dd72d388b427393"
+ integrity sha512-5ymOayy6mfT35xTqzni7UjXvCNEg8/pH4pI5RenITp9PBc02KGTYjSV1WboXiQDYSh5KomLT0ngBLEAIhV1QoQ==
+ dependencies:
+ "@types/react" "*"
+
"@types/react-dom@^18.2.18":
version "18.2.18"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
@@ -9535,7 +9547,7 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-base-64@0.1.0:
+base-64@0.1.0, base-64@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
@@ -9560,15 +9572,6 @@ bcp-47-match@^2.0.3:
resolved "https://registry.yarnpkg.com/bcp-47-match/-/bcp-47-match-2.0.3.tgz#603226f6e5d3914a581408be33b28a53144b09d0"
integrity sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==
-bcp-47@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/bcp-47/-/bcp-47-2.1.0.tgz#7e80734c3338fe8320894981dccf4968c3092df6"
- integrity sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==
- dependencies:
- is-alphabetical "^2.0.0"
- is-alphanumerical "^2.0.0"
- is-decimal "^2.0.0"
-
better-opn@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-3.0.2.tgz#f96f35deaaf8f34144a4102651babcf00d1d8817"
@@ -10699,22 +10702,6 @@ css-tree@^1.1.2, css-tree@^1.1.3:
mdn-data "2.0.14"
source-map "^0.6.1"
-css-tree@^2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20"
- integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==
- dependencies:
- mdn-data "2.0.30"
- source-map-js "^1.0.1"
-
-css-tree@~2.2.0:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032"
- integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==
- dependencies:
- mdn-data "2.0.28"
- source-map-js "^1.0.1"
-
css-what@^3.2.1:
version "3.4.2"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
@@ -10791,13 +10778,6 @@ csso@^4.0.2, csso@^4.2.0:
dependencies:
css-tree "^1.1.2"
-csso@^5.0.5:
- version "5.0.5"
- resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6"
- integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==
- dependencies:
- css-tree "~2.2.0"
-
cssom@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
@@ -11371,11 +11351,6 @@ emoji-mart@^5.5.2:
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af"
integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==
-emoji-regex@^10.4.0:
- version "10.4.0"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4"
- integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
-
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -13918,19 +13893,6 @@ ipaddr.js@^2.1.0:
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8"
integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==
-is-alphabetical@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b"
- integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==
-
-is-alphanumerical@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875"
- integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==
- dependencies:
- is-alphabetical "^2.0.0"
- is-decimal "^2.0.0"
-
is-arguments@^1.0.4:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
@@ -14025,11 +13987,6 @@ is-date-object@^1.0.1, is-date-object@^1.0.5:
dependencies:
has-tostringtag "^1.0.0"
-is-decimal@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7"
- integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==
-
is-directory@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
@@ -16272,16 +16229,6 @@ mdn-data@2.0.14:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
-mdn-data@2.0.28:
- version "2.0.28"
- resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba"
- integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==
-
-mdn-data@2.0.30:
- version "2.0.30"
- resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
- integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
-
mdn-data@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
@@ -16748,6 +16695,21 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+mobx-react-lite@^3.4.0:
+ version "3.4.3"
+ resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.4.3.tgz#3a4c22c30bfaa8b1b2aa48d12b2ba811c0947ab7"
+ integrity sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==
+
+mobx-utils@^6.0.6:
+ version "6.0.8"
+ resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-6.0.8.tgz#843e222c7694050c2e42842682fd24a84fdb7024"
+ integrity sha512-fPNt0vJnHwbQx9MojJFEnJLfM3EMGTtpy4/qOOW6xueh1mPofMajrbYAUvByMYAvCJnpy1A5L0t+ZVB5niKO4g==
+
+mobx@^6.6.1:
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.10.0.tgz#3537680fe98d45232cc19cc8f76280bd8bb6b0b7"
+ integrity sha512-WMbVpCMFtolbB8swQ5E2YRrU+Yu8iLozCVx3CdGjbBKlP7dFiCSuiG06uea3JCFN5DnvtAX7+G5Bp82e2xu0ww==
+
moo@^0.5.1:
version "0.5.2"
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c"
@@ -18923,6 +18885,15 @@ react-app-polyfill@^3.0.0:
regenerator-runtime "^0.13.9"
whatwg-fetch "^3.6.2"
+react-avatar-editor@^13.0.0:
+ version "13.0.0"
+ resolved "https://registry.yarnpkg.com/react-avatar-editor/-/react-avatar-editor-13.0.0.tgz#55013625ee9ae715c1fe2dc553b8079994d8a5f2"
+ integrity sha512-0xw63MbRRQdDy7YI1IXU9+7tTFxYEFLV8CABvryYOGjZmXRTH2/UA0mafe57ns62uaEFX181kA4XlGlxCaeXKA==
+ dependencies:
+ "@babel/plugin-transform-runtime" "^7.12.1"
+ "@babel/runtime" "^7.12.5"
+ prop-types "^15.7.2"
+
"react-compiler-runtime@file:./lib/react-compiler-runtime":
version "0.0.1"
@@ -18982,11 +18953,6 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.3.tgz#5e3ca90e682fed1d73a7cb50c2c7402b3e85618d"
integrity sha512-ZnXwLQnGzrDpHBHiC56TXFXvmolPeMjTn1UOm610M4EXGzbEDR7oOIyS2ZiItgbs6eZc4oU/a0hpk8PrcKvv5g==
-react-image-crop@^11.0.7:
- version "11.0.7"
- resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-11.0.7.tgz#25f3d37ccbb65a05d19d23b4740a5912835c741e"
- integrity sha512-ZciKWHDYzmm366JDL18CbrVyjnjH0ojufGDmScfS4ZUqLHg4nm6ATY+K62C75W4ZRNt4Ii+tX0bSjNk9LQ2xzQ==
-
"react-is@^16.12.0 || ^17.0.0 || ^18.0.0", react-is@^18.0.0, react-is@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
@@ -19033,6 +18999,14 @@ react-native-drawer-layout@^4.0.0-alpha.3:
dependencies:
use-latest-callback "^0.1.9"
+react-native-fs@^2.20.0:
+ version "2.20.0"
+ resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6"
+ integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ==
+ dependencies:
+ base-64 "^0.1.0"
+ utf8 "^3.0.0"
+
react-native-gesture-handler@~2.16.2:
version "2.16.2"
resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.16.2.tgz#032bd2a07334292d7f6cff1dc9d1ec928f72e26d"
@@ -20644,16 +20618,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
-"string-width-cjs@npm:string-width@^4.2.0":
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -20762,7 +20727,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -20776,13 +20741,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -20977,19 +20935,6 @@ svgo@^2.7.0:
picocolors "^1.0.0"
stable "^0.1.8"
-svgo@^3.3.2:
- version "3.3.2"
- resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.2.tgz#ad58002652dffbb5986fc9716afe52d869ecbda8"
- integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==
- dependencies:
- "@trysound/sax" "0.2.0"
- commander "^7.2.0"
- css-select "^5.1.0"
- css-tree "^2.3.1"
- css-what "^6.1.0"
- csso "^5.0.5"
- picocolors "^1.0.0"
-
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@@ -21271,18 +21216,6 @@ tlds@^1.234.0:
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.242.0.tgz#da136a9c95b0efa1a4cd57dca8ef240c08ada4b7"
integrity sha512-aP3dXawgmbfU94mA32CJGHmJUE1E58HCB1KmlKRhBNtqBL27mSQcAEmcaMaQ1Za9kIVvOdbxJD3U5ycDy7nJ3w==
-tldts-core@^6.1.46:
- version "6.1.46"
- resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.46.tgz#062d64981ee83f934f875c178a97e42bcd13bef7"
- integrity sha512-zA3ai/j4aFcmbqTvTONkSBuWs0Q4X4tJxa0gV9sp6kDbq5dAhQDSg0WUkReEm0fBAKAGNj+wPKCCsR8MYOYmwA==
-
-tldts@^6.1.46:
- version "6.1.46"
- resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.46.tgz#0c3c4157efe732caeddd06eee6da891b26bd8a75"
- integrity sha512-fw81lXV2CijkNrZAZvee7wegs+EOlTyIuVl/z4q6OUzZHQ1jGL2xQzKXq9geYf/1tzo9LZQLrkcko2m8HLh+rg==
- dependencies:
- tldts-core "^6.1.46"
-
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -21817,6 +21750,11 @@ use-sidecar@^1.1.2:
detect-node-es "^1.1.0"
tslib "^2.0.0"
+utf8@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1"
+ integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==
+
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -22518,7 +22456,7 @@ workbox-window@6.6.1:
"@types/trusted-types" "^2.0.2"
workbox-core "6.6.1"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -22536,15 +22474,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"