+
diff --git a/eslint/index.js b/eslint/index.js
index cf5d41225d..6f75f1bc34 100644
--- a/eslint/index.js
+++ b/eslint/index.js
@@ -5,5 +5,6 @@ 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 06723043fe..26e688563e 100644
--- a/eslint/use-exact-imports.js
+++ b/eslint/use-exact-imports.js
@@ -1,4 +1,3 @@
-/* eslint-disable bsky-internal/use-exact-imports */
const BANNED_IMPORTS = [
'@fortawesome/free-regular-svg-icons',
'@fortawesome/free-solid-svg-icons',
@@ -6,11 +5,12 @@ const BANNED_IMPORTS = [
exports.create = function create(context) {
return {
- Literal(node) {
- if (typeof node.value !== 'string') {
+ ImportDeclaration(node) {
+ const source = node.source
+ if (typeof source.value !== 'string') {
return
}
- if (BANNED_IMPORTS.includes(node.value)) {
+ if (BANNED_IMPORTS.includes(source.value)) {
context.report({
node,
message:
diff --git a/eslint/use-prefixed-imports.js b/eslint/use-prefixed-imports.js
new file mode 100644
index 0000000000..141d536484
--- /dev/null
+++ b/eslint/use-prefixed-imports.js
@@ -0,0 +1,39 @@
+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 a68c1dc4bf..50a33589ea 100644
--- a/jest/jestSetup.js
+++ b/jest/jestSetup.js
@@ -42,8 +42,16 @@ jest.mock('rn-fetch-blob', () => ({
fetch: jest.fn(),
}))
-jest.mock('@bam.tech/react-native-image-resizer', () => ({
- createResizedImage: 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('@segment/analytics-react-native', () => ({
diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift
index f863eaf223..481402890f 100644
--- a/modules/BlueskyNSE/NotificationService.swift
+++ b/modules/BlueskyNSE/NotificationService.swift
@@ -2,46 +2,80 @@ 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 {
- var prefs = UserDefaults(suiteName: APP_GROUP)
+ private var contentHandler: ContentHandler?
+ private var bestAttempt: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
- guard let bestAttempt = createCopy(request.content),
+ self.contentHandler = contentHandler
+
+ guard let bestAttempt = NSEUtil.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() {
- // If for some reason the alloted time expires, we don't actually want to display a notification
+ guard let contentHandler = self.contentHandler,
+ let bestAttempt = self.bestAttempt else {
+ return
+ }
+ contentHandler(bestAttempt)
}
- func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
- return content.mutableCopy() as? UNMutableNotificationContent
- }
+ // MARK: Mutations
func mutateWithBadge(_ content: UNMutableNotificationContent) {
- var count = prefs?.integer(forKey: "badgeCount") ?? 0
- count += 1
+ NSEUtil.shared.prefsQueue.sync {
+ var count = NSEUtil.shared.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)
- 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)
+ NSEUtil.shared.prefs?.setValue(count, forKey: "badgeCount")
+ }
}
func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
- if self.prefs?.bool(forKey: "playSoundChat") == true {
+ if NSEUtil.shared.prefs?.bool(forKey: "playSoundChat") == true {
mutateWithDmSound(content)
}
}
@@ -54,3 +88,18 @@ 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 421abb3c41..43f46a5e56 100644
--- a/modules/Share-with-Bluesky/Info.plist
+++ b/modules/Share-with-Bluesky/Info.plist
@@ -16,6 +16,8 @@
1
NSExtensionActivationSupportsImageWithMaxCount
10
+
NSExtensionActivationSupportsMovieWithMaxCount
+
1
NSExtensionPointIdentifier
@@ -38,4 +40,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 c045d578fe..63143277a5 100644
--- a/modules/Share-with-Bluesky/ShareViewController.swift
+++ b/modules/Share-with-Bluesky/ShareViewController.swift
@@ -5,7 +5,6 @@ class ShareViewController: UIViewController {
// scheme.
let appScheme = Bundle.main.object(forInfoDictionaryKey: "MainAppScheme") as? String ?? "bluesky"
- //
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
@@ -24,6 +23,8 @@ 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()
}
@@ -31,31 +32,23 @@ class ShareViewController: UIViewController {
}
private func handleText(item: NSItemProvider) async {
- 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)
- }
+ 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 {
- 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)
- }
+ 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 {
@@ -105,6 +98,25 @@ 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
@@ -114,27 +126,26 @@ 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 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)"
- }
+ 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)"
}
- return nil
- } catch {
- 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 ba7882902c..4b3486545e 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 && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/ && cp -v ./web-build/static/media/*.png ./bskyweb/static/media/",
+ "build-web": "expo export:web && node ./scripts/post-web-build.js",
"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"
+ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web",
+ "icons:optimize": "svgo -f ./assets/icons"
},
"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,11 +110,13 @@
"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",
@@ -158,24 +160,20 @@
"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",
@@ -204,6 +202,7 @@
"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"
},
@@ -235,7 +234,6 @@
"@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",
@@ -269,6 +267,7 @@
"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",
@@ -336,8 +335,13 @@
},
"lint-staged": {
"*{.js,.jsx,.ts,.tsx}": [
- "eslint --cache --fix",
+ "eslint --cache --fix"
+ ],
+ "*{.js,.jsx,.ts,.tsx,.css}": [
"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 4878bb9f7e..ea26b821da 100644
--- a/patches/expo-modules-core+1.12.11.patch
+++ b/patches/expo-modules-core+1.12.11.patch
@@ -4,11 +4,23 @@ 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 789ba84ace..aee3da1ecc 100644
--- a/patches/react-native+0.74.1.patch
+++ b/patches/react-native+0.74.1.patch
@@ -1,5 +1,18 @@
+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..9974932 100644
+index b0d71dc..41b9a0e 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
@@ -36,7 +49,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..4c32b31 100644
+index b09e653..f93cb46 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 baaa7cb8b7..7bbee38554 100644
--- a/scripts/post-web-build.js
+++ b/scripts/post-web-build.js
@@ -20,7 +20,30 @@ console.log(`Writing ${templateFile}`)
const outputFile = entrypoints
.map(name => {
const file = path.basename(name)
- return ``
+ const ext = path.extname(file)
+
+ if (ext === '.js') {
+ return ``
+ }
+ if (ext === '.css') {
+ 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 9214253aca..c6334379f7 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,6 +29,11 @@ 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'
@@ -55,7 +60,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, useFonts} from '#/alf'
+import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
@@ -66,6 +71,11 @@ import {BackgroundNotificationPreferencesProvider} from '../modules/expo-backgro
SplashScreen.preventAutoHideAsync()
+/**
+ * Begin geolocation ASAP
+ */
+beginResolveGeolocation()
+
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
@@ -106,60 +116,64 @@ 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(() => {
- initPersistedState().then(() => setReady(true))
+ Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
+ setReady(true),
+ )
}, [])
- if (!isReady || !loaded) {
+ if (!isReady) {
return null
}
@@ -168,36 +182,38 @@ 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 1c66507336..1664812d08 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -1,5 +1,6 @@
-import 'lib/sentry' // must be near top
-import 'view/icons'
+import '#/lib/sentry' // must be near top
+import '#/view/icons'
+import './style.css'
import React, {useEffect, useState} from 'react'
import {KeyboardProvider} from 'react-native-keyboard-controller'
@@ -18,6 +19,11 @@ 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'
@@ -46,7 +52,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, useFonts} from '#/alf'
+import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
@@ -54,6 +60,11 @@ 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()
@@ -96,61 +107,64 @@ 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(() => {
- initPersistedState().then(() => setReady(true))
+ Promise.all([initPersistedState(), ensureGeolocationResolved()]).then(() =>
+ setReady(true),
+ )
}, [])
- if (!isReady || !loaded) {
+ if (!isReady) {
return null
}
@@ -159,31 +173,33 @@ 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 9f75d305ae..0c8eb330d7 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -276,16 +276,13 @@ export const atoms = {
letterSpacing: tokens.TRACKING,
},
font_normal: {
- fontWeight: tokens.fontWeight.normal,
- },
- font_semibold: {
- fontWeight: tokens.fontWeight.semibold,
+ fontWeight: tokens.fontWeight.regular,
},
font_bold: {
- fontWeight: tokens.fontWeight.bold,
+ fontWeight: tokens.fontWeight.semibold,
},
font_heavy: {
- fontWeight: tokens.fontWeight.heavy,
+ fontWeight: tokens.fontWeight.extrabold,
},
italic: {
fontStyle: 'italic',
diff --git a/src/alf/fonts.ts b/src/alf/fonts.ts
index ce658fa05b..b11ce939f8 100644
--- a/src/alf/fonts.ts
+++ b/src/alf/fonts.ts
@@ -1,6 +1,4 @@
-import {useFonts as defaultUseFonts} from 'expo-font'
-
-import {isNative, isWeb} from '#/platform/detection'
+import {isWeb} from '#/platform/detection'
import {Device, device} from '#/storage'
const FAMILIES = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif`
@@ -34,38 +32,6 @@ 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.
*/
@@ -108,4 +74,10 @@ 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 f5d2247f9f..9f7ec5c673 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_0,
+ white: color.gray_25,
black: color.trueBlack,
contrast_25: color.gray_975,
diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts
index d43d2b67dd..3f30702e85 100644
--- a/src/alf/tokens.ts
+++ b/src/alf/tokens.ts
@@ -47,11 +47,16 @@ export const borderRadius = {
full: 999,
} as const
+/**
+ * These correspond to Inter font files we actually load.
+ */
export const fontWeight = {
- normal: '400',
- semibold: '500',
- bold: '600',
- heavy: '700',
+ regular: '400',
+ // medium: '500',
+ semibold: '600',
+ // bold: '700',
+ extrabold: '800',
+ // black: '900',
} as const
export const gradients = {
diff --git a/src/components/AppLanguageDropdown.tsx b/src/components/AppLanguageDropdown.tsx
index 02cd0ce2d4..6170ab2e20 100644
--- a/src/components/AppLanguageDropdown.tsx
+++ b/src/components/AppLanguageDropdown.tsx
@@ -24,8 +24,6 @@ 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 a106d99663..00a7b53011 100644
--- a/src/components/AppLanguageDropdown.web.tsx
+++ b/src/components/AppLanguageDropdown.web.tsx
@@ -27,8 +27,6 @@ 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 704aa9d987..8728b88c2c 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 {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
+import {atoms as a, flatten, select, tokens, useTheme, web} 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' | 'xsmall' | 'small' | 'medium' | 'large'
+export type ButtonSize = 'tiny' | 'small' | 'large'
export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = {
/**
@@ -343,39 +343,46 @@ export const Button = React.forwardRef(
if (shape === 'default') {
if (size === 'large') {
- 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,
- )
+ baseStyles.push({
+ paddingVertical: 13,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ gap: 8,
+ })
} else if (size === 'small') {
- 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)
+ baseStyles.push({
+ paddingVertical: 8,
+ paddingHorizontal: 12,
+ borderRadius: 6,
+ gap: 6,
+ })
} else if (size === 'tiny') {
- baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs)
+ baseStyles.push({
+ paddingVertical: 4,
+ paddingHorizontal: 8,
+ borderRadius: 4,
+ gap: 4,
+ })
}
} else if (shape === 'round' || shape === 'square') {
if (size === 'large') {
if (shape === 'round') {
- baseStyles.push({height: 54, width: 54})
+ baseStyles.push({height: 46, width: 46})
} else {
- baseStyles.push({height: 50, width: 50})
+ baseStyles.push({height: 44, width: 44})
}
} else if (size === 'small') {
- baseStyles.push({height: 34, width: 34})
- } else if (size === 'xsmall') {
- baseStyles.push({height: 28, width: 28})
+ if (shape === 'round') {
+ baseStyles.push({height: 36, width: 36})
+ } else {
+ baseStyles.push({height: 34, width: 34})
+ }
} else if (size === 'tiny') {
- baseStyles.push({height: 20, width: 20})
+ if (shape === 'round') {
+ baseStyles.push({height: 22, width: 22})
+ } else {
+ baseStyles.push({height: 21, width: 21})
+ }
}
if (shape === 'round') {
@@ -619,11 +626,11 @@ export function useSharedButtonTextStyles() {
}
if (size === 'large') {
- baseStyles.push(a.text_md, android({paddingBottom: 1}))
+ 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}))
} else if (size === 'tiny') {
- baseStyles.push(a.text_xs, android({paddingBottom: 1}))
- } else {
- baseStyles.push(a.text_sm, android({paddingBottom: 1}))
+ baseStyles.push(a.text_xs, a.leading_tight)
}
return StyleSheet.flatten(baseStyles)
@@ -643,31 +650,98 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
export function ButtonIcon({
icon: Comp,
position,
- size: iconSize,
+ size,
}: {
icon: React.ComponentType
position?: 'left' | 'right'
size?: SVGIconProps['size']
}) {
- const {size, disabled} = useButtonContext()
+ const {size: buttonSize, 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 e6d664cfda..b28f66f839 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 {sanitizeHandle} from 'lib/strings/handles'
-import {precacheFeedFromGeneratorView} from 'state/queries/feed'
-import {useSession} from 'state/session'
+import {useSession} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
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,7 +121,10 @@ export function TitleAndByline({
return (
-
+
{title}
{creator && (
diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx
index 4017a7b0be..35a346c3a5 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 542f2d2993..03b8ece6b1 100644
--- a/src/components/LabelingServiceCard/index.tsx
+++ b/src/components/LabelingServiceCard/index.tsx
@@ -9,6 +9,7 @@ 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'
@@ -43,21 +44,45 @@ 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 ? (
-
+
) : (
-
- By {sanitizeHandle(handle, '@')}
+
+ {_(msg`By ${sanitizeHandle(handle, '@')}`)}
)
}
+export function RegionalNotice() {
+ const t = useTheme()
+ return (
+
+
+
+ Required in your region
+
+
+ )
+}
+
export function LikeCount({count}: {count: number}) {
const t = useTheme()
return (
@@ -66,7 +91,7 @@ export function LikeCount({count}: {count: number}) {
a.mt_sm,
a.text_sm,
t.atoms.text_contrast_medium,
- {fontWeight: '500'},
+ {fontWeight: '600'},
]}>
@@ -85,7 +110,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 6c25faffb8..c80b9f3707 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -9,6 +9,7 @@ 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 {
@@ -17,11 +18,10 @@ import {
isExternalUrl,
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
-import {isNative} from '#/platform/detection'
+import {isNative, isWeb} 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,7 +244,10 @@ export function Link({
export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick
> &
- Pick
+ Pick & {
+ disableUnderline?: boolean
+ title?: TextProps['title']
+ }
export function InlineLinkText({
children,
@@ -257,6 +260,7 @@ export function InlineLinkText({
selectable,
label,
shareOnLongPress,
+ disableUnderline,
...rest
}: InlineLinkProps) {
const t = useTheme()
@@ -290,11 +294,12 @@ export function InlineLinkText({
{...rest}
style={[
{color: t.palette.primary_500},
- (hovered || focused || pressed) && {
- ...web({outline: 0}),
- textDecorationLine: 'underline',
- textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
- },
+ (hovered || focused || pressed) &&
+ !disableUnderline && {
+ ...web({outline: 0}),
+ textDecorationLine: 'underline',
+ textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
+ },
flattenedStyle,
]}
role="link"
@@ -365,3 +370,18 @@ 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 829f36d471..ed5838fb04 100644
--- a/src/components/ListCard.tsx
+++ b/src/components/ListCard.tsx
@@ -7,13 +7,14 @@ import {
moderateUserList,
ModerationUI,
} from '@atproto/api'
-import {Trans} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
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,
@@ -111,6 +112,7 @@ export function TitleAndByline({
modUi?: ModerationUI
}) {
const t = useTheme()
+ const {_} = useLingui()
const {currentAccount} = useSession()
return (
@@ -130,6 +132,7 @@ export function TitleAndByline({
{title}
@@ -139,15 +142,12 @@ export function TitleAndByline({
{creator && (
- {purpose === MODLIST ? (
-
- Moderation list by {sanitizeHandle(creator.handle, '@')}
-
- ) : (
- List by {sanitizeHandle(creator.handle, '@')}
- )}
+ {purpose === MODLIST
+ ? _(msg`Moderation list by ${sanitizeHandle(creator.handle, '@')}`)
+ : _(msg`List by ${sanitizeHandle(creator.handle, '@')}`)}
)}
diff --git a/src/components/MediaInsetBorder.tsx b/src/components/MediaInsetBorder.tsx
index ef8b00e2e0..ed89880f40 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 742a11667c..974d83593f 100644
--- a/src/components/Pills.tsx
+++ b/src/components/Pills.tsx
@@ -130,9 +130,10 @@ export function Label({
)}
{name}
{handle}
diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
index 3890790dbe..4cda42fdbe 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,6 +411,7 @@ function Inner({
() => currentAccount?.did === profile.did,
[currentAccount, profile],
)
+ const isLabeler = profile.associated?.labeler
return (
@@ -419,11 +420,13 @@ function Inner({
{!isMe &&
+ !isLabeler &&
(isBlockedUser ? (
}) {
diff --git a/src/components/ProgressGuide/Task.tsx b/src/components/ProgressGuide/Task.tsx
index a83715a425..f2ceba52ac 100644
--- a/src/components/ProgressGuide/Task.tsx
+++ b/src/components/ProgressGuide/Task.tsx
@@ -35,9 +35,7 @@ export function ProgressGuideTask({
)}
-
- {title}
-
+ {title}
{subtitle && (
diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx
index 346312af51..69e0082606 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 7836bbef95..8765cdee31 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 870cbbb9fd..a848cd5b92 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="xsmall"
+ size="small"
onPress={() => control.close()}>
Close
diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx
index bd308fc73a..44f01a1545 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,6 +78,7 @@ function WizardListCard({
/>
diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx
index 15f88468a7..501e23872f 100644
--- a/src/components/Typography.tsx
+++ b/src/components/Typography.tsx
@@ -1,15 +1,85 @@
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 {isNative} from '#/platform/detection'
+import {logger} from '#/logger'
+import {isIOS, isNative} from '#/platform/detection'
import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
+import {IS_DEV} from '#/env'
-export type TextProps = RNTextProps & {
+export type StringChild = string | (string | null)[]
+
+export type TextProps = Omit & {
/**
* 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}
+
+ ))
+ })}
+
+ )
}
/**
@@ -64,7 +134,15 @@ export function normalizeTextStyles(
/**
* Our main text component. Use this most of the time.
*/
-export function Text({style, selectable, ...rest}: TextProps) {
+export function Text({
+ children,
+ emoji,
+ style,
+ selectable,
+ title,
+ dataSet,
+ ...rest
+}: TextProps) {
const {fonts, flags} = useAlf()
const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
@@ -73,7 +151,29 @@ export function Text({style, selectable, ...rest}: TextProps) {
flags,
})
- return
+ 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}
+
+ )
}
export function createHeadingElement({level}: {level: number}) {
diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx
index d831c6002a..08608f9d88 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 f43c3c6fe6..ca75b01390 100644
--- a/src/components/dialogs/Embed.tsx
+++ b/src/components/dialogs/Embed.tsx
@@ -106,21 +106,23 @@ 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 f7e6145975..765b8adc7e 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="medium"
+ size="large"
variant="solid">
Enable external media
@@ -95,7 +95,7 @@ export function EmbedConsentDialog({
onPress={onShowPress}
onAccessibilityEscape={control.close}
color="secondary"
- size="medium"
+ size="large"
variant="solid">
Enable {externalEmbedLabels[source]} only
@@ -106,7 +106,7 @@ export function EmbedConsentDialog({
onAccessibilityEscape={control.close}
onPress={onHidePress}
color="secondary"
- size="medium"
+ size="large"
variant="ghost">
No thanks
diff --git a/src/components/dialogs/GifSelect.ios.tsx b/src/components/dialogs/GifSelect.ios.tsx
index 091a23e51c..2f867e8657 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="medium"
+ size="large"
variant="solid">
Close
diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx
index 4c60c6ebeb..1afc588dad 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="medium"
+ size="large"
variant="solid">
Close
diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx
index 38273aad54..81a6141038 100644
--- a/src/components/dialogs/MutedWords.tsx
+++ b/src/components/dialogs/MutedWords.tsx
@@ -319,7 +319,7 @@ function MutedWordsInner() {
{_(msg`Save`)}
@@ -491,9 +491,7 @@ function Selectable({
},
style,
]}>
-
- {label}
-
+ {label}
{isSelected ? (
) : (
diff --git a/src/components/dialogs/nuxs/NeueTypography.tsx b/src/components/dialogs/nuxs/NeueTypography.tsx
index f33cea8e78..f160c87743 100644
--- a/src/components/dialogs/nuxs/NeueTypography.tsx
+++ b/src/components/dialogs/nuxs/NeueTypography.tsx
@@ -48,20 +48,19 @@ export function NeueTypography() {
-
- Introducing new font settings ✨
+
+ New font settings ✨
-
+
- To the ensure the best possible experience, we're introducing a
- new theme font, along with adjustable font sizing settings.
+ We're introducing a new theme font, along with adjustable font
+ sizing.
- Defaults are shown below. You can edit these in your Appearance
- Settings later.
+ You can adjust 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 8960824094..21e775a108 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_semibold,
+ a.font_bold,
a.leading_snug,
lightTheme.atoms.text_contrast_medium,
]}>
@@ -551,7 +551,7 @@ export function TenMillionInner({
style={[
a.flex_1,
a.text_sm,
- a.font_semibold,
+ a.font_bold,
a.leading_snug,
a.text_right,
lightTheme.atoms.text_contrast_low,
@@ -643,14 +643,7 @@ export function TenMillionInner({
+ style={[a.text_5xl, a.leading_tight, a.pb_lg, a.font_heavy]}>
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 1a6bbbe601..ab9ec16e4d 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,6 +170,7 @@ function HeaderReady({
control.close()}>
diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx
index c916f4efce..1c78d2abbb 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="medium"
+ size="large"
color="primary"
variant="solid">
diff --git a/src/components/forms/TextField.tsx b/src/components/forms/TextField.tsx
index 23229c8f44..94ee261e38 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 391b1c8b70..4e3695bbf2 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: 20,
- width: 20,
+ height: 24,
+ width: 24,
},
baseStyles,
hovered ? baseHoverStyles : {},
@@ -383,9 +383,9 @@ export function Switch() {
t.atoms.border_contrast_high,
{
borderWidth: 1,
- height: 20,
- width: 32,
- padding: 2,
+ height: 24,
+ width: 36,
+ padding: 3,
},
baseStyles,
hovered ? baseHoverStyles : {},
@@ -395,8 +395,8 @@ export function Switch() {
style={[
a.rounded_full,
{
- height: 14,
- width: 14,
+ height: 16,
+ width: 16,
},
selected
? {
@@ -436,8 +436,8 @@ export function Radio() {
t.atoms.border_contrast_high,
{
borderWidth: 1,
- height: 20,
- width: 20,
+ height: 24,
+ width: 24,
},
baseStyles,
hovered ? baseHoverStyles : {},
@@ -447,7 +447,7 @@ export function Radio() {
style={[
a.absolute,
a.rounded_full,
- {height: 12, width: 12},
+ {height: 16, width: 16},
selected
? {
backgroundColor: t.palette.primary_500,
diff --git a/src/components/icons/Accessibility.tsx b/src/components/icons/Accessibility.tsx
new file mode 100644
index 0000000000..1e5ec0c090
--- /dev/null
+++ b/src/components/icons/Accessibility.tsx
@@ -0,0 +1,5 @@
+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 011bf6afa3..82e0d6e7f6 100644
--- a/src/components/icons/ArrowBoxLeft.tsx
+++ b/src/components/icons/ArrowBoxLeft.tsx
@@ -3,3 +3,7 @@ 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
new file mode 100644
index 0000000000..b59c1680ef
--- /dev/null
+++ b/src/components/icons/AspectRatio.tsx
@@ -0,0 +1,13 @@
+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 2487250545..ef0d1003f1 100644
--- a/src/components/icons/At.tsx
+++ b/src/components/icons/At.tsx
@@ -1,5 +1,9 @@
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.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',
+ 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',
})
diff --git a/src/components/icons/BirthdayCake.tsx b/src/components/icons/BirthdayCake.tsx
new file mode 100644
index 0000000000..8e41cbac11
--- /dev/null
+++ b/src/components/icons/BirthdayCake.tsx
@@ -0,0 +1,5 @@
+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
new file mode 100644
index 0000000000..2865713743
--- /dev/null
+++ b/src/components/icons/BubbleInfo.tsx
@@ -0,0 +1,5 @@
+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
new file mode 100644
index 0000000000..4eb369379b
--- /dev/null
+++ b/src/components/icons/CircleQuestion.tsx
@@ -0,0 +1,5 @@
+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="medium"
+ size="large"
disabled={sending}>
Resend Email
diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx
index f2d13f6424..bf9bae5171 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_semibold],
+ gtMobile && [a.font_bold],
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_semibold],
+ gtMobile && [a.font_bold],
t.atoms.text_contrast_high,
web({
marginBottom: 1,
diff --git a/src/components/moderation/LabelPreference.tsx b/src/components/moderation/LabelPreference.tsx
index 78b50ff8b9..d6dc45d1a5 100644
--- a/src/components/moderation/LabelPreference.tsx
+++ b/src/components/moderation/LabelPreference.tsx
@@ -236,8 +236,7 @@ export function LabelerLabelPreference({
-
+
{adultDisabled ? (
Adult content is disabled.
) : isGlobalLabel ? (
diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx
index fe6932290d..e63cea93b2 100644
--- a/src/components/moderation/LabelsOnMeDialog.tsx
+++ b/src/components/moderation/LabelsOnMeDialog.tsx
@@ -132,8 +132,10 @@ function Label({
]}>
- {strings.name}
-
+
+ {strings.name}
+
+
{strings.description}
@@ -279,7 +281,7 @@ function AppealForm({
testID="backBtn"
variant="solid"
color="secondary"
- size="medium"
+ size="large"
onPress={onPressBack}
label={_(msg`Back`)}>
{_(msg`Back`)}
@@ -288,7 +290,7 @@ function AppealForm({
testID="submitBtn"
variant="solid"
color="primary"
- size="medium"
+ size="large"
onPress={onSubmit}
label={_(msg`Submit`)}>
{_(msg`Submit`)}
diff --git a/src/components/moderation/ModerationDetailsDialog.tsx b/src/components/moderation/ModerationDetailsDialog.tsx
index d95717cf43..2259178538 100644
--- a/src/components/moderation/ModerationDetailsDialog.tsx
+++ b/src/components/moderation/ModerationDetailsDialog.tsx
@@ -118,7 +118,11 @@ 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 = ''
@@ -127,7 +131,7 @@ function ModerationDetailsDialogInner({
return (
-
+
{name}
diff --git a/src/components/moderation/ScreenHider.tsx b/src/components/moderation/ScreenHider.tsx
index f855d63331..5680b60c2d 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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {NavigationProp} from 'lib/routes/types'
+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,13 +86,7 @@ export function ScreenHider({
+ style={[a.text_4xl, a.font_bold, a.text_center, a.mb_md, t.atoms.text]}>
{isNoPwi ? (
Sign-in Required
) : (
@@ -118,7 +112,7 @@ export function ScreenHider({
(
// temporary file).
const newPath = uri.replace(/\.jpe?g$/, '.bin')
try {
- await RNFS.copyFile(uri, newPath)
+ await copyAsync({from: uri, to: newPath})
} catch {
// Failed to copy the file, just use the original
return await fn(uri)
@@ -74,7 +76,7 @@ async function withSafeFile(
return await fn(newPath)
} finally {
// Remove the temporary file
- await RNFS.unlink(newPath)
+ await safeDeleteAsync(newPath)
}
} else {
return fn(uri)
diff --git a/src/lib/embeds.ts b/src/lib/embeds.ts
index a758987b20..2904f1cc36 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 02940f793d..f588808fc3 100644
--- a/src/lib/haptics.ts
+++ b/src/lib/haptics.ts
@@ -1,20 +1,24 @@
import React from 'react'
import {impactAsync, ImpactFeedbackStyle} from 'expo-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
+import {isIOS, isWeb} from '#/platform/detection'
+import {useHapticsDisabled} from '#/state/preferences/disable-haptics'
export function useHaptics() {
const isHapticsDisabled = useHapticsDisabled()
- return React.useCallback(() => {
- if (isHapticsDisabled || isWeb) {
- return
- }
- impactAsync(hapticImpact)
- }, [isHapticsDisabled])
+ 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],
+ )
}
diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts
index 3f01e98c5e..e75f13755f 100644
--- a/src/lib/media/manip.ts
+++ b/src/lib/media/manip.ts
@@ -6,18 +6,20 @@ 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(
@@ -165,29 +167,47 @@ 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++) {
- const quality = 100 - i * 10
- const resizeRes = await ImageResizer.createResizedImage(
+ // nearest 10th
+ const quality = Math.round((1 - 0.1 * i) * 10) / 10
+ const resizeRes = await manipulateAsync(
localUri,
- opts.width,
- opts.height,
- 'JPEG',
- quality,
- undefined,
- undefined,
- undefined,
- {mode: opts.mode},
+ [{resize: newDimensions}],
+ {
+ format: SaveFormat.JPEG,
+ compress: quality,
+ },
)
- if (resizeRes.size < opts.maxSize) {
+
+ 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)
return {
- path: normalizePath(resizeRes.path),
+ path: normalizePath(resizeRes.uri),
mime: 'image/jpeg',
- size: resizeRes.size,
+ size: fileInfo.size,
width: resizeRes.width,
height: resizeRes.height,
}
} else {
- safeDeleteAsync(resizeRes.path)
+ safeDeleteAsync(resizeRes.uri)
}
}
throw new Error(
@@ -311,3 +331,25 @@ 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 e6b46ba774..fc6fcde45e 100644
--- a/src/lib/media/picker.e2e.tsx
+++ b/src/lib/media/picker.e2e.tsx
@@ -1,25 +1,37 @@
-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() {
- 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]
+ 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')
+ }
+
return await compressIfNeeded({
- path: file.path,
+ path: file,
mime: 'image/jpeg',
- size: file.size,
+ size: fileInfo.size,
width: 4288,
height: 2848,
})
diff --git a/src/lib/media/picker.shared.ts b/src/lib/media/picker.shared.ts
index 9146cd7787..85539a833e 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/jpeg',
+ mime: image.mimeType || '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 8782e14570..a53ffc9614 100644
--- a/src/lib/media/picker.web.tsx
+++ b/src/lib/media/picker.web.tsx
@@ -18,9 +18,11 @@ export async function openCropper(opts: CropperOptions): Promise {
name: 'crop-image',
uri: opts.path,
dimensions:
- opts.height && opts.width
+ opts.width && opts.height
? {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 e6f442759f..ec94256ea1 100644
--- a/src/lib/media/types.ts
+++ b/src/lib/media/types.ts
@@ -18,4 +18,7 @@ export interface CameraOpts {
cropperCircleOverlay?: boolean
}
-export type CropperOptions = Parameters[0]
+export type CropperOptions = Parameters[0] & {
+ webAspectRatio?: number
+ webCircularCrop?: boolean
+}
diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts
index 59d88023bf..7576a9c33c 100644
--- a/src/lib/moderation.ts
+++ b/src/lib/moderation.ts
@@ -33,6 +33,20 @@ 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
new file mode 100644
index 0000000000..4dfda658f7
--- /dev/null
+++ b/src/lib/strings/__tests__/email.test.ts
@@ -0,0 +1,82 @@
+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
new file mode 100644
index 0000000000..04b6038476
--- /dev/null
+++ b/src/lib/strings/email.ts
@@ -0,0 +1,9 @@
+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 6a3d796110..55fb1a844b 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,14 +79,13 @@ export const s = StyleSheet.create({
// font weights
fw600: {fontWeight: '600'},
- bold: {fontWeight: '700'},
- fw500: {fontWeight: '500'},
- semiBold: {fontWeight: '500'},
+ bold: {fontWeight: '600'},
+ fw500: {fontWeight: '600'},
+ semiBold: {fontWeight: '600'},
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 d16f9f632a..eb11872fa3 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: '500',
+ fontWeight: '600',
},
'2xl-bold': {
fontSize: 18,
letterSpacing: tokens.TRACKING,
- fontWeight: '700',
+ fontWeight: '600',
},
'2xl-heavy': {
fontSize: 18,
@@ -125,12 +125,12 @@ export const defaultTheme: Theme = {
'xl-medium': {
fontSize: 17,
letterSpacing: tokens.TRACKING,
- fontWeight: '500',
+ fontWeight: '600',
},
'xl-bold': {
fontSize: 17,
letterSpacing: tokens.TRACKING,
- fontWeight: '700',
+ fontWeight: '600',
},
'xl-heavy': {
fontSize: 17,
@@ -150,12 +150,12 @@ export const defaultTheme: Theme = {
'lg-medium': {
fontSize: 16,
letterSpacing: tokens.TRACKING,
- fontWeight: '500',
+ fontWeight: '600',
},
'lg-bold': {
fontSize: 16,
letterSpacing: tokens.TRACKING,
- fontWeight: '700',
+ fontWeight: '600',
},
'lg-heavy': {
fontSize: 16,
@@ -175,12 +175,12 @@ export const defaultTheme: Theme = {
'md-medium': {
fontSize: 15,
letterSpacing: tokens.TRACKING,
- fontWeight: '500',
+ fontWeight: '600',
},
'md-bold': {
fontSize: 15,
letterSpacing: tokens.TRACKING,
- fontWeight: '700',
+ fontWeight: '600',
},
'md-heavy': {
fontSize: 15,
@@ -200,12 +200,12 @@ export const defaultTheme: Theme = {
'sm-medium': {
fontSize: 14,
letterSpacing: tokens.TRACKING,
- fontWeight: '500',
+ fontWeight: '600',
},
'sm-bold': {
fontSize: 14,
letterSpacing: tokens.TRACKING,
- fontWeight: '700',
+ fontWeight: '600',
},
'sm-heavy': {
fontSize: 14,
@@ -225,12 +225,12 @@ export const defaultTheme: Theme = {
'xs-medium': {
fontSize: 13,
letterSpacing: tokens.TRACKING,
- fontWeight: '500',
+ fontWeight: '600',
},
'xs-bold': {
fontSize: 13,
letterSpacing: tokens.TRACKING,
- fontWeight: '700',
+ fontWeight: '600',
},
'xs-heavy': {
fontSize: 13,
@@ -241,24 +241,24 @@ export const defaultTheme: Theme = {
'title-2xl': {
fontSize: 34,
letterSpacing: tokens.TRACKING,
- fontWeight: '500',
+ fontWeight: '600',
},
'title-xl': {
fontSize: 28,
letterSpacing: tokens.TRACKING,
- fontWeight: '500',
+ fontWeight: '600',
},
'title-lg': {
fontSize: 22,
- fontWeight: '500',
+ fontWeight: '600',
},
title: {
- fontWeight: '500',
+ fontWeight: '600',
fontSize: 20,
letterSpacing: tokens.TRACKING,
},
'title-sm': {
- fontWeight: 'bold',
+ fontWeight: '600',
fontSize: 17,
letterSpacing: tokens.TRACKING,
},
@@ -273,12 +273,12 @@ export const defaultTheme: Theme = {
fontWeight: '400',
},
'button-lg': {
- fontWeight: '500',
+ fontWeight: '600',
fontSize: 18,
letterSpacing: tokens.TRACKING,
},
button: {
- fontWeight: '500',
+ fontWeight: '600',
fontSize: 14,
letterSpacing: tokens.TRACKING,
},
@@ -325,11 +325,11 @@ export const darkTheme: Theme = {
textInverted: colors.green2,
},
inverted: {
- background: lightPalette.white,
+ background: darkPalette.white,
backgroundLight: lightPalette.contrast_50,
text: lightPalette.black,
textLight: lightPalette.contrast_700,
- textInverted: lightPalette.white,
+ textInverted: darkPalette.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
new file mode 100644
index 0000000000..9e19e372b8
--- /dev/null
+++ b/src/locale/deviceLocales.ts
@@ -0,0 +1,53 @@
+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 3bae45214d..eb60fc5cf4 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,8 +160,13 @@ 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'
@@ -176,3 +181,20 @@ 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 c62ae71aae..dc30c2fd33 100644
--- a/src/platform/detection.ts
+++ b/src/platform/detection.ts
@@ -1,8 +1,4 @@
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'
@@ -15,9 +11,3 @@ 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 997fe419ed..9b0b5b1660 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 380f1080b0..3f4ce563be 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="xsmall"
+ size="small"
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="xsmall"
+ size="small"
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="xsmall"
+ size="small"
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="xsmall"
+ size="small"
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="xsmall"
+ size="small"
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="xsmall"
+ size="small"
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 3a98b87341..74412763f2 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 473bb08ea4..a694cbb837 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 {useGoBack} from 'lib/hooks/useGoBack'
-import {sanitizeHandle} from 'lib/strings/handles'
-import {useListBlockMutation, useListMuteMutation} from 'state/queries/list'
+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="medium"
+ size="large"
disabled={isProcessing}>
Go Back
diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx
index 8c002b1600..678ba51237 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="medium"
+ size="large"
onPress={onPressBack}>
{_(msg`Back`)}
diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx
index 8588888b87..7acaae5101 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="medium"
+ size="large"
onPress={onPressBack}>
Back
@@ -143,7 +143,7 @@ export const ForgotPasswordForm = ({
label={_(msg`Next`)}
variant="solid"
color={'primary'}
- size="medium"
+ size="large"
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="medium"
+ size="large"
variant="ghost"
color="secondary">
diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx
index 9a01c04990..9c2237214b 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="medium"
+ size="large"
onPress={onPressBack}>
Back
@@ -299,7 +299,7 @@ export const LoginForm = ({
accessibilityHint={_(msg`Retries login`)}
variant="solid"
color="secondary"
- size="medium"
+ size="large"
onPress={onPressRetryConnect}>
Retry
@@ -319,7 +319,7 @@ export const LoginForm = ({
accessibilityHint={_(msg`Navigates to the next screen`)}
variant="solid"
color="primary"
- size="medium"
+ size="large"
onPress={onPressNext}>
Next
diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx
index 5407f3f1e3..03e7d86696 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="medium">
+ size="large">
Okay
diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx
index 88f7ec5416..a6658621cc 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="medium"
+ size="large"
onPress={onPressBack}>
Back
@@ -174,7 +174,7 @@ export const SetNewPasswordForm = ({
label={_(msg`Next`)}
variant="solid"
color="primary"
- size="medium"
+ size="large"
onPress={onPressNext}>
Next
diff --git a/src/screens/Messages/Conversation/ChatDisabled.tsx b/src/screens/Messages/Conversation/ChatDisabled.tsx
index 23acc41cde..c768d2504b 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="medium"
+ size="large"
onPress={onBack}
label={_(msg`Back`)}>
{_(msg`Back`)}
@@ -137,7 +137,7 @@ function DialogInner() {
testID="submitBtn"
variant="solid"
color="primary"
- size="medium"
+ size="large"
onPress={onSubmit}
label={_(msg`Submit`)}>
{_(msg`Submit`)}
diff --git a/src/screens/Messages/Conversation/MessageInputEmbed.tsx b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
index bf28ed4fe9..2d1551019e 100644
--- a/src/screens/Messages/Conversation/MessageInputEmbed.tsx
+++ b/src/screens/Messages/Conversation/MessageInputEmbed.tsx
@@ -174,7 +174,6 @@ 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 c45cc28d7a..e9668b4e11 100644
--- a/src/screens/Messages/List/ChatListItem.tsx
+++ b/src/screens/Messages/List/ChatListItem.tsx
@@ -10,6 +10,10 @@ 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,
@@ -19,10 +23,6 @@ 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,6 +248,7 @@ 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 cd3179674c..9bfe6c3fac 100644
--- a/src/screens/Moderation/index.tsx
+++ b/src/screens/Moderation/index.tsx
@@ -7,6 +7,7 @@ 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'
@@ -22,8 +23,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'
@@ -338,7 +339,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 955e3d72c8..e30162c3af 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,8 +19,9 @@ export function ProfileHeaderDisplayName({
return (
+ style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '600'}]}>
{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 0344f1a234..ba869b6626 100644
--- a/src/screens/Profile/Header/Handle.tsx
+++ b/src/screens/Profile/Header/Handle.tsx
@@ -1,11 +1,12 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
-import {Trans} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+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'
@@ -18,6 +19,7 @@ export function ProfileHeaderHandle({
disableTaps?: boolean
}) {
const t = useTheme()
+ const {_} = useLingui()
const invalidHandle = isInvalidHandle(profile.handle)
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
return (
@@ -33,6 +35,7 @@ export function ProfileHeaderHandle({
) : undefined}
- {invalidHandle ? ⚠Invalid Handle : `@${profile.handle}`}
+ {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
)
diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx
index d675fb38ed..69e04f4af1 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 2be42d13e6..6958b7a478 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 73bd428c8b..e2401bb116 100644
--- a/src/screens/Signup/BackNextButtons.tsx
+++ b/src/screens/Signup/BackNextButtons.tsx
@@ -15,6 +15,7 @@ export interface BackNextButtonsProps {
onBackPress: () => void
onNextPress?: () => void
onRetryPress?: () => void
+ overrideNextText?: string
}
export function BackNextButtons({
@@ -25,6 +26,7 @@ export function BackNextButtons({
onBackPress,
onNextPress,
onRetryPress,
+ overrideNextText,
}: BackNextButtonsProps) {
const {_} = useLingui()
@@ -34,7 +36,7 @@ export function BackNextButtons({
label={_(msg`Go back to previous step`)}
variant="solid"
color="secondary"
- size="medium"
+ size="large"
onPress={onBackPress}>
Back
@@ -46,7 +48,7 @@ export function BackNextButtons({
label={_(msg`Press to retry`)}
variant="solid"
color="primary"
- size="medium"
+ size="large"
onPress={onRetryPress}>
Retry
@@ -59,11 +61,11 @@ export function BackNextButtons({
label={_(msg`Continue to next step`)}
variant="solid"
color="primary"
- size="medium"
+ size="large"
disabled={isLoading || isNextDisabled}
onPress={onNextPress}>
- Next
+ {overrideNextText ? overrideNextText : Next}
{isLoading && }
diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx
index e0a7912fd7..2d4b07318d 100644
--- a/src/screens/Signup/StepInfo/index.tsx
+++ b/src/screens/Signup/StepInfo/index.tsx
@@ -3,8 +3,10 @@ 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'
@@ -46,13 +48,41 @@ export function StepInfo({
const inviteCodeValueRef = useRef(state.inviteCode)
const emailValueRef = useRef(state.email)
+ const prevEmailValueRef = useRef(state.email)
const passwordValueRef = useRef(state.password)
- const onNextPress = React.useCallback(async () => {
+ 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 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
}
@@ -89,13 +119,7 @@ export function StepInfo({
logEvent('signup:nextPressed', {
activeStep: state.activeStep,
})
- }, [
- _,
- dispatch,
- state.activeStep,
- state.dateOfBirth,
- state.serviceDescription?.inviteCodeRequired,
- ])
+ }
return (
@@ -148,6 +172,9 @@ export function StepInfo({
testID="emailInput"
onChangeText={value => {
emailValueRef.current = value.trim()
+ if (hasWarnedEmail) {
+ setHasWarnedEmail(false)
+ }
}}
label={_(msg`Enter your email address`)}
defaultValue={state.email}
@@ -208,6 +235,7 @@ 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 0e1a2e61fa..3209800328 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 5f1d5e0628..68ff3aa7bc 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 {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 {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'
+} from '#/state/shell/starter-pack'
+import {LoggedOutScreenState} from '#/view/com/auth/LoggedOut'
import {formatCount} from '#/view/com/util/numeric/format'
-import {LoggedOutScreenState} from 'view/com/auth/LoggedOut'
-import {CenteredView} from 'view/com/util/Views'
-import {Logo} from 'view/icons/Logo'
+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,12 +188,7 @@ function LandingScreenLoaded({
{record.name}
+ style={[a.text_center, a.font_bold, a.text_md, {color: 'white'}]}>
Starter pack by {`@${creator.handle}`}
@@ -219,11 +214,7 @@ function LandingScreenLoaded({
color={t.atoms.text_contrast_medium.color}
/>
{formatCount(i18n, JOINED_THIS_WEEK)} joined this week
@@ -308,7 +299,7 @@ function LandingScreenLoaded({
label={_(msg`Signup without a starter pack`)}
variant="solid"
color="secondary"
- size="medium"
+ size="large"
style={[a.py_lg]}
onPress={onJoinWithoutPress}>
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index 5b267ff272..e3d32a1dd5 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 {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 {useSetActiveStarterPack} from '#/state/shell/starter-pack'
+import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
+import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
import * as Toast from '#/view/com/util/Toast'
-import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
-import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
-import {CenteredView} from 'view/com/util/Views'
+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="medium">
+ size="large">
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 40a4a510b7..65a3500f62 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 {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 {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 {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 {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'
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="medium"
+ size="large"
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
new file mode 100644
index 0000000000..f4c8b712ef
--- /dev/null
+++ b/src/state/gallery.ts
@@ -0,0 +1,299 @@
+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
new file mode 100644
index 0000000000..4d45bb574b
--- /dev/null
+++ b/src/state/geolocation.tsx
@@ -0,0 +1,169 @@
+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 529dc55907..5be21dfd39 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -3,8 +3,6 @@ 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'
@@ -37,24 +35,15 @@ 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'
}
@@ -137,9 +126,7 @@ 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
deleted file mode 100644
index 828905002e..0000000000
--- a/src/state/models/media/gallery.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-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
deleted file mode 100644
index ccabd50475..0000000000
--- a/src/state/models/media/image.e2e.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-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
deleted file mode 100644
index 55f6364911..0000000000
--- a/src/state/models/media/image.ts
+++ /dev/null
@@ -1,310 +0,0 @@
-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 6f4beae2ca..51d757ad8b 100644
--- a/src/state/persisted/index.ts
+++ b/src/state/persisted/index.ts
@@ -8,6 +8,7 @@ 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'
@@ -33,10 +34,10 @@ export async function write(
key: K,
value: Schema[K],
): Promise {
- _state = {
+ _state = normalizeData({
..._state,
[key]: value,
- }
+ })
await writeToStorage(_state)
}
write satisfies PersistedApi['write']
@@ -81,6 +82,9 @@ async function readFromStorage(): Promise {
})
}
if (rawData) {
- return tryParse(rawData)
+ const parsed = tryParse(rawData)
+ if (parsed) {
+ return normalizeData(parsed)
+ }
}
}
diff --git a/src/state/persisted/index.web.ts b/src/state/persisted/index.web.ts
index 7521776bc0..4cfc87cdb1 100644
--- a/src/state/persisted/index.web.ts
+++ b/src/state/persisted/index.web.ts
@@ -9,6 +9,7 @@ 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'
@@ -56,10 +57,10 @@ export async function write(
} catch (e) {
// Ignore and go through the normal path.
}
- _state = {
+ _state = normalizeData({
..._state,
[key]: value,
- }
+ })
writeToStorage(_state)
broadcast.postMessage({event: {type: UPDATE_EVENT, key}})
broadcast.postMessage({event: UPDATE_EVENT}) // Backcompat while upgrading
@@ -140,9 +141,11 @@ function readFromStorage(): Schema | undefined {
return lastResult
} else {
const result = tryParse(rawData)
- lastRawData = rawData
- lastResult = result
- return result
+ if (result) {
+ lastRawData = rawData
+ lastResult = normalizeData(result)
+ return lastResult
+ }
}
}
}
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index 331a111a2e..8040179496 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -1,7 +1,8 @@
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
@@ -55,10 +56,39 @@ const schema = z.object({
lastEmailConfirm: z.string().optional(),
}),
languagePrefs: z.object({
- primaryLanguage: z.string(), // should move to server
- contentLanguages: z.array(z.string()), // should move to server
- postLanguage: z.string(), // should move to server
+ /**
+ * 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.
+ */
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
@@ -108,13 +138,17 @@ export const defaults: Schema = {
lastEmailConfirm: undefined,
},
languagePrefs: {
- primaryLanguage: deviceLocales[0] || 'en',
- contentLanguages: deviceLocales || [],
- postLanguage: deviceLocales[0] || 'en',
- postLanguageHistory: (deviceLocales || [])
+ primaryLanguage: deviceLanguageCodes[0] || 'en',
+ contentLanguages: deviceLanguageCodes || [],
+ postLanguage: deviceLanguageCodes[0] || 'en',
+ postLanguageHistory: (deviceLanguageCodes || [])
.concat(['en', 'ja', 'pt', 'de'])
.slice(0, 6),
- appLanguage: deviceLocales[0] || 'en',
+ // try full language tag first, then fallback to language code
+ appLanguage: findSupportedAppLanguage([
+ deviceLocales.at(0)?.languageTag,
+ deviceLanguageCodes[0],
+ ]),
},
requireAltTextEnabled: false,
largeAltBadgeEnabled: false,
diff --git a/src/state/persisted/util.ts b/src/state/persisted/util.ts
new file mode 100644
index 0000000000..64a8bf9459
--- /dev/null
+++ b/src/state/persisted/util.ts
@@ -0,0 +1,51 @@
+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 abf78da3ce..acc0467715 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} from '#/lib/moderation'
+import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useAgent} from '#/state/session'
@@ -113,6 +113,10 @@ function computeSuggestions({
return items.filter(profile => {
const modui = moderateProfile(profile, moderationOpts).ui('profileList')
const isExactMatch = q && profile.handle.toLowerCase() === q
- return isExactMatch || !modui.filter || isJustAMute(modui)
+ return (
+ (isExactMatch && !moduiContainsHideableOffense(modui)) ||
+ !modui.filter ||
+ isJustAMute(modui)
+ )
})
}
diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts
index e0ee02294e..a251d170ec 100644
--- a/src/state/queries/notifications/util.ts
+++ b/src/state/queries/notifications/util.ts
@@ -13,6 +13,7 @@ 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'
@@ -104,6 +105,10 @@ 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 3e22c262cb..44c5cf9343 100644
--- a/src/state/session/__tests__/session-test.ts
+++ b/src/state/session/__tests__/session-test.ts
@@ -10,6 +10,10 @@ 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
new file mode 100644
index 0000000000..c594294b2a
--- /dev/null
+++ b/src/state/session/additional-moderation-authorities.ts
@@ -0,0 +1,41 @@
+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 d8ded90f69..01684fe0ba 100644
--- a/src/state/session/moderation.ts
+++ b/src/state/session/moderation.ts
@@ -1,6 +1,7 @@
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'
@@ -8,6 +9,7 @@ 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(
@@ -31,6 +33,8 @@ 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 6755ec9a66..8e12386bd3 100644
--- a/src/state/shell/composer/index.tsx
+++ b/src/state/shell/composer/index.tsx
@@ -9,6 +9,7 @@ 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 {
@@ -77,7 +78,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const closeComposer = useNonReactiveCallback(() => {
let wasOpen = !!state
- setState(undefined)
+ if (wasOpen) {
+ setState(undefined)
+ purgeTemporaryImageFiles()
+ }
+
return wasOpen
})
diff --git a/src/storage/index.ts b/src/storage/index.ts
index 4be08170dd..7ef226d3aa 100644
--- a/src/storage/index.ts
+++ b/src/storage/index.ts
@@ -1,5 +1,6 @@
import {MMKV} from 'react-native-mmkv'
+import {IS_DEV} from '#/env'
import {Device} from '#/storage/schema'
export * from '#/storage/schema'
@@ -71,4 +72,11 @@ export class Storage {
*
* `device.set([key], true)`
*/
-export const device = new Storage<[], Device>({id: 'device'})
+export const device = new Storage<[], Device>({id: 'bsky_device'})
+
+if (IS_DEV && typeof window !== 'undefined') {
+ // @ts-ignore
+ window.bsky_storage = {
+ device,
+ }
+}
diff --git a/src/storage/schema.ts b/src/storage/schema.ts
index 1a9656fede..cf410c77de 100644
--- a/src/storage/schema.ts
+++ b/src/storage/schema.ts
@@ -5,4 +5,7 @@ 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
new file mode 100644
index 0000000000..980d92ef77
--- /dev/null
+++ b/src/style.css
@@ -0,0 +1,355 @@
+/**
+ * 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 8eac1ab82f..a18f17612e 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,8 +35,7 @@ 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 9ffcbfb9df..1fd62e1d3f 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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+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'
@@ -78,11 +78,7 @@ export const SplashScreen = ({
)}
+ style={[a.text_md, a.font_bold, 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 0d64650ddb..fb69e1d9c7 100644
--- a/src/view/com/auth/server-input/index.tsx
+++ b/src/view/com/auth/server-input/index.tsx
@@ -3,14 +3,15 @@ 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({
@@ -153,9 +154,13 @@ export function ServerInputDialog({
]}>
Bluesky is an open network where you can choose your hosting
- provider. Custom hosting is now available in beta for
- developers.
-
+ provider. If you're a developer, you can host your own server.
+ {' '}
+
+ Learn more.
+
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index dfdfb3ebdf..3b7cf13851 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -44,7 +44,6 @@ 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'
@@ -68,9 +67,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,
@@ -122,12 +121,14 @@ 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 = observer(function ComposePost({
+export const ComposePost = ({
replyTo,
onPost,
quote: initQuote,
@@ -139,7 +140,7 @@ export const ComposePost = observer(function ComposePost({
cancelRef,
}: Props & {
cancelRef?: React.RefObject
-}) {
+}) => {
const {currentAccount} = useSession()
const agent = useAgent()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
@@ -212,9 +213,8 @@ export const ComposePost = observer(function ComposePost({
)
const [postgate, setPostgate] = useState(createPostgateRecord({post: ''}))
- const gallery = useMemo(
- () => new GalleryModel(initImageUris),
- [initImageUris],
+ const [images, setImages] = useState(() =>
+ createInitialImages(initImageUris),
)
const onClose = useCallback(() => {
closeComposer()
@@ -233,7 +233,7 @@ export const ComposePost = observer(function ComposePost({
const onPressCancel = useCallback(() => {
if (
graphemeLength > 0 ||
- !gallery.isEmpty ||
+ images.length !== 0 ||
extGif ||
videoUploadState.status !== 'idle'
) {
@@ -246,7 +246,7 @@ export const ComposePost = observer(function ComposePost({
}, [
extGif,
graphemeLength,
- gallery.isEmpty,
+ images.length,
closeAllDialogs,
discardPromptControl,
onClose,
@@ -299,22 +299,31 @@ export const ComposePost = observer(function 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 {
- await gallery.paste(uri)
+ const res = await pasteImage(uri)
+ onImageAdd([res])
}
},
- [gallery, track, selectVideo],
+ [track, selectVideo, onImageAdd],
)
const isAltTextRequiredAndMissing = useMemo(() => {
if (!requireAltTextEnabled) return false
- if (gallery.needsAltText) return true
+ if (images.some(img => img.alt === '')) return true
+
if (extGif) {
if (!extLink?.meta?.description) return true
@@ -322,7 +331,7 @@ export const ComposePost = observer(function ComposePost({
if (!parsedAlt.isPreferred) return true
}
return false
- }, [gallery.needsAltText, extLink, extGif, requireAltTextEnabled])
+ }, [images, extLink, extGif, requireAltTextEnabled])
const onPressPublish = React.useCallback(
async (finishedUploading?: boolean) => {
@@ -347,7 +356,7 @@ export const ComposePost = observer(function ComposePost({
if (
richtext.text.trim().length === 0 &&
- gallery.isEmpty &&
+ images.length === 0 &&
!extLink &&
!quote &&
videoUploadState.status === 'idle'
@@ -368,7 +377,7 @@ export const ComposePost = observer(function ComposePost({
await apilib.post(agent, {
rawText: richtext.text,
replyTo: replyTo?.uri,
- images: gallery.images,
+ images,
quote,
extLink,
labels,
@@ -405,7 +414,7 @@ export const ComposePost = observer(function ComposePost({
} catch (e: any) {
logger.error(e, {
message: `Composer: create post failed`,
- hasImages: gallery.size > 0,
+ hasImages: images.length > 0,
})
if (extLink) {
@@ -427,7 +436,7 @@ export const ComposePost = observer(function ComposePost({
} finally {
if (postUri) {
logEvent('post:create', {
- imageCount: gallery.size,
+ imageCount: images.length,
isReply: replyTo != null,
hasLink: extLink != null,
hasQuote: quote != null,
@@ -436,7 +445,7 @@ export const ComposePost = observer(function ComposePost({
})
}
track('Create Post', {
- imageCount: gallery.size,
+ imageCount: images.length,
})
if (replyTo && replyTo.uri) track('Post:Reply')
}
@@ -472,9 +481,7 @@ export const ComposePost = observer(function ComposePost({
agent,
captions,
extLink,
- gallery.images,
- gallery.isEmpty,
- gallery.size,
+ images,
graphemeLength,
isAltTextRequiredAndMissing,
isProcessing,
@@ -516,12 +523,12 @@ export const ComposePost = observer(function ComposePost({
: _(msg`What's up?`)
const canSelectImages =
- gallery.size < 4 &&
+ images.length < MAX_IMAGES &&
!extLink &&
videoUploadState.status === 'idle' &&
!videoUploadState.video
const hasMedia =
- gallery.size > 0 || Boolean(extLink) || Boolean(videoUploadState.video)
+ images.length > 0 || Boolean(extLink) || Boolean(videoUploadState.video)
const onEmojiButtonPress = useCallback(() => {
openEmojiPicker?.(textInput.current?.getCursorPosition())
@@ -716,8 +723,8 @@ export const ComposePost = observer(function ComposePost({
/>
-
- {gallery.isEmpty && extLink && (
+
+ {images.length === 0 && extLink && (
) : (
-
+
-
+
)
-})
+}
export function useComposerCancelRef() {
return useRef(null)
diff --git a/src/view/com/composer/ComposerReplyTo.tsx b/src/view/com/composer/ComposerReplyTo.tsx
index d4ba1f3a86..cf4d8c5600 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 {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 {atoms as a, useTheme} from '#/alf'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const t = useTheme()
@@ -122,94 +122,87 @@ function ComposerReplyToImages({
showFull: boolean
}) {
return (
-
-
- {(images.length === 1 && (
-
+
+ {(images.length === 1 && (
+
+ )) ||
+ (images.length === 2 && (
+
+
+
+
)) ||
- (images.length === 2 && (
-
+ (images.length === 3 && (
+
+
+
+
+
+
+
+ )) ||
+ (images.length === 4 && (
+
+
- )) ||
- (images.length === 3 && (
-
+
+
-
-
-
-
- )) ||
- (images.length === 4 && (
-
-
-
-
-
-
-
-
-
-
- ))}
-
+
+ ))}
)
}
@@ -240,23 +233,7 @@ const styles = StyleSheet.create({
borderRadius: 6,
overflow: 'hidden',
marginTop: 2,
- },
- imagesInner: {
- gap: 2,
- },
- imagesRow: {
- flexDirection: 'row',
- },
- singleImage: {
- width: 65,
- height: 65,
- },
- doubleImageTall: {
- width: 32.5,
- height: 65,
- },
- doubleImage: {
- width: 32.5,
- height: 32.5,
+ height: 64,
+ width: 64,
},
})
diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx
index 4801ca0abf..f48e50cfd7 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?.path,
+ thumb: link.localThumb?.source.path,
},
[link],
)
diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx
index b7690e1023..a05607c76c 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?.path,
+ thumb: linkProp.localThumb?.source.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
new file mode 100644
index 0000000000..4263587fd4
--- /dev/null
+++ b/src/view/com/composer/photos/EditImageDialog.tsx
@@ -0,0 +1,14 @@
+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
new file mode 100644
index 0000000000..0afb83ed96
--- /dev/null
+++ b/src/view/com/composer/photos/EditImageDialog.web.tsx
@@ -0,0 +1,105 @@
+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 7ff1b7b9ab..369f08d745 100644
--- a/src/view/com/composer/photos/Gallery.tsx
+++ b/src/view/com/composer/photos/Gallery.tsx
@@ -1,29 +1,38 @@
-import React, {useState} from 'react'
-import {ImageStyle, Keyboard, LayoutChangeEvent} from 'react-native'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import React from 'react'
+import {
+ ImageStyle,
+ Keyboard,
+ LayoutChangeEvent,
+ StyleSheet,
+ TouchableOpacity,
+ View,
+ ViewStyle,
+} 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 {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 {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 {useTheme} from '#/alf'
+import * as Dialog from '#/components/Dialog'
+import {EditImageDialog} from './EditImageDialog'
+import {ImageAltTextDialog} from './ImageAltTextDialog'
const IMAGE_GAP = 8
interface GalleryProps {
- gallery: GalleryModel
+ images: ComposerImage[]
+ onChange: (next: ComposerImage[]) => void
}
-export const Gallery = (props: GalleryProps) => {
- const [containerInfo, setContainerInfo] = useState()
+export let Gallery = (props: GalleryProps): React.ReactNode => {
+ const [containerInfo, setContainerInfo] = React.useState()
const onLayout = (evt: LayoutChangeEvent) => {
const {width, height} = evt.nativeEvent.layout
@@ -41,177 +50,200 @@ export const Gallery = (props: GalleryProps) => {
)
}
+Gallery = React.memo(Gallery)
interface GalleryInnerProps extends GalleryProps {
containerInfo: Dimensions
}
-const GalleryInner = observer(function GalleryImpl({
- gallery,
- containerInfo,
-}: GalleryInnerProps) {
- const {_} = useLingui()
+const GalleryInner = ({images, containerInfo, onChange}: GalleryInnerProps) => {
const {isMobile} = useWebMediaQueries()
- const {openModal} = useModalControls()
- const t = useTheme()
- let side: number
+ const {altTextControlStyle, imageControlsStyle, imageStyle} =
+ React.useMemo(() => {
+ const side =
+ images.length === 1
+ ? 250
+ : (containerInfo.width - IMAGE_GAP * (images.length - 1)) /
+ images.length
- if (gallery.size === 1) {
- side = 250
- } else {
- side = (containerInfo.width - IMAGE_GAP * (gallery.size - 1)) / gallery.size
- }
+ const isOverflow = isMobile && images.length > 2
- 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,
+ 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,
+ },
}
+ }, [images.length, containerInfo, isMobile])
- 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 ? (
+ return images.length !== 0 ? (
<>
- {gallery.images.map(image => (
-
- {
- Keyboard.dismiss()
- openModal({
- name: 'alt-text-image',
- image,
- })
+ {images.map((image, index) => {
+ return (
+ {
+ onChange(
+ images.map(i => (i.source === image.source ? next : i)),
+ )
}}
- 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}
- />
+ onRemove={() => {
+ const next = images.slice()
+ next.splice(index, 1)
-
-
- ))}
+ )
+ })}
>
) : 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()
@@ -263,7 +295,7 @@ const styles = StyleSheet.create({
altTextControlLabel: {
color: 'white',
fontSize: 12,
- fontWeight: 'bold',
+ fontWeight: '600',
letterSpacing: 1,
},
altTextHiddenRegion: {
diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx
new file mode 100644
index 0000000000..123e1066a5
--- /dev/null
+++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx
@@ -0,0 +1,121 @@
+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 f1f984103e..2183ca7902 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 {GalleryModel} from '#/state/models/media/gallery'
+import {ComposerImage, createComposerImage} from '#/state/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({gallery, disabled}: Props) {
+export function OpenCameraBtn({disabled, onAdd}: Props) {
const {track} = useAnalytics()
const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission()
@@ -48,13 +48,16 @@ export function OpenCameraBtn({gallery, disabled}: Props) {
if (mediaPermissionRes) {
await MediaLibrary.createAssetAsync(img.path)
}
- gallery.add(img)
+
+ const res = await createComposerImage(img)
+
+ onAdd([res])
} catch (err: any) {
// ignore
logger.warn('Error using camera', {error: err})
}
}, [
- gallery,
+ onAdd,
track,
requestCameraAccessIfNeeded,
mediaPermissionRes,
diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx
index 747653fc8d..95d2df022c 100644
--- a/src/view/com/composer/photos/SelectPhotoBtn.tsx
+++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx
@@ -5,18 +5,20 @@ 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 {GalleryModel} from '#/state/models/media/gallery'
+import {ComposerImage, createComposerImage} from '#/state/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 = {
- gallery: GalleryModel
+ size: number
disabled?: boolean
+ onAdd: (next: ComposerImage[]) => void
}
-export function SelectPhotoBtn({gallery, disabled}: Props) {
+export function SelectPhotoBtn({size, disabled, onAdd}: Props) {
const {track} = useAnalytics()
const {_} = useLingui()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
@@ -29,8 +31,17 @@ export function SelectPhotoBtn({gallery, disabled}: Props) {
return
}
- gallery.pick()
- }, [track, requestPhotoAccessIfNeeded, gallery])
+ 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])
return (
(null)
const textInputSelection = useRef({start: 0, end: 0})
const theme = useTheme()
@@ -180,25 +180,57 @@ 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}
-
+
)
})
- }, [richtext, pal.link, pal.text])
+ }, [t, richtext, inputTextStyle])
return (
-
+
{textDecorated}
@@ -229,24 +256,3 @@ 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 3db25746f3..77f69fa890 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -13,16 +13,18 @@ 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'
+} 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'
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'
@@ -58,6 +60,7 @@ 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')
@@ -247,13 +250,32 @@ 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 29b8f0bc65..a43e67c044 100644
--- a/src/view/com/composer/text-input/web/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/web/Autocomplete.tsx
@@ -5,19 +5,20 @@ 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 {usePalette} from 'lib/hooks/usePalette'
-import {Text} from 'view/com/util/text/Text'
-import {UserAvatar} from 'view/com/util/UserAvatar'
+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
@@ -180,7 +181,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 666473afd9..33d4dbc6c1 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(localThumb => {
+ .then(thumb => (thumb ? createComposerImage(thumb) : undefined))
+ .then(thumb => {
if (aborted) {
return
}
setExtLink({
...extLink,
isLoading: false, // done
- localThumb: localThumb ? new ImageModel(localThumb) : undefined,
+ localThumb: thumb,
})
})
return cleanup
diff --git a/src/view/com/composer/videos/SubtitleDialog.tsx b/src/view/com/composer/videos/SubtitleDialog.tsx
index 10c2d75642..c07fdfc562 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="xsmall"
+ size="small"
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 856a0eb4fc..44a6b53b6f 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 68437c37a0..3276cf8821 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -12,6 +12,10 @@ 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'
@@ -21,12 +25,8 @@ 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
deleted file mode 100644
index ba489cde7b..0000000000
--- a/src/view/com/modals/AltImage.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-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 3088c92a1d..7717f597dd 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: 'bold',
+ fontWeight: '600',
fontSize: 24,
marginBottom: 18,
},
@@ -373,7 +373,7 @@ const styles = StyleSheet.create({
marginTop: 20,
},
label: {
- fontWeight: 'bold',
+ fontWeight: '600',
},
form: {
paddingHorizontal: 6,
diff --git a/src/view/com/modals/CropImage.web.tsx b/src/view/com/modals/CropImage.web.tsx
new file mode 100644
index 0000000000..41ca306573
--- /dev/null
+++ b/src/view/com/modals/CropImage.web.tsx
@@ -0,0 +1,145 @@
+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
deleted file mode 100644
index b39dcd9364..0000000000
--- a/src/view/com/modals/EditImage.tsx
+++ /dev/null
@@ -1,402 +0,0 @@
-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 4b94aeb42f..beea3ca1a8 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: 'bold',
+ fontWeight: '600',
fontSize: 24,
marginBottom: 18,
},
label: {
- fontWeight: 'bold',
+ fontWeight: '600',
paddingHorizontal: 4,
paddingBottom: 4,
marginTop: 20,
diff --git a/src/view/com/modals/InAppBrowserConsent.tsx b/src/view/com/modals/InAppBrowserConsent.tsx
index 3fa5159346..37b039c605 100644
--- a/src/view/com/modals/InAppBrowserConsent.tsx
+++ b/src/view/com/modals/InAppBrowserConsent.tsx
@@ -1,19 +1,18 @@
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]
@@ -89,7 +88,7 @@ export function Component({href}: {href: string}) {
const styles = StyleSheet.create({
title: {
textAlign: 'center',
- fontWeight: 'bold',
+ fontWeight: '600',
fontSize: 24,
marginBottom: 12,
},
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 3455e1cdf8..90e93821c5 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -3,13 +3,11 @@ 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'
@@ -75,12 +73,6 @@ 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 c4bab6fb18..a2acc23bb9 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -2,20 +2,18 @@ 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 './crop-image/CropImage.web'
+import * as CropImageModal from './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'
@@ -54,11 +52,7 @@ function Modal({modal}: {modal: ModalIface}) {
}
const onPressMask = () => {
- if (
- modal.name === 'crop-image' ||
- modal.name === 'edit-image' ||
- modal.name === 'alt-text-image'
- ) {
+ if (modal.name === 'crop-image') {
return // dont close on mask presses during crop
}
closeModal()
@@ -93,10 +87,6 @@ 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 f6db94ed85..b0b76644f0 100644
--- a/src/view/com/modals/UserAddRemoveLists.tsx
+++ b/src/view/com/modals/UserAddRemoveLists.tsx
@@ -9,7 +9,12 @@ 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,
@@ -19,11 +24,6 @@ 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,21 +65,27 @@ 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
deleted file mode 100644
index 303d15ba5b..0000000000
--- a/src/view/com/modals/crop-image/cropImageUtil.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-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 b8c125b65c..360cc0e404 100644
--- a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
+++ b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx
@@ -1,19 +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 {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%']
@@ -37,10 +38,10 @@ export function Component({}: {}) {
langs.sort((a, b) => {
const hasA =
langPrefs.contentLanguages.includes(a.code2) ||
- deviceLocales.includes(a.code2)
+ deviceLanguageCodes.includes(a.code2)
const hasB =
langPrefs.contentLanguages.includes(b.code2) ||
- deviceLocales.includes(b.code2)
+ deviceLanguageCodes.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1
return 1
@@ -110,7 +111,7 @@ const styles = StyleSheet.create({
},
title: {
textAlign: 'center',
- fontWeight: 'bold',
+ fontWeight: '600',
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 05cfb81156..2b0eb8cf24 100644
--- a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
+++ b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx
@@ -1,20 +1,21 @@
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%']
@@ -38,10 +39,10 @@ export function Component() {
langs.sort((a, b) => {
const hasA =
hasPostLanguage(langPrefs.postLanguage, a.code2) ||
- deviceLocales.includes(a.code2)
+ deviceLanguageCodes.includes(a.code2)
const hasB =
hasPostLanguage(langPrefs.postLanguage, b.code2) ||
- deviceLocales.includes(b.code2)
+ deviceLanguageCodes.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1
return 1
@@ -118,7 +119,7 @@ const styles = StyleSheet.create({
},
title: {
textAlign: 'center',
- fontWeight: 'bold',
+ fontWeight: '600',
fontSize: 24,
marginBottom: 12,
},
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index f5ab2608a8..669fd9bdee 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 {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 {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,7 +183,11 @@ let FeedItem = ({
key={authors[0].href}
style={[pal.text, s.bold]}
href={authors[0].href}
- text={forceLTR(firstAuthorName)}
+ text={
+
+ {forceLTR(firstAuthorName)}
+
+ }
disableMismatchWarning
/>
)
@@ -547,7 +551,7 @@ function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileViewBasic}) {
label={_(msg`Say hello!`)}
variant="ghost"
color="primary"
- size="xsmall"
+ size="small"
style={[a.self_center, {marginLeft: 'auto'}]}
disabled={isLoading}
onPress={async () => {
@@ -705,12 +709,13 @@ 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, '@')}
@@ -727,7 +732,11 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
return (
<>
- {text?.length > 0 && {text}}
+ {text?.length > 0 && (
+
+ {text}
+
+ )}
(itemRefs.current[i] = node)}
+ ref={node => (itemRefs.current[i] = node as any)}
onLayout={e => onItemLayout(e, i)}
style={styles.item}
hoverStyle={pal.viewLight}
onPress={() => onPressItem(i)}>
-
+
{sanitizeHandle(post.author.handle, '@')}
@@ -553,18 +558,14 @@ let PostThreadItemLoaded = ({
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 9033fb96f7..ec730a5e16 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 7537a46448..fb9cdb065e 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'),
+ )}
+
+ }
href={makeProfileLink(reason.by)}
onBeforePress={onOpenReposter}
/>
@@ -337,7 +345,7 @@ let FeedItemInner = ({
+ {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 0920026f60..dc68ee7a17 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: 52,
+ width: 42,
alignItems: 'center',
},
})
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index fd32e37a42..eab8611dd4 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,6 +103,7 @@ 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 e07acef281..d6995749bf 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: 'bold'}]}
+ style={[pal.text, {fontWeight: '600'}]}
text={title || ''}
onPress={emitSoftReset}
numberOfLines={4}
diff --git a/src/view/com/util/Html.tsx b/src/view/com/util/Html.tsx
index 2e47194811..f77fb16034 100644
--- a/src/view/com/util/Html.tsx
+++ b/src/view/com/util/Html.tsx
@@ -1,16 +1,17 @@
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 {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+
+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'
/**
* These utilities are used to define long documents in an html-like
@@ -166,7 +167,7 @@ const useStyles = () => {
h4: {
marginTop: 0,
marginBottom: 10,
- fontWeight: 'bold',
+ fontWeight: '600',
},
p: {
marginBottom: 10,
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index 6e75e88ca6..6620eb8e28 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 {i18n} = useLingui()
+ const t = useTheme()
+ 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)
@@ -53,9 +49,18 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
}, [queryClient, opts.author])
return (
-
+
{opts.showAvatar && (
-
+
{
)}
-
-
+
-
+
+ {forceLTR(
+ sanitizeDisplayName(
+ displayName,
+ opts.moderation?.ui('displayName'),
+ ),
+ )}
+
+
+
+ disableUnderline
+ onPress={onBeforePressAuthor}
+ style={[a.text_md, t.atoms.text_contrast_medium, a.leading_tight]}>
+
+ {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
+
+
- {!isAndroid && (
-
- ·
-
- )}
+
+
+ ·
+
+
{({timeElapsed}) => (
-
+ disableMismatchWarning
+ disableUnderline
+ onPress={onBeforePressPost}
+ style={[
+ a.text_md,
+ t.atoms.text_contrast_medium,
+ a.leading_tight,
+ web({
+ whiteSpace: 'nowrap',
+ }),
+ ]}>
+ {timeElapsed}
+
)}
@@ -117,21 +138,3 @@ 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 77276f1843..48659e2295 100644
--- a/src/view/com/util/PressableWithHover.tsx
+++ b/src/view/com/util/PressableWithHover.tsx
@@ -1,39 +1,35 @@
-import React, {
- useState,
- useCallback,
- PropsWithChildren,
- forwardRef,
- Ref,
-} from 'react'
+import React, {forwardRef, PropsWithChildren} from 'react'
import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
-import {addStyle} from 'lib/styles'
+import {View} from 'react-native'
+
+import {addStyle} from '#/lib/styles'
+import {useInteractionState} from '#/components/hooks/useInteractionState'
interface PressableWithHover extends PressableProps {
hoverStyle: StyleProp
}
-export const PressableWithHover = forwardRef(function PressableWithHoverImpl(
- {
- children,
- style,
- hoverStyle,
- ...props
- }: PropsWithChildren,
- ref: Ref,
+export const PressableWithHover = forwardRef<
+ View,
+ PropsWithChildren
+>(function PressableWithHoverImpl(
+ {children, style, hoverStyle, ...props},
+ ref,
) {
- 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
+ const {
+ state: hovered,
+ onIn: onHoverIn,
+ onOut: onHoverOut,
+ } = useInteractionState()
return (
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index b2f56c1385..76d9d1503e 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 {logger} from '#/logger'
-import {usePalette} from 'lib/hooks/usePalette'
+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 {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 {logger} from '#/logger'
+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,6 +321,8 @@ 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 93ea32750d..13f4081fce 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 {logger} from '#/logger'
-import {usePalette} from 'lib/hooks/usePalette'
+import {usePalette} from '#/lib/hooks/usePalette'
import {
useCameraPermission,
usePhotoLibraryPermission,
-} 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'
+} 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'
import {tokens, useTheme as useAlfTheme} from '#/alf'
import {
Camera_Filled_Stroke2_Corner0_Rounded as CameraFilled,
@@ -72,6 +72,7 @@ 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 9cb9997f60..8a444d5901 100644
--- a/src/view/com/util/UserInfoText.tsx
+++ b/src/view/com/util/UserInfoText.tsx
@@ -1,15 +1,16 @@
import React from 'react'
-import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
import {StyleProp, StyleSheet, TextStyle} from 'react-native'
-import {TextLinkOnWebOnly} from './Link'
-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 {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 {TextLinkOnWebOnly} from './Link'
+import {LoadingPlaceholder} from './LoadingPlaceholder'
+import {Text} from './text/Text'
export function UserInfoText({
type = 'md',
@@ -50,11 +51,15 @@ 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 ca417034db..e5121b350a 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: 'bold',
+ fontWeight: '600',
},
subtitle: {
fontSize: 13,
diff --git a/src/view/com/util/fab/FABInner.tsx b/src/view/com/util/fab/FABInner.tsx
index ee8e1f47a2..5d8aac81af 100644
--- a/src/view/com/util/fab/FABInner.tsx
+++ b/src/view/com/util/fab/FABInner.tsx
@@ -4,11 +4,13 @@ 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
@@ -17,9 +19,11 @@ export interface FABProps
icon: JSX.Element
}
-export function FABInner({testID, icon, ...props}: FABProps) {
+export function FABInner({testID, icon, onPress, ...props}: FABProps) {
const insets = useSafeAreaInsets()
const {isMobile, isTablet} = useWebMediaQueries()
+ const playHaptic = useHaptics()
+ const isHapticsDisabled = useHapticsDisabled()
const fabMinimalShellTransform = useMinimalShellFabTransform()
const {
state: pressed,
@@ -42,6 +46,15 @@ export function FABInner({testID, icon, ...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 ? '500' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
},
secondary: {
color: theme.palette.secondary.text,
- fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
},
inverted: {
color: theme.palette.inverted.text,
- fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.inverted.isLowContrast ? '600' : undefined,
},
'primary-outline': {
color: theme.palette.primary.textInverted,
- fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
},
'secondary-outline': {
color: theme.palette.secondary.textInverted,
- fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
},
'primary-light': {
color: theme.palette.primary.textInverted,
- fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
},
'secondary-light': {
color: theme.palette.secondary.textInverted,
- fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
},
default: {
color: theme.palette.default.text,
- fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.default.isLowContrast ? '600' : undefined,
},
'default-light': {
color: theme.palette.default.text,
- fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.default.isLowContrast ? '600' : undefined,
},
})
return (
diff --git a/src/view/com/util/forms/ToggleButton.tsx b/src/view/com/util/forms/ToggleButton.tsx
index c98e846cd3..706796fc40 100644
--- a/src/view/com/util/forms/ToggleButton.tsx
+++ b/src/view/com/util/forms/ToggleButton.tsx
@@ -1,11 +1,12 @@
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,
@@ -100,39 +101,39 @@ export function ToggleButton({
const labelStyle = choose>(type, {
primary: {
color: theme.palette.primary.text,
- fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
},
secondary: {
color: theme.palette.secondary.text,
- fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
},
inverted: {
color: theme.palette.inverted.text,
- fontWeight: theme.palette.inverted.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.inverted.isLowContrast ? '600' : undefined,
},
'primary-outline': {
color: theme.palette.primary.textInverted,
- fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
},
'secondary-outline': {
color: theme.palette.secondary.textInverted,
- fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
},
'primary-light': {
color: theme.palette.primary.textInverted,
- fontWeight: theme.palette.primary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
},
'secondary-light': {
color: theme.palette.secondary.textInverted,
- fontWeight: theme.palette.secondary.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
},
default: {
color: theme.palette.default.text,
- fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.default.isLowContrast ? '600' : undefined,
},
'default-light': {
color: theme.palette.default.text,
- fontWeight: theme.palette.default.isLowContrast ? '500' : undefined,
+ fontWeight: theme.palette.default.isLowContrast ? '600' : undefined,
},
})
return (
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index 9abbe2875f..a9bfc1c966 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 GalleryItemProps {
+interface Props {
images: AppBskyEmbedImages.ViewImage[]
index: number
onPress?: EventFunction
onLongPress?: EventFunction
onPressIn?: EventFunction
- imageStyle?: ComponentProps['style']
+ imageStyle?: StyleProp
viewContext?: PostEmbedViewContext
+ insetBorderStyle?: StyleProp
}
-export const GalleryItem: FC = ({
+export function GalleryItem({
images,
index,
imageStyle,
@@ -31,7 +32,8 @@ export const GalleryItem: FC = ({
onPressIn,
onLongPress,
viewContext,
-}) => {
+ insetBorderStyle,
+}: Props) {
const t = useTheme()
const {_} = useLingui()
const largeAltBadge = useLargeAltBadgeEnabled()
@@ -47,7 +49,6 @@ export const GalleryItem: FC = ({
onLongPress={onLongPress ? () => onLongPress(index) : undefined}
style={[
a.flex_1,
- a.rounded_sm,
a.overflow_hidden,
t.atoms.bg_contrast_25,
imageStyle,
@@ -63,7 +64,7 @@ export const GalleryItem: FC = ({
accessibilityHint=""
accessibilityIgnoresInvertColors
/>
-
+
{hasAlt && !hideBadges ? (
-
+
@@ -54,10 +63,18 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
return (
-
+
-
+
)
@@ -65,15 +82,35 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
case 3:
return (
-
-
+
+
-
-
+
+
-
-
+
+
@@ -83,19 +120,51 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
return (
<>
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
>
@@ -105,3 +174,22 @@ 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 8c4928dfcd..0ecdf25b93 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="medium"
+ size="large"
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 1f966d7107..6f1c88dcdf 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_sm,
+ a.rounded_md,
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 e6ab86f9c5..98332c33b0 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 64ea0029fa..6d5eacd1a0 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_sm,
+ a.rounded_md,
a.overflow_hidden,
{
borderBottomLeftRadius: 0,
@@ -245,7 +245,7 @@ export function ExternalPlayer({
/>
+
@@ -132,7 +132,7 @@ export function GifEmbed({
@@ -293,13 +299,6 @@ 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 267b5d1843..24802d1882 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 908c06e221..3180dd99eb 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_sm,
- a.my_xs,
+ a.rounded_md,
+ a.mt_xs,
]}>
(null)
- const ref = useRef
(null)
+ const videoRef = useRef(null)
const [focused, setFocused] = useState(false)
const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false)
const figId = useId()
@@ -30,64 +31,24 @@ export function VideoEmbedInnerWeb({
throw error
}
- 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])
+ const hlsRef = useHLS({
+ focused,
+ playlist: embed.playlist,
+ setHasSubtitleTrack,
+ setError,
+ videoRef,
+ })
return (
-
+
@@ -110,7 +71,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 36b32a0725..8ffe482a8f 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} from '#/alf'
-import {Button} from '#/components/Button'
+import {atoms as a, useTheme, web} from '#/alf'
+import {PressableWithHover} from '../../../PressableWithHover'
export function ControlButton({
active,
@@ -21,19 +21,21 @@ export function ControlButton({
}) {
const t = useTheme()
return (
-
+ style={[
+ a.p_xs,
+ a.rounded_full,
+ web({transition: 'background-color 0.1s'}),
+ ]}
+ hoverStyle={{backgroundColor: 'rgba(255, 255, 255, 0.2)'}}>
{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 5bd7e0d179..2d1427347d 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,9 +358,8 @@ export function Controls({
style={[
a.flex_1,
a.px_xs,
- a.pt_2xs,
- a.pb_md,
- a.gap_md,
+ a.pb_sm,
+ a.gap_sm,
a.flex_row,
a.align_center,
]}>
@@ -373,7 +372,11 @@ 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 b4a6cf8251..d4982b0e27 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 {usePalette} from 'lib/hooks/usePalette'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
+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,9 +247,6 @@ function MaybeListCard({view}: {view: AppBskyGraphDefs.ListView}) {
}
const styles = StyleSheet.create({
- container: {
- marginTop: 8,
- },
altContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,
@@ -262,7 +259,7 @@ const styles = StyleSheet.create({
alt: {
color: 'white',
fontSize: 7,
- fontWeight: 'bold',
+ fontWeight: '600',
},
customFeedOuter: {
borderWidth: StyleSheet.hairlineWidth,
diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx
index 52a45b0e2e..3d885480cc 100644
--- a/src/view/com/util/text/Text.tsx
+++ b/src/view/com/util/text/Text.tsx
@@ -2,27 +2,40 @@ 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 {isIOS, isWeb} from 'platform/detection'
+import {lh, s} from '#/lib/styles'
+import {TypographyVariant, useTheme} from '#/lib/ThemeContext'
+import {logger} from '#/logger'
+import {isIOS} from '#/platform/detection'
import {applyFonts, useAlf} from '#/alf'
+import {
+ childHasEmoji,
+ childIsString,
+ renderChildrenWithEmoji,
+ StringChild,
+} from '#/components/Typography'
+import {IS_DEV} from '#/env'
-export type CustomTextProps = TextProps & {
+export type CustomTextProps = Omit & {
type?: TypographyVariant
lineHeight?: number
title?: string
dataSet?: Record
selectable?: boolean
-}
-
-const fontFamilyStyle = {
- fontFamily:
- '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif',
-}
+} & (
+ | {
+ emoji: true
+ children: StringChild
+ }
+ | {
+ emoji?: false
+ children: TextProps['children']
+ }
+ )
export function Text({
type = 'md',
children,
+ emoji,
lineHeight,
style,
title,
@@ -35,6 +48,18 @@ 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,
@@ -58,7 +83,7 @@ export function Text({
selectable={selectable}
uiTextView
{...props}>
- {children}
+ {isIOS && emoji ? renderChildrenWithEmoji(children) : children}
)
}
@@ -66,7 +91,6 @@ export function Text({
const flattened = StyleSheet.flatten([
s.black,
typography,
- isWeb && fontFamilyStyle,
lineHeightStyle,
style,
])
@@ -87,7 +111,7 @@ export function Text({
dataSet={Object.assign({tooltip: title}, dataSet || {})}
selectable={selectable}
{...props}>
- {children}
+ {isIOS && emoji ? renderChildrenWithEmoji(children) : children}
)
}
diff --git a/src/view/com/util/text/ThemedText.tsx b/src/view/com/util/text/ThemedText.tsx
deleted file mode 100644
index 2844d273c2..0000000000
--- a/src/view/com/util/text/ThemedText.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-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 2992e5c7e9..158dc8b8da 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 0f27db5229..bd69d7a550 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 {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 {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: '500',
+ fontWeight: '600',
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: '500',
+ fontWeight: '600',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
@@ -147,7 +147,7 @@ export function LanguageSettingsScreen(_props: Props) {
fontSize: 14,
fontFamily: 'inherit',
letterSpacing: 0.5,
- fontWeight: '500',
+ fontWeight: '600',
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: '500',
+ fontWeight: '600',
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: '500',
+ fontWeight: '600',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
@@ -239,7 +239,7 @@ export function LanguageSettingsScreen(_props: Props) {
fontSize: 14,
fontFamily: 'inherit',
letterSpacing: 0.5,
- fontWeight: '500',
+ fontWeight: '600',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx
index 9daeaba187..d6a86e5143 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 b7d993acc7..39ba540b49 100644
--- a/src/view/screens/ModerationModlists.tsx
+++ b/src/view/screens/ModerationModlists.tsx
@@ -1,20 +1,21 @@
import React from 'react'
import {View} from 'react-native'
-import {useFocusEffect, useNavigation} from '@react-navigation/native'
-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 {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 {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 {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'
type Props = NativeStackScreenProps
export function ModerationModlistsScreen({}: Props) {
@@ -54,7 +55,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 ade7a53d90..8b3550d6b3 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 {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 {s} from '#/lib/styles'
import {
useExternalEmbedsPrefs,
useSetExternalEmbedPref,
-} from 'state/preferences'
-import {ToggleButton} from 'view/com/util/forms/ToggleButton'
+} from '#/state/preferences'
+import {useSetMinimalShellMode} from '#/state/shell'
+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 8aa4221e6c..085250e3bd 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 4a311f91ce..7a5a88869d 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 5ef6459810..810bbff889 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -16,9 +16,18 @@ 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'
@@ -26,29 +35,21 @@ 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 {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 {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 {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
@@ -107,7 +108,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 30d16506e0..07d762c0fe 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -24,11 +24,18 @@ 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'
@@ -40,13 +47,6 @@ 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: 'bold',
+ fontWeight: '600',
borderBottomWidth: 1,
},
]}>
@@ -959,6 +959,7 @@ function SearchHistory({
accessibilityIgnoresInvertColors
/>
{profile.displayName || profile.handle}
@@ -1134,7 +1135,7 @@ const styles = StyleSheet.create({
borderRadius: 8,
},
searchHistoryTitle: {
- fontWeight: 'bold',
+ fontWeight: '600',
paddingVertical: 12,
paddingHorizontal: 10,
},
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index fe449fcdbc..737ca2d28a 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -18,6 +18,18 @@ 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'
@@ -33,26 +45,14 @@ 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 {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 {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 2935103dfb..66040c2e3d 100644
--- a/src/view/screens/Storybook/Buttons.tsx
+++ b/src/view/screens/Storybook/Buttons.tsx
@@ -9,7 +9,6 @@ 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'
@@ -70,81 +69,115 @@ export function Buttons() {
),
)}
- {/*
-
- {['gradient_sunset', 'gradient_nordic', 'gradient_bonfire'].map(
- name => (
-
-
- Button
-
-
- Button
-
-
- ),
- )}
-
- */}
-
- Link out
-
+
+ Button
+
+
+ Button
+
-
- Link out
-
+
+ Button
+
+
+ Button
+
-
- Link xxxxxx
-
-
-
+
- Link out
-
-
-
-
- Link out
+ Button
-
+
+
+ Button
+
+
+ Button
+
+
+
+ Button
+
+
+
+
+
+
+
+
+
+
+
+
+ Button
+
+
+ Button
+
+
+
+
+
+
+
+
+
+
+
+
+ Button
+
+
+ Button
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+
-
+
+
@@ -91,16 +92,17 @@ function StorybookInner() {
+
-
+
)
-})
+}
function Providers({
children,
diff --git a/src/view/shell/Composer.tsx b/src/view/shell/Composer.tsx
index 1c97df9c39..049f35d35d 100644
--- a/src/view/shell/Composer.tsx
+++ b/src/view/shell/Composer.tsx
@@ -1,17 +1,12 @@
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 const Composer = observer(function ComposerImpl({
- winHeight,
-}: {
- winHeight: number
-}) {
+export function Composer({winHeight}: {winHeight: number}) {
const state = useComposerState()
const pal = usePalette('default')
const initInterp = useAnimatedValue(0)
@@ -62,7 +57,7 @@ export const Composer = observer(function ComposerImpl({
/>
)
-})
+}
const styles = StyleSheet.create({
wrapper: {
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index facead2c1e..226fe24966 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 {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 {formatCount} from '#/view/com/util/numeric/format'
+import {Text} from '#/view/com/util/text/Text'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
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: 'bold',
+ fontWeight: '600',
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 c575e3d9b1..9255957cb4 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: 'bold',
+ fontWeight: '600',
color: colors.white,
fontVariant: ['tabular-nums'],
},
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index 72e34ac469..2f5f954274 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 {usePalette} from 'lib/hooks/usePalette'
-import {getCurrentRoute} from 'lib/routes/helpers'
-import {NavigationProp} from 'lib/routes/types'
-import {TextLink} from 'view/com/util/Link'
+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 ? '500' : 'normal'},
+ {letterSpacing: 0.15, fontWeight: current ? '600' : '400'},
]}
/>
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index ca8073f573..6cceaccd92 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -12,7 +12,13 @@ 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'
@@ -20,18 +26,12 @@ 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 {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 {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 {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: 'bold',
+ fontWeight: '600',
paddingHorizontal: 4,
borderRadius: 6,
},
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index 1ba2d3f3db..b43dbcce32 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 {usePalette} from 'lib/hooks/usePalette'
-import {NavigationProp} from 'lib/routes/types'
-import {precacheProfile} from 'state/queries/profile'
+import {precacheProfile} from '#/state/queries/profile'
+import {SearchInput} from '#/view/com/util/forms/SearchInput'
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,6 +126,7 @@ 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 8902f7b6e0..71e5ac0892 100644
--- a/web/index.html
+++ b/web/index.html
@@ -17,295 +17,70 @@
%WEB_TITLE%
+
+
+
+
+
+
+
+
+
+
+
@@ -362,7 +137,7 @@
-
+
diff --git a/yarn.lock b/yarn.lock
index 98479ba44c..17fe862372 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.12.1", "@babel/plugin-transform-runtime@^7.16.4":
+"@babel/plugin-transform-runtime@^7.0.0", "@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,11 +2983,6 @@
"@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"
@@ -8262,13 +8257,6 @@
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"
@@ -9547,7 +9535,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==
@@ -9572,6 +9560,15 @@ 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"
@@ -10702,6 +10699,22 @@ 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"
@@ -10778,6 +10791,13 @@ 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"
@@ -11351,6 +11371,11 @@ 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"
@@ -13893,6 +13918,19 @@ 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"
@@ -13987,6 +14025,11 @@ 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"
@@ -16229,6 +16272,16 @@ 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"
@@ -16695,21 +16748,6 @@ 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"
@@ -18885,15 +18923,6 @@ 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"
@@ -18953,6 +18982,11 @@ 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"
@@ -18999,14 +19033,6 @@ 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"
@@ -20618,7 +20644,16 @@ 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", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"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:
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==
@@ -20727,7 +20762,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@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm: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==
@@ -20741,6 +20776,13 @@ 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"
@@ -20935,6 +20977,19 @@ 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"
@@ -21216,6 +21271,18 @@ 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"
@@ -21750,11 +21817,6 @@ 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"
@@ -22456,7 +22518,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@^7.0.0:
+"wrap-ansi-cjs@npm: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==
@@ -22474,6 +22536,15 @@ 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"