Merge remote-tracking branch 'origin/main' into by-you
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
[*.{kt,kts}]
|
||||||
|
indent_size=2
|
||||||
@@ -10,7 +10,7 @@ Get the app itself:
|
|||||||
|
|
||||||
## Development Resources
|
## Development Resources
|
||||||
|
|
||||||
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also on open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
|
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
|
||||||
|
|
||||||
There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application.
|
There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application.
|
||||||
|
|
||||||
@@ -42,10 +42,10 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th
|
|||||||
- Open an issue and give some time for discussion before submitting a PR.
|
- Open an issue and give some time for discussion before submitting a PR.
|
||||||
- Stay away from PRs like...
|
- Stay away from PRs like...
|
||||||
- Changing "Post" to "Skeet."
|
- Changing "Post" to "Skeet."
|
||||||
- Refactoring the codebase, eg to replace mobx with redux or something.
|
- Refactoring the codebase, e.g., to replace MobX with Redux or something.
|
||||||
- Adding entirely new features without prior discussion.
|
- Adding entirely new features without prior discussion.
|
||||||
|
|
||||||
Remember, we serve a wide community of users. Our day to day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for!
|
Remember, we serve a wide community of users. Our day-to-day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for!
|
||||||
|
|
||||||
## Forking guidelines
|
## Forking guidelines
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ If you discover any security issues, please send an email to security@bsky.app.
|
|||||||
|
|
||||||
## Are you a developer interested in building on atproto?
|
## Are you a developer interested in building on atproto?
|
||||||
|
|
||||||
Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party can be as seamless as first-party through custom feeds, federated services, clients, and more.
|
Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party integration can be as seamless as first-party through custom feeds, federated services, clients, and more.
|
||||||
|
|
||||||
## License (MIT)
|
## License (MIT)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
appId: xyz.blueskyweb.app
|
||||||
|
---
|
||||||
|
- runScript:
|
||||||
|
file: ../setupServer.js
|
||||||
|
env:
|
||||||
|
SERVER_PATH: "?users&posts&feeds"
|
||||||
|
- runFlow:
|
||||||
|
file: ../setupApp.yml
|
||||||
|
- tapOn:
|
||||||
|
id: "e2eSignInAlice"
|
||||||
|
- tapOn: "/sys/debug"
|
||||||
|
- tapOn:
|
||||||
|
id: "sharedPrefsTestOpenBtn"
|
||||||
|
- tapOn:
|
||||||
|
id: "setStringBtn"
|
||||||
|
- assertVisible: "Hello"
|
||||||
|
- tapOn:
|
||||||
|
id: "removeStringBtn"
|
||||||
|
- assertVisible: "null"
|
||||||
|
- tapOn:
|
||||||
|
id: "setBoolBtn"
|
||||||
|
- assertVisible: "true"
|
||||||
|
- tapOn:
|
||||||
|
id: "setNumberBtn"
|
||||||
|
- assertVisible: "123"
|
||||||
|
- tapOn:
|
||||||
|
id: "addToSetBtn"
|
||||||
|
- assertVisible: "true"
|
||||||
|
- tapOn:
|
||||||
|
id: "removeFromSetBtn"
|
||||||
|
- assertVisible: "false"
|
||||||
@@ -211,6 +211,8 @@ module.exports = function (config) {
|
|||||||
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'expo-video',
|
||||||
|
'react-native-compressor',
|
||||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||||
'./plugins/withAndroidManifestPlugin.js',
|
'./plugins/withAndroidManifestPlugin.js',
|
||||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||||
@@ -219,6 +221,7 @@ module.exports = function (config) {
|
|||||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||||
'./plugins/shareExtension/withShareExtensions.js',
|
'./plugins/shareExtension/withShareExtensions.js',
|
||||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||||
|
'./plugins/withAppDelegateReferrer.js',
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
extra: {
|
extra: {
|
||||||
eas: {
|
eas: {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2.444h2V13Zm0 4.444h-2V19h2v-1.556ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 370 B |
@@ -30,12 +30,11 @@ class ShareViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleText(item: NSItemProvider) async -> Void {
|
private func handleText(item: NSItemProvider) async {
|
||||||
do {
|
do {
|
||||||
if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String {
|
if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String {
|
||||||
if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
||||||
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
|
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
|
||||||
{
|
|
||||||
_ = self.openURL(url)
|
_ = self.openURL(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,12 +44,11 @@ class ShareViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleUrl(item: NSItemProvider) async -> Void {
|
private func handleUrl(item: NSItemProvider) async {
|
||||||
do {
|
do {
|
||||||
if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL {
|
if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL {
|
||||||
if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
||||||
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
|
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
|
||||||
{
|
|
||||||
_ = self.openURL(url)
|
_ = self.openURL(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +58,7 @@ class ShareViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleImages(items: [NSItemProvider]) async -> Void {
|
private func handleImages(items: [NSItemProvider]) async {
|
||||||
let firstFourItems: [NSItemProvider]
|
let firstFourItems: [NSItemProvider]
|
||||||
if items.count < 4 {
|
if items.count < 4 {
|
||||||
firstFourItems = items
|
firstFourItems = items
|
||||||
@@ -72,7 +70,7 @@ class ShareViewController: UIViewController {
|
|||||||
var imageUris = ""
|
var imageUris = ""
|
||||||
|
|
||||||
for (index, item) in firstFourItems.enumerated() {
|
for (index, item) in firstFourItems.enumerated() {
|
||||||
var imageUriInfo: String? = nil
|
var imageUriInfo: String?
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL {
|
if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL {
|
||||||
@@ -100,8 +98,7 @@ class ShareViewController: UIViewController {
|
|||||||
|
|
||||||
if valid,
|
if valid,
|
||||||
let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
||||||
let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)")
|
let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)") {
|
||||||
{
|
|
||||||
_ = self.openURL(url)
|
_ = self.openURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,13 +116,11 @@ class ShareViewController: UIViewController {
|
|||||||
// extension does.
|
// extension does.
|
||||||
if let dir = FileManager()
|
if let dir = FileManager()
|
||||||
.containerURL(
|
.containerURL(
|
||||||
forSecurityApplicationGroupIdentifier: "group.app.bsky")
|
forSecurityApplicationGroupIdentifier: "group.app.bsky") {
|
||||||
{
|
|
||||||
let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"
|
let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"
|
||||||
|
|
||||||
if let newUri = URL(string: filePath),
|
if let newUri = URL(string: filePath),
|
||||||
let jpegData = image.jpegData(compressionQuality: 1)
|
let jpegData = image.jpegData(compressionQuality: 1) {
|
||||||
{
|
|
||||||
try jpegData.write(to: newUri)
|
try jpegData.write(to: newUri)
|
||||||
return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)"
|
return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)"
|
||||||
}
|
}
|
||||||
@@ -136,7 +131,7 @@ class ShareViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func completeRequest() -> Void {
|
private func completeRequest() {
|
||||||
self.extensionContext?.completeRequest(returningItems: nil)
|
self.extensionContext?.completeRequest(returningItems: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import com.google.firebase.messaging.RemoteMessage
|
|||||||
|
|
||||||
class BackgroundNotificationHandler(
|
class BackgroundNotificationHandler(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val notifInterface: BackgroundNotificationHandlerInterface
|
private val notifInterface: BackgroundNotificationHandlerInterface,
|
||||||
) {
|
) {
|
||||||
fun handleMessage(remoteMessage: RemoteMessage) {
|
fun handleMessage(remoteMessage: RemoteMessage) {
|
||||||
if (ExpoBackgroundNotificationHandlerModule.isForegrounded) {
|
if (ExpoBackgroundNotificationHandlerModule.isForegrounded) {
|
||||||
|
|||||||
+48
-47
@@ -8,67 +8,68 @@ class ExpoBackgroundNotificationHandlerModule : Module() {
|
|||||||
var isForegrounded = false
|
var isForegrounded = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun definition() = ModuleDefinition {
|
override fun definition() =
|
||||||
Name("ExpoBackgroundNotificationHandler")
|
ModuleDefinition {
|
||||||
|
Name("ExpoBackgroundNotificationHandler")
|
||||||
|
|
||||||
OnCreate {
|
OnCreate {
|
||||||
NotificationPrefs(appContext.reactContext).initialize()
|
NotificationPrefs(appContext.reactContext).initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
OnActivityEntersForeground {
|
OnActivityEntersForeground {
|
||||||
isForegrounded = true
|
isForegrounded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
OnActivityEntersBackground {
|
OnActivityEntersBackground {
|
||||||
isForegrounded = false
|
isForegrounded = false
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("getAllPrefsAsync") {
|
AsyncFunction("getAllPrefsAsync") {
|
||||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs()
|
return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs()
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("getBoolAsync") { forKey: String ->
|
AsyncFunction("getBoolAsync") { forKey: String ->
|
||||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey)
|
return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("getStringAsync") { forKey: String ->
|
AsyncFunction("getStringAsync") { forKey: String ->
|
||||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey)
|
return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("getStringArrayAsync") { forKey: String ->
|
AsyncFunction("getStringArrayAsync") { forKey: String ->
|
||||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey)
|
return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("setBoolAsync") { forKey: String, value: Boolean ->
|
AsyncFunction("setBoolAsync") { forKey: String, value: Boolean ->
|
||||||
NotificationPrefs(appContext.reactContext).setBoolean(forKey, value)
|
NotificationPrefs(appContext.reactContext).setBoolean(forKey, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("setStringAsync") { forKey: String, value: String ->
|
AsyncFunction("setStringAsync") { forKey: String, value: String ->
|
||||||
NotificationPrefs(appContext.reactContext).setString(forKey, value)
|
NotificationPrefs(appContext.reactContext).setString(forKey, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> ->
|
AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> ->
|
||||||
NotificationPrefs(appContext.reactContext).setStringArray(forKey, value)
|
NotificationPrefs(appContext.reactContext).setStringArray(forKey, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("addToStringArrayAsync") { forKey: String, string: String ->
|
AsyncFunction("addToStringArrayAsync") { forKey: String, string: String ->
|
||||||
NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string)
|
NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String ->
|
AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String ->
|
||||||
NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string)
|
NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> ->
|
AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> ->
|
||||||
NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
|
NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
|
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
|
||||||
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
|
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("setBadgeCountAsync") { _: Int ->
|
AsyncFunction("setBadgeCountAsync") { _: Int ->
|
||||||
// This does nothing on Android
|
// This does nothing on Android
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-48
@@ -2,20 +2,24 @@ package expo.modules.backgroundnotificationhandler
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
||||||
val DEFAULTS = mapOf<String, Any>(
|
val DEFAULTS =
|
||||||
"playSoundChat" to true,
|
mapOf<String, Any>(
|
||||||
"playSoundFollow" to false,
|
"playSoundChat" to true,
|
||||||
"playSoundLike" to false,
|
"playSoundFollow" to false,
|
||||||
"playSoundMention" to false,
|
"playSoundLike" to false,
|
||||||
"playSoundQuote" to false,
|
"playSoundMention" to false,
|
||||||
"playSoundReply" to false,
|
"playSoundQuote" to false,
|
||||||
"playSoundRepost" to false,
|
"playSoundReply" to false,
|
||||||
"mutedThreads" to mapOf<String, List<String>>()
|
"playSoundRepost" to false,
|
||||||
)
|
"mutedThreads" to mapOf<String, List<String>>(),
|
||||||
|
)
|
||||||
|
|
||||||
class NotificationPrefs (private val context: Context?) {
|
class NotificationPrefs(
|
||||||
private val prefs = context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
|
private val context: Context?,
|
||||||
?: throw Error("Context is null")
|
) {
|
||||||
|
private val prefs =
|
||||||
|
context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
|
||||||
|
?: throw Error("Context is null")
|
||||||
|
|
||||||
fun initialize() {
|
fun initialize() {
|
||||||
prefs
|
prefs
|
||||||
@@ -41,94 +45,99 @@ class NotificationPrefs (private val context: Context?) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.apply()
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllPrefs(): MutableMap<String, *> {
|
fun getAllPrefs(): MutableMap<String, *> = prefs.all
|
||||||
return prefs.all
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBoolean(key: String): Boolean {
|
fun getBoolean(key: String): Boolean = prefs.getBoolean(key, false)
|
||||||
return prefs.getBoolean(key, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getString(key: String): String? {
|
fun getString(key: String): String? = prefs.getString(key, null)
|
||||||
return prefs.getString(key, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getStringArray(key: String): Array<String>? {
|
fun getStringArray(key: String): Array<String>? = prefs.getStringSet(key, null)?.toTypedArray()
|
||||||
return prefs.getStringSet(key, null)?.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setBoolean(key: String, value: Boolean) {
|
fun setBoolean(
|
||||||
|
key: String,
|
||||||
|
value: Boolean,
|
||||||
|
) {
|
||||||
prefs
|
prefs
|
||||||
.edit()
|
.edit()
|
||||||
.apply {
|
.apply {
|
||||||
putBoolean(key, value)
|
putBoolean(key, value)
|
||||||
}
|
}.apply()
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setString(key: String, value: String) {
|
fun setString(
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
prefs
|
prefs
|
||||||
.edit()
|
.edit()
|
||||||
.apply {
|
.apply {
|
||||||
putString(key, value)
|
putString(key, value)
|
||||||
}
|
}.apply()
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setStringArray(key: String, value: Array<String>) {
|
fun setStringArray(
|
||||||
|
key: String,
|
||||||
|
value: Array<String>,
|
||||||
|
) {
|
||||||
prefs
|
prefs
|
||||||
.edit()
|
.edit()
|
||||||
.apply {
|
.apply {
|
||||||
putStringSet(key, value.toSet())
|
putStringSet(key, value.toSet())
|
||||||
}
|
}.apply()
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addToStringArray(key: String, string: String) {
|
fun addToStringArray(
|
||||||
|
key: String,
|
||||||
|
string: String,
|
||||||
|
) {
|
||||||
prefs
|
prefs
|
||||||
.edit()
|
.edit()
|
||||||
.apply {
|
.apply {
|
||||||
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
||||||
set.add(string)
|
set.add(string)
|
||||||
putStringSet(key, set)
|
putStringSet(key, set)
|
||||||
}
|
}.apply()
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFromStringArray(key: String, string: String) {
|
fun removeFromStringArray(
|
||||||
|
key: String,
|
||||||
|
string: String,
|
||||||
|
) {
|
||||||
prefs
|
prefs
|
||||||
.edit()
|
.edit()
|
||||||
.apply {
|
.apply {
|
||||||
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
||||||
set.remove(string)
|
set.remove(string)
|
||||||
putStringSet(key, set)
|
putStringSet(key, set)
|
||||||
}
|
}.apply()
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addManyToStringArray(key: String, strings: Array<String>) {
|
fun addManyToStringArray(
|
||||||
|
key: String,
|
||||||
|
strings: Array<String>,
|
||||||
|
) {
|
||||||
prefs
|
prefs
|
||||||
.edit()
|
.edit()
|
||||||
.apply {
|
.apply {
|
||||||
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
||||||
set.addAll(strings.toSet())
|
set.addAll(strings.toSet())
|
||||||
putStringSet(key, set)
|
putStringSet(key, set)
|
||||||
}
|
}.apply()
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeManyFromStringArray(key: String, strings: Array<String>) {
|
fun removeManyFromStringArray(
|
||||||
|
key: String,
|
||||||
|
strings: Array<String>,
|
||||||
|
) {
|
||||||
prefs
|
prefs
|
||||||
.edit()
|
.edit()
|
||||||
.apply {
|
.apply {
|
||||||
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
||||||
set.removeAll(strings.toSet())
|
set.removeAll(strings.toSet())
|
||||||
putStringSet(key, set)
|
putStringSet(key, set)
|
||||||
}
|
}.apply()
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+9
-10
@@ -2,16 +2,16 @@ import ExpoModulesCore
|
|||||||
|
|
||||||
let APP_GROUP = "group.app.bsky"
|
let APP_GROUP = "group.app.bsky"
|
||||||
|
|
||||||
let DEFAULTS: [String:Any] = [
|
let DEFAULTS: [String: Any] = [
|
||||||
"playSoundChat" : true,
|
"playSoundChat": true,
|
||||||
"playSoundFollow": false,
|
"playSoundFollow": false,
|
||||||
"playSoundLike": false,
|
"playSoundLike": false,
|
||||||
"playSoundMention": false,
|
"playSoundMention": false,
|
||||||
"playSoundQuote": false,
|
"playSoundQuote": false,
|
||||||
"playSoundReply": false,
|
"playSoundReply": false,
|
||||||
"playSoundRepost": false,
|
"playSoundRepost": false,
|
||||||
"mutedThreads": [:] as! [String:[String]],
|
"mutedThreads": [:] as! [String: [String]],
|
||||||
"badgeCount": 0,
|
"badgeCount": 0
|
||||||
]
|
]
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -35,7 +35,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("getAllPrefsAsync") { () -> [String:Any]? in
|
AsyncFunction("getAllPrefsAsync") { () -> [String: Any]? in
|
||||||
var keys: [String] = []
|
var keys: [String] = []
|
||||||
DEFAULTS.forEach { p in
|
DEFAULTS.forEach { p in
|
||||||
keys.append(p.key)
|
keys.append(p.key)
|
||||||
@@ -64,22 +64,21 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) -> Void in
|
AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) in
|
||||||
userDefaults?.setValue(value, forKey: forKey)
|
userDefaults?.setValue(value, forKey: forKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("setStringAsync") { (forKey: String, value: String) -> Void in
|
AsyncFunction("setStringAsync") { (forKey: String, value: String) in
|
||||||
userDefaults?.setValue(value, forKey: forKey)
|
userDefaults?.setValue(value, forKey: forKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) -> Void in
|
AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) in
|
||||||
userDefaults?.setValue(value, forKey: forKey)
|
userDefaults?.setValue(value, forKey: forKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in
|
AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in
|
||||||
if var curr = userDefaults?.stringArray(forKey: forKey),
|
if var curr = userDefaults?.stringArray(forKey: forKey),
|
||||||
!curr.contains(string)
|
!curr.contains(string) {
|
||||||
{
|
|
||||||
curr.append(string)
|
curr.append(string)
|
||||||
userDefaults?.setValue(curr, forKey: forKey)
|
userDefaults?.setValue(curr, forKey: forKey)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -5,7 +5,10 @@ import android.graphics.Canvas
|
|||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
|
||||||
class AppCompatImageViewExtended(context: Context, private val parent: GifView): AppCompatImageView(context) {
|
class AppCompatImageViewExtended(
|
||||||
|
context: Context,
|
||||||
|
private val parent: GifView,
|
||||||
|
) : AppCompatImageView(context) {
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
super.onDraw(canvas)
|
super.onDraw(canvas)
|
||||||
|
|
||||||
|
|||||||
+42
-41
@@ -6,49 +6,50 @@ import expo.modules.kotlin.modules.Module
|
|||||||
import expo.modules.kotlin.modules.ModuleDefinition
|
import expo.modules.kotlin.modules.ModuleDefinition
|
||||||
|
|
||||||
class ExpoBlueskyGifViewModule : Module() {
|
class ExpoBlueskyGifViewModule : Module() {
|
||||||
override fun definition() = ModuleDefinition {
|
override fun definition() =
|
||||||
Name("ExpoBlueskyGifView")
|
ModuleDefinition {
|
||||||
|
Name("ExpoBlueskyGifView")
|
||||||
|
|
||||||
AsyncFunction("prefetchAsync") { sources: List<String> ->
|
AsyncFunction("prefetchAsync") { sources: List<String> ->
|
||||||
val activity = appContext.currentActivity ?: return@AsyncFunction
|
val activity = appContext.currentActivity ?: return@AsyncFunction
|
||||||
val glide = Glide.with(activity)
|
val glide = Glide.with(activity)
|
||||||
|
|
||||||
sources.forEach { source ->
|
sources.forEach { source ->
|
||||||
glide
|
glide
|
||||||
.download(source)
|
.download(source)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||||
.submit()
|
.submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
View(GifView::class) {
|
||||||
|
Events(
|
||||||
|
"onPlayerStateChange",
|
||||||
|
)
|
||||||
|
|
||||||
|
Prop("source") { view: GifView, source: String ->
|
||||||
|
view.source = source
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("placeholderSource") { view: GifView, source: String ->
|
||||||
|
view.placeholderSource = source
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("autoplay") { view: GifView, autoplay: Boolean ->
|
||||||
|
view.autoplay = autoplay
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("playAsync") { view: GifView ->
|
||||||
|
view.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("pauseAsync") { view: GifView ->
|
||||||
|
view.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("toggleAsync") { view: GifView ->
|
||||||
|
view.toggle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
View(GifView::class) {
|
|
||||||
Events(
|
|
||||||
"onPlayerStateChange"
|
|
||||||
)
|
|
||||||
|
|
||||||
Prop("source") { view: GifView, source: String ->
|
|
||||||
view.source = source
|
|
||||||
}
|
|
||||||
|
|
||||||
Prop("placeholderSource") { view: GifView, source: String ->
|
|
||||||
view.placeholderSource = source
|
|
||||||
}
|
|
||||||
|
|
||||||
Prop("autoplay") { view: GifView, autoplay: Boolean ->
|
|
||||||
view.autoplay = autoplay
|
|
||||||
}
|
|
||||||
|
|
||||||
AsyncFunction("playAsync") { view: GifView ->
|
|
||||||
view.play()
|
|
||||||
}
|
|
||||||
|
|
||||||
AsyncFunction("pauseAsync") { view: GifView ->
|
|
||||||
view.pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
AsyncFunction("toggleAsync") { view: GifView ->
|
|
||||||
view.toggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-70
@@ -1,6 +1,5 @@
|
|||||||
package expo.modules.blueskygifview
|
package expo.modules.blueskygifview
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
@@ -15,7 +14,10 @@ import expo.modules.kotlin.exception.Exceptions
|
|||||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||||
import expo.modules.kotlin.views.ExpoView
|
import expo.modules.kotlin.views.ExpoView
|
||||||
|
|
||||||
class GifView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
class GifView(
|
||||||
|
context: Context,
|
||||||
|
appContext: AppContext,
|
||||||
|
) : ExpoView(context, appContext) {
|
||||||
// Events
|
// Events
|
||||||
private val onPlayerStateChange by EventDispatcher()
|
private val onPlayerStateChange by EventDispatcher()
|
||||||
|
|
||||||
@@ -44,8 +46,7 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// <editor-fold desc="Lifecycle">
|
||||||
//<editor-fold desc="Lifecycle">
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
this.setBackgroundColor(Color.TRANSPARENT)
|
this.setBackgroundColor(Color.TRANSPARENT)
|
||||||
@@ -70,80 +71,82 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
|
|||||||
super.onDetachedFromWindow()
|
super.onDetachedFromWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
//</editor-fold>
|
// </editor-fold>
|
||||||
|
|
||||||
//<editor-fold desc="Loading">
|
// <editor-fold desc="Loading">
|
||||||
|
|
||||||
private fun load() {
|
private fun load() {
|
||||||
if (placeholderSource == null || source == null) {
|
if (placeholderSource == null || source == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.webpRequest = glide.load(source)
|
this.webpRequest =
|
||||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
glide
|
||||||
.skipMemoryCache(false)
|
.load(source)
|
||||||
.listener(object: RequestListener<Drawable> {
|
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||||
override fun onResourceReady(
|
.skipMemoryCache(false)
|
||||||
resource: Drawable?,
|
.listener(
|
||||||
model: Any?,
|
object : RequestListener<Drawable> {
|
||||||
target: Target<Drawable>?,
|
override fun onResourceReady(
|
||||||
dataSource: com.bumptech.glide.load.DataSource?,
|
resource: Drawable?,
|
||||||
isFirstResource: Boolean
|
model: Any?,
|
||||||
): Boolean {
|
target: Target<Drawable>?,
|
||||||
if (placeholderRequest != null) {
|
dataSource: com.bumptech.glide.load.DataSource?,
|
||||||
glide.clear(placeholderRequest)
|
isFirstResource: Boolean,
|
||||||
}
|
): Boolean {
|
||||||
return false
|
if (placeholderRequest != null) {
|
||||||
}
|
glide.clear(placeholderRequest)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLoadFailed(
|
override fun onLoadFailed(
|
||||||
e: GlideException?,
|
e: GlideException?,
|
||||||
model: Any?,
|
model: Any?,
|
||||||
target: Target<Drawable>?,
|
target: Target<Drawable>?,
|
||||||
isFirstResource: Boolean
|
isFirstResource: Boolean,
|
||||||
): Boolean {
|
): Boolean = true
|
||||||
return true
|
},
|
||||||
}
|
).into(this.imageView)
|
||||||
})
|
|
||||||
.into(this.imageView)
|
|
||||||
|
|
||||||
if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
|
if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
|
||||||
this.placeholderRequest = glide.load(placeholderSource)
|
this.placeholderRequest =
|
||||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
glide
|
||||||
// Let's not bloat the memory cache with placeholders
|
.load(placeholderSource)
|
||||||
.skipMemoryCache(true)
|
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||||
.listener(object: RequestListener<Drawable> {
|
// Let's not bloat the memory cache with placeholders
|
||||||
override fun onResourceReady(
|
.skipMemoryCache(true)
|
||||||
resource: Drawable?,
|
.listener(
|
||||||
model: Any?,
|
object : RequestListener<Drawable> {
|
||||||
target: Target<Drawable>?,
|
override fun onResourceReady(
|
||||||
dataSource: com.bumptech.glide.load.DataSource?,
|
resource: Drawable?,
|
||||||
isFirstResource: Boolean
|
model: Any?,
|
||||||
): Boolean {
|
target: Target<Drawable>?,
|
||||||
// Incase this request finishes after the webp, let's just not set
|
dataSource: com.bumptech.glide.load.DataSource?,
|
||||||
// the drawable. This shouldn't happen because the request should get cancelled
|
isFirstResource: Boolean,
|
||||||
if (imageView.drawable == null) {
|
): Boolean {
|
||||||
imageView.setImageDrawable(resource)
|
// Incase this request finishes after the webp, let's just not set
|
||||||
}
|
// the drawable. This shouldn't happen because the request should get cancelled
|
||||||
return true
|
if (imageView.drawable == null) {
|
||||||
}
|
imageView.setImageDrawable(resource)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLoadFailed(
|
override fun onLoadFailed(
|
||||||
e: GlideException?,
|
e: GlideException?,
|
||||||
model: Any?,
|
model: Any?,
|
||||||
target: Target<Drawable>?,
|
target: Target<Drawable>?,
|
||||||
isFirstResource: Boolean
|
isFirstResource: Boolean,
|
||||||
): Boolean {
|
): Boolean = true
|
||||||
return true
|
},
|
||||||
}
|
).submit()
|
||||||
})
|
|
||||||
.submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//</editor-fold>
|
// </editor-fold>
|
||||||
|
|
||||||
//<editor-fold desc="Controls">
|
// <editor-fold desc="Controls">
|
||||||
|
|
||||||
fun play() {
|
fun play() {
|
||||||
this.imageView.play()
|
this.imageView.play()
|
||||||
@@ -165,16 +168,18 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//</editor-fold>
|
// </editor-fold>
|
||||||
|
|
||||||
//<editor-fold desc="Util">
|
// <editor-fold desc="Util">
|
||||||
|
|
||||||
fun firePlayerStateChange() {
|
fun firePlayerStateChange() {
|
||||||
onPlayerStateChange(mapOf(
|
onPlayerStateChange(
|
||||||
"isPlaying" to this.isPlaying,
|
mapOf(
|
||||||
"isLoaded" to this.isLoaded,
|
"isPlaying" to this.isPlaying,
|
||||||
))
|
"isLoaded" to this.isLoaded,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
//</editor-fold>
|
// </editor-fold>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
|
|||||||
private var placeholderOperation: SDWebImageCombinedOperation?
|
private var placeholderOperation: SDWebImageCombinedOperation?
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
var source: String? = nil
|
var source: String?
|
||||||
var placeholderSource: String? = nil
|
var placeholderSource: String?
|
||||||
var autoplay = true {
|
var autoplay = true {
|
||||||
didSet {
|
didSet {
|
||||||
if !autoplay {
|
if !autoplay {
|
||||||
@@ -78,8 +78,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
|
|||||||
// See:
|
// See:
|
||||||
// https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently
|
// https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently
|
||||||
if !SDImageCache.shared.diskImageDataExists(withKey: source),
|
if !SDImageCache.shared.diskImageDataExists(withKey: source),
|
||||||
let url = URL(string: placeholderSource)
|
let url = URL(string: placeholderSource) {
|
||||||
{
|
|
||||||
self.placeholderOperation = imageManager.loadImage(
|
self.placeholderOperation = imageManager.loadImage(
|
||||||
with: url,
|
with: url,
|
||||||
options: [.retryFailed],
|
options: [.retryFailed],
|
||||||
@@ -132,8 +131,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
|
|||||||
if let placeholderSource = self.placeholderSource,
|
if let placeholderSource = self.placeholderSource,
|
||||||
imageUrl?.absoluteString == placeholderSource,
|
imageUrl?.absoluteString == placeholderSource,
|
||||||
self.imageView.image == nil,
|
self.imageView.image == nil,
|
||||||
let image = image
|
let image = image {
|
||||||
{
|
|
||||||
self.setImage(image)
|
self.setImage(image)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -142,8 +140,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
|
|||||||
imageUrl?.absoluteString == source,
|
imageUrl?.absoluteString == source,
|
||||||
// UIImage perf suckssss if the image is animated
|
// UIImage perf suckssss if the image is animated
|
||||||
let data = data,
|
let data = data,
|
||||||
let animatedImage = SDAnimatedImage(data: data)
|
let animatedImage = SDAnimatedImage(data: data) {
|
||||||
{
|
|
||||||
self.placeholderOperation?.cancel()
|
self.placeholderOperation?.cancel()
|
||||||
self.isPlaying = self.autoplay
|
self.isPlaying = self.autoplay
|
||||||
self.isLoaded = true
|
self.isLoaded = true
|
||||||
|
|||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
package expo.modules.blueskyswissarmy.deviceprefs
|
|
||||||
|
|
||||||
import expo.modules.kotlin.modules.Module
|
|
||||||
import expo.modules.kotlin.modules.ModuleDefinition
|
|
||||||
|
|
||||||
class ExpoBlueskyDevicePrefsModule : Module() {
|
|
||||||
override fun definition() = ModuleDefinition {
|
|
||||||
Name("ExpoBlueskyDevicePrefs")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+84
-35
@@ -1,54 +1,103 @@
|
|||||||
package expo.modules.blueskyswissarmy.referrer
|
package expo.modules.blueskyswissarmy.referrer
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.android.installreferrer.api.InstallReferrerClient
|
import com.android.installreferrer.api.InstallReferrerClient
|
||||||
import com.android.installreferrer.api.InstallReferrerStateListener
|
import com.android.installreferrer.api.InstallReferrerStateListener
|
||||||
|
import expo.modules.kotlin.Promise
|
||||||
import expo.modules.kotlin.modules.Module
|
import expo.modules.kotlin.modules.Module
|
||||||
import expo.modules.kotlin.modules.ModuleDefinition
|
import expo.modules.kotlin.modules.ModuleDefinition
|
||||||
import expo.modules.kotlin.Promise
|
|
||||||
|
|
||||||
class ExpoBlueskyReferrerModule : Module() {
|
class ExpoBlueskyReferrerModule : Module() {
|
||||||
override fun definition() = ModuleDefinition {
|
private var intent: Intent? = null
|
||||||
Name("ExpoBlueskyReferrer")
|
private var activityReferrer: Uri? = null
|
||||||
|
|
||||||
AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise ->
|
override fun definition() =
|
||||||
val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build()
|
ModuleDefinition {
|
||||||
referrerClient.startConnection(object : InstallReferrerStateListener {
|
Name("ExpoBlueskyReferrer")
|
||||||
override fun onInstallReferrerSetupFinished(responseCode: Int) {
|
|
||||||
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
|
|
||||||
Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.")
|
|
||||||
|
|
||||||
val response = referrerClient.installReferrer
|
OnNewIntent {
|
||||||
Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}")
|
intent = it
|
||||||
|
activityReferrer = appContext.currentActivity?.referrer
|
||||||
|
}
|
||||||
|
|
||||||
promise.resolve(
|
AsyncFunction("getReferrerInfoAsync") {
|
||||||
mapOf(
|
val intentReferrer =
|
||||||
"installReferrer" to response.installReferrer,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
"clickTimestamp" to response.referrerClickTimestampSeconds,
|
intent?.getParcelableExtra(Intent.EXTRA_REFERRER, Uri::class.java)
|
||||||
"installTimestamp" to response.installBeginTimestampSeconds
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.")
|
intent?.getParcelableExtra(Intent.EXTRA_REFERRER)
|
||||||
promise.reject(
|
|
||||||
"ERR_GOOGLE_PLAY_REFERRER_UNKNOWN",
|
|
||||||
"Failed to get referrer info",
|
|
||||||
Exception("Failed to get referrer info")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
referrerClient.endConnection()
|
|
||||||
|
// Some apps explicitly set a referrer, like Chrome. In these cases, we prefer this since
|
||||||
|
// it's the actual website that the user came from rather than the app.
|
||||||
|
if (intentReferrer is Uri) {
|
||||||
|
val res =
|
||||||
|
mapOf(
|
||||||
|
"referrer" to intentReferrer.toString(),
|
||||||
|
"hostname" to intentReferrer.host,
|
||||||
|
)
|
||||||
|
intent = null
|
||||||
|
return@AsyncFunction res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInstallReferrerServiceDisconnected() {
|
// In all other cases, we'll just record the app that sent the intent.
|
||||||
Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.")
|
if (activityReferrer != null) {
|
||||||
referrerClient.endConnection()
|
// referrer could become null here. `.toString()` though can be called on null
|
||||||
promise.reject(
|
val res =
|
||||||
"ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED",
|
mapOf(
|
||||||
"Failed to get referrer info",
|
"referrer" to activityReferrer.toString(),
|
||||||
Exception("Failed to get referrer info")
|
"hostname" to (activityReferrer?.host ?: ""),
|
||||||
)
|
)
|
||||||
|
activityReferrer = null
|
||||||
|
return@AsyncFunction res
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
return@AsyncFunction null
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise ->
|
||||||
|
val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build()
|
||||||
|
referrerClient.startConnection(
|
||||||
|
object : InstallReferrerStateListener {
|
||||||
|
override fun onInstallReferrerSetupFinished(responseCode: Int) {
|
||||||
|
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
|
||||||
|
Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.")
|
||||||
|
|
||||||
|
val response = referrerClient.installReferrer
|
||||||
|
Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}")
|
||||||
|
|
||||||
|
promise.resolve(
|
||||||
|
mapOf(
|
||||||
|
"installReferrer" to response.installReferrer,
|
||||||
|
"clickTimestamp" to response.referrerClickTimestampSeconds,
|
||||||
|
"installTimestamp" to response.installBeginTimestampSeconds,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.")
|
||||||
|
promise.reject(
|
||||||
|
"ERR_GOOGLE_PLAY_REFERRER_UNKNOWN",
|
||||||
|
"Failed to get referrer info",
|
||||||
|
Exception("Failed to get referrer info"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
referrerClient.endConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInstallReferrerServiceDisconnected() {
|
||||||
|
Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.")
|
||||||
|
referrerClient.endConnection()
|
||||||
|
promise.reject(
|
||||||
|
"ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED",
|
||||||
|
"Failed to get referrer info",
|
||||||
|
Exception("Failed to get referrer info"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
package expo.modules.blueskyswissarmy.sharedprefs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import expo.modules.kotlin.jni.JavaScriptValue
|
||||||
|
import expo.modules.kotlin.modules.Module
|
||||||
|
import expo.modules.kotlin.modules.ModuleDefinition
|
||||||
|
|
||||||
|
class ExpoBlueskySharedPrefsModule : Module() {
|
||||||
|
private fun getContext(): Context {
|
||||||
|
val context = appContext.reactContext ?: throw Error("Context is null")
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun definition() =
|
||||||
|
ModuleDefinition {
|
||||||
|
Name("ExpoBlueskySharedPrefs")
|
||||||
|
|
||||||
|
Function("setString") { key: String, value: String ->
|
||||||
|
return@Function SharedPrefs(getContext()).setValue(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("setValue") { key: String, value: JavaScriptValue ->
|
||||||
|
val context = getContext()
|
||||||
|
Log.d("ExpoBlueskySharedPrefs", "Setting value for key: $key")
|
||||||
|
try {
|
||||||
|
if (value.isNumber()) {
|
||||||
|
SharedPrefs(context).setValue(key, value.getFloat())
|
||||||
|
} else if (value.isBool()) {
|
||||||
|
SharedPrefs(context).setValue(key, value.getBool())
|
||||||
|
} else if (value.isNull() || value.isUndefined()) {
|
||||||
|
SharedPrefs(context).removeValue(key)
|
||||||
|
} else {
|
||||||
|
Log.d(NAME, "Unsupported type: ${value.kind()}")
|
||||||
|
}
|
||||||
|
} catch (e: Error) {
|
||||||
|
Log.d(NAME, "Error setting value: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("removeValue") { key: String ->
|
||||||
|
return@Function SharedPrefs(getContext()).removeValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("getString") { key: String ->
|
||||||
|
return@Function SharedPrefs(getContext()).getString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("getNumber") { key: String ->
|
||||||
|
return@Function SharedPrefs(getContext()).getFloat(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("getBool") { key: String ->
|
||||||
|
return@Function SharedPrefs(getContext()).getBoolean(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("addToSet") { key: String, value: String ->
|
||||||
|
return@Function SharedPrefs(getContext()).addToSet(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("removeFromSet") { key: String, value: String ->
|
||||||
|
return@Function SharedPrefs(getContext()).removeFromSet(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("setContains") { key: String, value: String ->
|
||||||
|
return@Function SharedPrefs(getContext()).setContains(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+241
@@ -0,0 +1,241 @@
|
|||||||
|
package expo.modules.blueskyswissarmy.sharedprefs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
|
val DEFAULTS =
|
||||||
|
mapOf<String, Any>(
|
||||||
|
"playSoundChat" to true,
|
||||||
|
"playSoundFollow" to false,
|
||||||
|
"playSoundLike" to false,
|
||||||
|
"playSoundMention" to false,
|
||||||
|
"playSoundQuote" to false,
|
||||||
|
"playSoundReply" to false,
|
||||||
|
"playSoundRepost" to false,
|
||||||
|
"badgeCount" to 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const val NAME = "SharedPrefs"
|
||||||
|
|
||||||
|
class SharedPrefs(
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private var hasInitialized = false
|
||||||
|
|
||||||
|
private var instance: SharedPreferences? = null
|
||||||
|
|
||||||
|
fun getInstance(
|
||||||
|
context: Context,
|
||||||
|
info: String? = "(no info)",
|
||||||
|
): SharedPreferences {
|
||||||
|
if (instance == null) {
|
||||||
|
Log.d(NAME, "No preferences instance found, creating one.")
|
||||||
|
instance = context.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val safeInstance = instance ?: throw Error("Preferences is null: $info")
|
||||||
|
|
||||||
|
if (!hasInitialized) {
|
||||||
|
Log.d(NAME, "Preferences instance has not been initialized yet.")
|
||||||
|
initialize(safeInstance)
|
||||||
|
hasInitialized = true
|
||||||
|
Log.d(NAME, "Preferences instance has been initialized.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialize(instance: SharedPreferences) {
|
||||||
|
instance
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
DEFAULTS.forEach { (key, value) ->
|
||||||
|
if (instance.contains(key)) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
when (value) {
|
||||||
|
is Boolean -> {
|
||||||
|
putBoolean(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
is String -> {
|
||||||
|
putString(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Array<*> -> {
|
||||||
|
putStringSet(key, value.map { it.toString() }.toSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
is Map<*, *> -> {
|
||||||
|
putStringSet(key, value.map { it.toString() }.toSet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
safeInstance
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
putString(key, value)
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(
|
||||||
|
key: String,
|
||||||
|
value: Float,
|
||||||
|
) {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
safeInstance
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
putFloat(key, value)
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(
|
||||||
|
key: String,
|
||||||
|
value: Boolean,
|
||||||
|
) {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
safeInstance
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
putBoolean(key, value)
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setValue(
|
||||||
|
key: String,
|
||||||
|
value: Set<String>,
|
||||||
|
) {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
safeInstance
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
putStringSet(key, value)
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeValue(key: String) {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
safeInstance
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
remove(key)
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getString(key: String): String? {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
return safeInstance.getString(key, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFloat(key: String): Float? {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
if (!safeInstance.contains(key)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return safeInstance.getFloat(key, 0.0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ktlint:standard:function-naming")
|
||||||
|
fun _setAnyValue(
|
||||||
|
key: String,
|
||||||
|
value: Any,
|
||||||
|
) {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
safeInstance
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
when (value) {
|
||||||
|
is String -> putString(key, value)
|
||||||
|
is Float -> putFloat(key, value)
|
||||||
|
is Boolean -> putBoolean(key, value)
|
||||||
|
is Set<*> -> putStringSet(key, value.map { it.toString() }.toSet())
|
||||||
|
else -> throw Error("Unsupported type: ${value::class.java}")
|
||||||
|
}
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBoolean(key: String): Boolean? {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
if (!safeInstance.contains(key)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
Log.d(NAME, "Getting boolean for key: $key")
|
||||||
|
val res = safeInstance.getBoolean(key, false)
|
||||||
|
Log.d(NAME, "Got boolean for key: $key, value: $res")
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addToSet(
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
val set = safeInstance.getStringSet(key, setOf()) ?: setOf()
|
||||||
|
val newSet =
|
||||||
|
set.toMutableSet().apply {
|
||||||
|
add(value)
|
||||||
|
}
|
||||||
|
safeInstance
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
putStringSet(key, newSet)
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeFromSet(
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
val set = safeInstance.getStringSet(key, setOf()) ?: setOf()
|
||||||
|
val newSet =
|
||||||
|
set.toMutableSet().apply {
|
||||||
|
remove(value)
|
||||||
|
}
|
||||||
|
safeInstance
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
putStringSet(key, newSet)
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setContains(
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
): Boolean {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
val set = safeInstance.getStringSet(key, setOf()) ?: setOf()
|
||||||
|
return set.contains(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasValue(key: String): Boolean {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
return safeInstance.contains(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getValues(keys: Set<String>): Map<String, Any?> {
|
||||||
|
val safeInstance = getInstance(context)
|
||||||
|
return keys.associateWith { key ->
|
||||||
|
when (val value = safeInstance.all[key]) {
|
||||||
|
is String -> value
|
||||||
|
is Float -> value
|
||||||
|
is Boolean -> value
|
||||||
|
is Set<*> -> value
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"platforms": ["ios", "tvos", "android", "web"],
|
"platforms": ["ios", "tvos", "android", "web"],
|
||||||
"ios": {
|
"ios": {
|
||||||
"modules": ["ExpoBlueskyDevicePrefsModule", "ExpoBlueskyReferrerModule"]
|
"modules": ["ExpoBlueskySharedPrefsModule", "ExpoBlueskyReferrerModule"]
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"modules": [
|
"modules": [
|
||||||
"expo.modules.blueskyswissarmy.deviceprefs.ExpoBlueskyDevicePrefsModule",
|
"expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule",
|
||||||
"expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule"
|
"expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as DevicePrefs from './src/DevicePrefs'
|
|
||||||
import * as Referrer from './src/Referrer'
|
import * as Referrer from './src/Referrer'
|
||||||
|
import * as SharedPrefs from './src/SharedPrefs'
|
||||||
|
|
||||||
export {DevicePrefs, Referrer}
|
export {Referrer, SharedPrefs}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import ExpoModulesCore
|
|
||||||
|
|
||||||
public class ExpoBlueskyDevicePrefsModule: Module {
|
|
||||||
func getDefaults(_ useAppGroup: Bool) -> UserDefaults? {
|
|
||||||
if useAppGroup {
|
|
||||||
return UserDefaults(suiteName: "group.app.bsky")
|
|
||||||
} else {
|
|
||||||
return UserDefaults.standard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func definition() -> ModuleDefinition {
|
|
||||||
Name("ExpoBlueskyDevicePrefs")
|
|
||||||
|
|
||||||
AsyncFunction("getStringValueAsync") { (key: String, useAppGroup: Bool) in
|
|
||||||
return self.getDefaults(useAppGroup)?.string(forKey: key)
|
|
||||||
}
|
|
||||||
|
|
||||||
AsyncFunction("setStringValueAsync") { (key: String, value: String?, useAppGroup: Bool) in
|
|
||||||
self.getDefaults(useAppGroup)?.setValue(value, forKey: key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import Foundation
|
||||||
|
import ExpoModulesCore
|
||||||
|
|
||||||
|
public class ExpoBlueskySharedPrefsModule: Module {
|
||||||
|
let defaults = UserDefaults(suiteName: "group.app.bsky")
|
||||||
|
|
||||||
|
func getDefaults(_ info: String = "(no info)") -> UserDefaults? {
|
||||||
|
guard let defaults = self.defaults else {
|
||||||
|
NSLog("Failed to get defaults for app group: \(info)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
public func definition() -> ModuleDefinition {
|
||||||
|
Name("ExpoBlueskySharedPrefs")
|
||||||
|
|
||||||
|
// JavaScripValue causes a crash when trying to check `isString()`. Let's
|
||||||
|
// explicitly define setString instead.
|
||||||
|
Function("setString") { (key: String, value: String?) in
|
||||||
|
SharedPrefs.shared.setValue(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("setValue") { (key: String, value: JavaScriptValue) in
|
||||||
|
if value.isNumber() {
|
||||||
|
SharedPrefs.shared.setValue(key, value.getDouble())
|
||||||
|
} else if value.isBool() {
|
||||||
|
SharedPrefs.shared.setValue(key, value.getBool())
|
||||||
|
} else if value.isNull() || value.isUndefined() {
|
||||||
|
SharedPrefs.shared.removeValue(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("removeValue") { (key: String) in
|
||||||
|
SharedPrefs.shared.removeValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("getString") { (key: String) in
|
||||||
|
return SharedPrefs.shared.getString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("getBool") { (key: String) in
|
||||||
|
return SharedPrefs.shared.getBool(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("getNumber") { (key: String) in
|
||||||
|
return SharedPrefs.shared.getNumber(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("addToSet") { (key: String, value: String) in
|
||||||
|
SharedPrefs.shared.addToSet(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("removeFromSet") { (key: String, value: String) in
|
||||||
|
SharedPrefs.shared.removeFromSet(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
Function("setContains") { (key: String, value: String) in
|
||||||
|
return SharedPrefs.shared.setContains(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public class SharedPrefs {
|
||||||
|
public static let shared = SharedPrefs()
|
||||||
|
|
||||||
|
private let defaults = UserDefaults(suiteName: "group.app.bsky")
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if defaults == nil {
|
||||||
|
NSLog("Failed to get user defaults for app group.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getDefaults(_ info: String = "(no info)") -> UserDefaults? {
|
||||||
|
guard let defaults = self.defaults else {
|
||||||
|
NSLog("Failed to get defaults for app group: \(info)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setValue(_ key: String, _ value: String?) {
|
||||||
|
getDefaults(key)?.setValue(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setValue(_ key: String, _ value: Double?) {
|
||||||
|
getDefaults(key)?.setValue(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setValue(_ key: String, _ value: Bool?) {
|
||||||
|
getDefaults(key)?.setValue(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func _setAnyValue(_ key: String, _ value: Any?) {
|
||||||
|
getDefaults(key)?.setValue(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func removeValue(_ key: String) {
|
||||||
|
getDefaults(key)?.removeObject(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getString(_ key: String) -> String? {
|
||||||
|
return getDefaults(key)?.string(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getNumber(_ key: String) -> Double? {
|
||||||
|
return getDefaults(key)?.double(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getBool(_ key: String) -> Bool? {
|
||||||
|
return getDefaults(key)?.bool(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func addToSet(_ key: String, _ value: String) {
|
||||||
|
var dict: [String: Bool]?
|
||||||
|
if var currDict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] {
|
||||||
|
currDict[value] = true
|
||||||
|
dict = currDict
|
||||||
|
} else {
|
||||||
|
dict = [
|
||||||
|
value: true
|
||||||
|
]
|
||||||
|
}
|
||||||
|
getDefaults(key)?.setValue(dict, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func removeFromSet(_ key: String, _ value: String) {
|
||||||
|
guard var dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dict.removeValue(forKey: value)
|
||||||
|
getDefaults(key)?.setValue(dict, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setContains(_ key: String, _ value: String) -> Bool {
|
||||||
|
guard let dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return dict[value] == true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hasValue(_ key: String) -> Bool {
|
||||||
|
return getDefaults(key)?.value(forKey: key) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getValues(_ keys: [String]) -> [String: Any?]? {
|
||||||
|
return getDefaults("keys:\(keys)")?.dictionaryWithValues(forKeys: keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import {requireNativeModule} from 'expo-modules-core'
|
|
||||||
|
|
||||||
const NativeModule = requireNativeModule('ExpoBlueskyDevicePrefs')
|
|
||||||
|
|
||||||
export function getStringValueAsync(
|
|
||||||
key: string,
|
|
||||||
useAppGroup?: boolean,
|
|
||||||
): Promise<string | null> {
|
|
||||||
return NativeModule.getStringValueAsync(key, useAppGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setStringValueAsync(
|
|
||||||
key: string,
|
|
||||||
value: string | null,
|
|
||||||
useAppGroup?: boolean,
|
|
||||||
): Promise<void> {
|
|
||||||
return NativeModule.setStringValueAsync(key, value, useAppGroup)
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import {NotImplementedError} from '../NotImplemented'
|
|
||||||
|
|
||||||
export function getStringValueAsync(
|
|
||||||
key: string,
|
|
||||||
useAppGroup?: boolean,
|
|
||||||
): Promise<string | null> {
|
|
||||||
throw new NotImplementedError({key, useAppGroup})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setStringValueAsync(
|
|
||||||
key: string,
|
|
||||||
value: string | null,
|
|
||||||
useAppGroup?: boolean,
|
|
||||||
): Promise<string | null> {
|
|
||||||
throw new NotImplementedError({key, value, useAppGroup})
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import {requireNativeModule} from 'expo'
|
import {requireNativeModule} from 'expo'
|
||||||
|
|
||||||
import {GooglePlayReferrerInfo} from './types'
|
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||||
|
|
||||||
export const NativeModule = requireNativeModule('ExpoBlueskyReferrer')
|
export const NativeModule = requireNativeModule('ExpoBlueskyReferrer')
|
||||||
|
|
||||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo | null> {
|
||||||
return NativeModule.getGooglePlayReferrerInfoAsync()
|
return NativeModule.getGooglePlayReferrerInfoAsync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getReferrerInfoAsync(): Promise<ReferrerInfo | null> {
|
||||||
|
return NativeModule.getReferrerInfoAsync()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {SharedPrefs} from '../../index'
|
||||||
|
import {NotImplementedError} from '../NotImplemented'
|
||||||
|
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||||
|
|
||||||
|
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||||
|
throw new NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReferrerInfoAsync(): Promise<ReferrerInfo | null> {
|
||||||
|
const referrer = SharedPrefs.getString('referrer')
|
||||||
|
if (referrer) {
|
||||||
|
SharedPrefs.removeValue('referrer')
|
||||||
|
try {
|
||||||
|
const url = new URL(referrer)
|
||||||
|
return {
|
||||||
|
referrer,
|
||||||
|
hostname: url.hostname,
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
referrer,
|
||||||
|
hostname: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const referrerApp = SharedPrefs.getString('referrerApp')
|
||||||
|
if (referrerApp) {
|
||||||
|
SharedPrefs.removeValue('referrerApp')
|
||||||
|
return {
|
||||||
|
referrer: referrerApp,
|
||||||
|
hostname: referrerApp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import {NotImplementedError} from '../NotImplemented'
|
import {NotImplementedError} from '../NotImplemented'
|
||||||
import {GooglePlayReferrerInfo} from './types'
|
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||||
|
|
||||||
// @ts-ignore throws
|
|
||||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||||
throw new NotImplementedError()
|
throw new NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getReferrerInfoAsync(): Promise<ReferrerInfo | null> {
|
||||||
|
throw new NotImplementedError()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import {Platform} from 'react-native'
|
||||||
|
|
||||||
|
import {NotImplementedError} from '../NotImplemented'
|
||||||
|
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||||
|
|
||||||
|
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||||
|
throw new NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReferrerInfoAsync(): Promise<ReferrerInfo | null> {
|
||||||
|
if (
|
||||||
|
Platform.OS === 'web' &&
|
||||||
|
// for ssr
|
||||||
|
typeof document !== 'undefined' &&
|
||||||
|
document != null &&
|
||||||
|
document.referrer
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const url = new URL(document.referrer)
|
||||||
|
if (url.hostname !== 'bsky.app') {
|
||||||
|
return {
|
||||||
|
referrer: url.href,
|
||||||
|
hostname: url.hostname,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If something happens to the URL parsing, we don't want to actually cause any problems for the user. Just
|
||||||
|
// log the error so we might catch it
|
||||||
|
console.error('Failed to parse referrer URL')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
export type GooglePlayReferrerInfo =
|
export type GooglePlayReferrerInfo = {
|
||||||
| {
|
installReferrer?: string
|
||||||
installReferrer?: string
|
clickTimestamp?: number
|
||||||
clickTimestamp?: number
|
installTimestamp?: number
|
||||||
installTimestamp?: number
|
}
|
||||||
}
|
|
||||||
| undefined
|
export type ReferrerInfo = {
|
||||||
|
referrer: string
|
||||||
|
hostname: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {requireNativeModule} from 'expo-modules-core'
|
||||||
|
|
||||||
|
const NativeModule = requireNativeModule('ExpoBlueskySharedPrefs')
|
||||||
|
|
||||||
|
export function setValue(
|
||||||
|
key: string,
|
||||||
|
value: string | number | boolean | null | undefined,
|
||||||
|
): void {
|
||||||
|
// A bug on Android causes `JavaScripValue.isString()` to cause a crash on some occasions, seemingly because of a
|
||||||
|
// memory violation. Instead, we will use a specific function to set strings on this platform.
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return NativeModule.setString(key, value)
|
||||||
|
}
|
||||||
|
return NativeModule.setValue(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeValue(key: string): void {
|
||||||
|
return NativeModule.removeValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getString(key: string): string | undefined {
|
||||||
|
return nullToUndefined(NativeModule.getString(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNumber(key: string): number | undefined {
|
||||||
|
return nullToUndefined(NativeModule.getNumber(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBool(key: string): boolean | undefined {
|
||||||
|
return nullToUndefined(NativeModule.getBool(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addToSet(key: string, value: string): void {
|
||||||
|
return NativeModule.addToSet(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeFromSet(key: string, value: string): void {
|
||||||
|
return NativeModule.removeFromSet(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setContains(key: string, value: string): boolean {
|
||||||
|
return NativeModule.setContains(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS returns `null` if a value does not exist, and Android returns `undefined. Normalize these here for JS types
|
||||||
|
function nullToUndefined(value: any) {
|
||||||
|
if (value == null) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import {NotImplementedError} from '../NotImplemented'
|
||||||
|
|
||||||
|
export function setValue(
|
||||||
|
key: string,
|
||||||
|
value: string | number | boolean | null | undefined,
|
||||||
|
): void {
|
||||||
|
throw new NotImplementedError({key, value})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeValue(key: string): void {
|
||||||
|
throw new NotImplementedError({key})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getString(key: string): string | null {
|
||||||
|
throw new NotImplementedError({key})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNumber(key: string): number | null {
|
||||||
|
throw new NotImplementedError({key})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBool(key: string): boolean | null {
|
||||||
|
throw new NotImplementedError({key})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addToSet(key: string, value: string): void {
|
||||||
|
throw new NotImplementedError({key, value})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeFromSet(key: string, value: string): void {
|
||||||
|
throw new NotImplementedError({key, value})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setContains(key: string, value: string): boolean {
|
||||||
|
throw new NotImplementedError({key, value})
|
||||||
|
}
|
||||||
+19
-17
@@ -13,16 +13,17 @@ import java.io.FileOutputStream
|
|||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
class ExpoReceiveAndroidIntentsModule : Module() {
|
class ExpoReceiveAndroidIntentsModule : Module() {
|
||||||
override fun definition() = ModuleDefinition {
|
override fun definition() =
|
||||||
Name("ExpoReceiveAndroidIntents")
|
ModuleDefinition {
|
||||||
|
Name("ExpoReceiveAndroidIntents")
|
||||||
|
|
||||||
OnNewIntent {
|
OnNewIntent {
|
||||||
handleIntent(it)
|
handleIntent(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleIntent(intent: Intent?) {
|
private fun handleIntent(intent: Intent?) {
|
||||||
if(appContext.currentActivity == null || intent == null) return
|
if (appContext.currentActivity == null || intent == null) return
|
||||||
|
|
||||||
if (intent.action == Intent.ACTION_SEND) {
|
if (intent.action == Intent.ACTION_SEND) {
|
||||||
if (intent.type == "text/plain") {
|
if (intent.type == "text/plain") {
|
||||||
@@ -40,7 +41,7 @@ class ExpoReceiveAndroidIntentsModule : Module() {
|
|||||||
private fun handleTextIntent(intent: Intent) {
|
private fun handleTextIntent(intent: Intent) {
|
||||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||||
val encoded = URLEncoder.encode(it, "UTF-8")
|
val encoded = URLEncoder.encode(it, "UTF-8")
|
||||||
"bluesky://intent/compose?text=${encoded}".toUri().let { uri ->
|
"bluesky://intent/compose?text=$encoded".toUri().let { uri ->
|
||||||
val newIntent = Intent(Intent.ACTION_VIEW, uri)
|
val newIntent = Intent(Intent.ACTION_VIEW, uri)
|
||||||
appContext.currentActivity?.startActivity(newIntent)
|
appContext.currentActivity?.startActivity(newIntent)
|
||||||
}
|
}
|
||||||
@@ -48,11 +49,12 @@ class ExpoReceiveAndroidIntentsModule : Module() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleImageIntent(intent: Intent) {
|
private fun handleImageIntent(intent: Intent) {
|
||||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
val uri =
|
||||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
} else {
|
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
} else {
|
||||||
}
|
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||||
|
}
|
||||||
if (uri == null) return
|
if (uri == null) return
|
||||||
|
|
||||||
handleImageIntents(listOf(uri))
|
handleImageIntents(listOf(uri))
|
||||||
@@ -76,16 +78,16 @@ class ExpoReceiveAndroidIntentsModule : Module() {
|
|||||||
uris.forEachIndexed { index, uri ->
|
uris.forEachIndexed { index, uri ->
|
||||||
val info = getImageInfo(uri)
|
val info = getImageInfo(uri)
|
||||||
val params = buildUriData(info)
|
val params = buildUriData(info)
|
||||||
allParams = "${allParams}${params}"
|
allParams = "${allParams}$params"
|
||||||
|
|
||||||
if (index < uris.count() - 1) {
|
if (index < uris.count() - 1) {
|
||||||
allParams = "${allParams},"
|
allParams = "$allParams,"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val encoded = URLEncoder.encode(allParams, "UTF-8")
|
val encoded = URLEncoder.encode(allParams, "UTF-8")
|
||||||
|
|
||||||
"bluesky://intent/compose?imageUris=${encoded}".toUri().let {
|
"bluesky://intent/compose?imageUris=$encoded".toUri().let {
|
||||||
val newIntent = Intent(Intent.ACTION_VIEW, it)
|
val newIntent = Intent(Intent.ACTION_VIEW, it)
|
||||||
appContext.currentActivity?.startActivity(newIntent)
|
appContext.currentActivity?.startActivity(newIntent)
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,7 @@ class ExpoReceiveAndroidIntentsModule : Module() {
|
|||||||
return mapOf(
|
return mapOf(
|
||||||
"width" to bitmap.width,
|
"width" to bitmap.width,
|
||||||
"height" to bitmap.height,
|
"height" to bitmap.height,
|
||||||
"path" to file.path.toString()
|
"path" to file.path.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +116,6 @@ class ExpoReceiveAndroidIntentsModule : Module() {
|
|||||||
val path = info.getValue("path")
|
val path = info.getValue("path")
|
||||||
val width = info.getValue("width")
|
val width = info.getValue("width")
|
||||||
val height = info.getValue("height")
|
val height = info.getValue("height")
|
||||||
return "file://${path}|${width}|${height}"
|
return "file://$path|$width|$height"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
|||||||
self.cancelGestureRecognizers = [lpg, tg]
|
self.cancelGestureRecognizers = [lpg, tg]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// We don't want to recognize the scroll pan gesture and the swipe back gesture together
|
// We don't want to recognize the scroll pan gesture and the swipe back gesture together
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer {
|
if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer {
|
||||||
@@ -64,11 +63,11 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
|||||||
|
|
||||||
// This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change
|
// This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change
|
||||||
// the scroll view gesture's delegate, so we add an additional recognizer to detect this.
|
// the scroll view gesture's delegate, so we add an additional recognizer to detect this.
|
||||||
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
|
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) {
|
||||||
self.stopTimer()
|
self.stopTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) -> Void {
|
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
|
||||||
guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else {
|
guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -129,7 +128,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var animTranslation = -translation
|
var animTranslation = -translation
|
||||||
self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { timer in
|
self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { _ in
|
||||||
velocity *= 0.9875
|
velocity *= 0.9875
|
||||||
animTranslation = (-velocity / 120) + animTranslation
|
animTranslation = (-velocity / 120) + animTranslation
|
||||||
|
|
||||||
@@ -190,7 +189,6 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func enableCancelGestureRecognizers() {
|
func enableCancelGestureRecognizers() {
|
||||||
self.cancelGestureRecognizers?.forEach { r in
|
self.cancelGestureRecognizers?.forEach { r in
|
||||||
r.isEnabled = true
|
r.isEnabled = true
|
||||||
@@ -203,11 +201,11 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollToOffset(_ offset: Int, animated: Bool = true) -> Void {
|
func scrollToOffset(_ offset: Int, animated: Bool = true) {
|
||||||
self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated)
|
self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopTimer() -> Void {
|
func stopTimer() {
|
||||||
self.disableCancelGestureRecognizers()
|
self.disableCancelGestureRecognizers()
|
||||||
self.animTimer?.invalidate()
|
self.animTimer?.invalidate()
|
||||||
self.animTimer = nil
|
self.animTimer = nil
|
||||||
|
|||||||
+6
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bsky.app",
|
"name": "bsky.app",
|
||||||
"version": "1.88.0",
|
"version": "1.89.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -29,9 +29,12 @@
|
|||||||
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
|
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
|
||||||
"test-coverage": "NODE_ENV=test jest --coverage",
|
"test-coverage": "NODE_ENV=test jest --coverage",
|
||||||
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
|
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
|
||||||
|
"lint-native": "swiftlint ./modules && ktlint ./modules",
|
||||||
|
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
|
||||||
"typecheck": "tsc --project ./tsconfig.check.json",
|
"typecheck": "tsc --project ./tsconfig.check.json",
|
||||||
"e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
|
"e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
|
||||||
"e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
"e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
|
||||||
|
"e2e:metro-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
|
||||||
"e2e:run": "maestro test __e2e__",
|
"e2e:run": "maestro test __e2e__",
|
||||||
"perf:test": "NODE_ENV=test maestro test",
|
"perf:test": "NODE_ENV=test maestro test",
|
||||||
"perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml",
|
"perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml",
|
||||||
@@ -136,6 +139,7 @@
|
|||||||
"expo-system-ui": "~3.0.4",
|
"expo-system-ui": "~3.0.4",
|
||||||
"expo-task-manager": "~11.8.1",
|
"expo-task-manager": "~11.8.1",
|
||||||
"expo-updates": "~0.25.14",
|
"expo-updates": "~0.25.14",
|
||||||
|
"expo-video": "^1.1.10",
|
||||||
"expo-web-browser": "~13.0.3",
|
"expo-web-browser": "~13.0.3",
|
||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
@@ -166,6 +170,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-keyed-flatten-children": "^3.0.0",
|
"react-keyed-flatten-children": "^3.0.0",
|
||||||
"react-native": "0.74.1",
|
"react-native": "0.74.1",
|
||||||
|
"react-native-compressor": "^1.8.24",
|
||||||
"react-native-date-picker": "^4.4.2",
|
"react-native-date-picker": "^4.4.2",
|
||||||
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const {withAppDelegate} = require('@expo/config-plugins')
|
||||||
|
const {mergeContents} = require('@expo/config-plugins/build/utils/generateCode')
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
module.exports = config => {
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
return withAppDelegate(config, async config => {
|
||||||
|
const delegatePath = path.join(
|
||||||
|
config.modRequest.platformProjectRoot,
|
||||||
|
'AppDelegate.mm',
|
||||||
|
)
|
||||||
|
|
||||||
|
let newContents = config.modResults.contents
|
||||||
|
newContents = mergeContents({
|
||||||
|
src: newContents,
|
||||||
|
anchor: '// Linking API',
|
||||||
|
newSrc: `
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
[defaults setObject:options[UIApplicationOpenURLOptionsSourceApplicationKey] forKey:@"referrerApp"];\n`,
|
||||||
|
offset: 2,
|
||||||
|
tag: 'referrer info - deep links',
|
||||||
|
comment: '//',
|
||||||
|
}).contents
|
||||||
|
|
||||||
|
newContents = mergeContents({
|
||||||
|
src: newContents,
|
||||||
|
anchor: '// Universal Links',
|
||||||
|
newSrc: `
|
||||||
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||||||
|
[defaults setURL:userActivity.referrerURL forKey:@"referrer"];\n`,
|
||||||
|
offset: 2,
|
||||||
|
tag: 'referrer info - universal links',
|
||||||
|
comment: '//',
|
||||||
|
}).contents
|
||||||
|
|
||||||
|
config.modResults.contents = newContents
|
||||||
|
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
}
|
||||||
+20
-1
@@ -31,7 +31,7 @@ import {
|
|||||||
} from 'lib/routes/types'
|
} from 'lib/routes/types'
|
||||||
import {RouteParams, State} from 'lib/routes/types'
|
import {RouteParams, State} from 'lib/routes/types'
|
||||||
import {bskyTitle} from 'lib/strings/headings'
|
import {bskyTitle} from 'lib/strings/headings'
|
||||||
import {isAndroid, isNative} from 'platform/detection'
|
import {isAndroid, isNative, isWeb} from 'platform/detection'
|
||||||
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
|
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
|
||||||
import {AppPasswords} from 'view/screens/AppPasswords'
|
import {AppPasswords} from 'view/screens/AppPasswords'
|
||||||
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
|
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
|
||||||
@@ -39,6 +39,7 @@ import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
|
|||||||
import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed'
|
import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed'
|
||||||
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
|
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
|
||||||
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
||||||
|
import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
|
||||||
import HashtagScreen from '#/screens/Hashtag'
|
import HashtagScreen from '#/screens/Hashtag'
|
||||||
import {ModerationScreen} from '#/screens/Moderation'
|
import {ModerationScreen} from '#/screens/Moderation'
|
||||||
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||||
@@ -48,6 +49,7 @@ import {
|
|||||||
StarterPackScreenShort,
|
StarterPackScreenShort,
|
||||||
} from '#/screens/StarterPack/StarterPackScreen'
|
} from '#/screens/StarterPack/StarterPackScreen'
|
||||||
import {Wizard} from '#/screens/StarterPack/Wizard'
|
import {Wizard} from '#/screens/StarterPack/Wizard'
|
||||||
|
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||||
import {init as initAnalytics} from './lib/analytics/analytics'
|
import {init as initAnalytics} from './lib/analytics/analytics'
|
||||||
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
||||||
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
|
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
|
||||||
@@ -233,6 +235,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
getComponent={() => DebugModScreen}
|
getComponent={() => DebugModScreen}
|
||||||
options={{title: title(msg`Moderation states`), requireAuth: true}}
|
options={{title: title(msg`Moderation states`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="SharedPreferencesTester"
|
||||||
|
getComponent={() => SharedPreferencesTesterScreen}
|
||||||
|
options={{title: title(msg`Shared Preferences Tester`)}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Log"
|
name="Log"
|
||||||
getComponent={() => LogScreen}
|
getComponent={() => LogScreen}
|
||||||
@@ -763,6 +770,18 @@ function logModuleInitTime() {
|
|||||||
initMs,
|
initMs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (isWeb) {
|
||||||
|
Referrer.getReferrerInfoAsync().then(info => {
|
||||||
|
if (info && info.hostname !== 'bsky.app') {
|
||||||
|
logEvent('deepLink:referrerReceived', {
|
||||||
|
to: window.location.href,
|
||||||
|
referrer: info?.referrer,
|
||||||
|
hostname: info?.hostname,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// This log is noisy, so keep false committed
|
// This log is noisy, so keep false committed
|
||||||
const shouldLog = false
|
const shouldLog = false
|
||||||
|
|||||||
+16
-5
@@ -1,7 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Dimensions} from 'react-native'
|
import {Dimensions} from 'react-native'
|
||||||
|
|
||||||
import * as themes from '#/alf/themes'
|
import {createThemes, defaultTheme} from '#/alf/themes'
|
||||||
|
import {Theme, ThemeName} from '#/alf/types'
|
||||||
|
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||||
|
|
||||||
export {atoms} from '#/alf/atoms'
|
export {atoms} from '#/alf/atoms'
|
||||||
export * as tokens from '#/alf/tokens'
|
export * as tokens from '#/alf/tokens'
|
||||||
@@ -39,8 +41,8 @@ function getActiveBreakpoints({width}: {width: number}) {
|
|||||||
* Context
|
* Context
|
||||||
*/
|
*/
|
||||||
export const Context = React.createContext<{
|
export const Context = React.createContext<{
|
||||||
themeName: themes.ThemeName
|
themeName: ThemeName
|
||||||
theme: themes.Theme
|
theme: Theme
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
active: BreakpointName | undefined
|
active: BreakpointName | undefined
|
||||||
gtPhone: boolean
|
gtPhone: boolean
|
||||||
@@ -49,7 +51,7 @@ export const Context = React.createContext<{
|
|||||||
}
|
}
|
||||||
}>({
|
}>({
|
||||||
themeName: 'light',
|
themeName: 'light',
|
||||||
theme: themes.light,
|
theme: defaultTheme,
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
active: undefined,
|
active: undefined,
|
||||||
gtPhone: false,
|
gtPhone: false,
|
||||||
@@ -61,7 +63,16 @@ export const Context = React.createContext<{
|
|||||||
export function ThemeProvider({
|
export function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
theme: themeName,
|
theme: themeName,
|
||||||
}: React.PropsWithChildren<{theme: themes.ThemeName}>) {
|
}: React.PropsWithChildren<{theme: ThemeName}>) {
|
||||||
|
const themes = React.useMemo(() => {
|
||||||
|
return createThemes({
|
||||||
|
hues: {
|
||||||
|
primary: BLUE_HUE,
|
||||||
|
negative: RED_HUE,
|
||||||
|
positive: GREEN_HUE,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
const theme = themes[themeName]
|
const theme = themes[themeName]
|
||||||
const [breakpoints, setBreakpoints] = React.useState(() =>
|
const [breakpoints, setBreakpoints] = React.useState(() =>
|
||||||
getActiveBreakpoints({width: Dimensions.get('window').width}),
|
getActiveBreakpoints({width: Dimensions.get('window').width}),
|
||||||
|
|||||||
+569
-447
File diff suppressed because it is too large
Load Diff
@@ -1,77 +1,6 @@
|
|||||||
import {
|
|
||||||
BLUE_HUE,
|
|
||||||
generateScale,
|
|
||||||
GREEN_HUE,
|
|
||||||
RED_HUE,
|
|
||||||
} from '#/alf/util/colorGeneration'
|
|
||||||
|
|
||||||
export const scale = generateScale(6, 100)
|
|
||||||
// dim shifted 6% lighter
|
|
||||||
export const dimScale = generateScale(12, 100)
|
|
||||||
|
|
||||||
export const color = {
|
export const color = {
|
||||||
trueBlack: '#000000',
|
|
||||||
|
|
||||||
temp_purple: 'rgb(105 0 255)',
|
temp_purple: 'rgb(105 0 255)',
|
||||||
temp_purple_dark: 'rgb(83 0 202)',
|
temp_purple_dark: 'rgb(83 0 202)',
|
||||||
|
|
||||||
gray_0: `hsl(${BLUE_HUE}, 20%, ${scale[14]}%)`,
|
|
||||||
gray_25: `hsl(${BLUE_HUE}, 20%, ${scale[13]}%)`,
|
|
||||||
gray_50: `hsl(${BLUE_HUE}, 20%, ${scale[12]}%)`,
|
|
||||||
gray_100: `hsl(${BLUE_HUE}, 20%, ${scale[11]}%)`,
|
|
||||||
gray_200: `hsl(${BLUE_HUE}, 20%, ${scale[10]}%)`,
|
|
||||||
gray_300: `hsl(${BLUE_HUE}, 20%, ${scale[9]}%)`,
|
|
||||||
gray_400: `hsl(${BLUE_HUE}, 20%, ${scale[8]}%)`,
|
|
||||||
gray_500: `hsl(${BLUE_HUE}, 20%, ${scale[7]}%)`,
|
|
||||||
gray_600: `hsl(${BLUE_HUE}, 24%, ${scale[6]}%)`,
|
|
||||||
gray_700: `hsl(${BLUE_HUE}, 24%, ${scale[5]}%)`,
|
|
||||||
gray_800: `hsl(${BLUE_HUE}, 28%, ${scale[4]}%)`,
|
|
||||||
gray_900: `hsl(${BLUE_HUE}, 28%, ${scale[3]}%)`,
|
|
||||||
gray_950: `hsl(${BLUE_HUE}, 28%, ${scale[2]}%)`,
|
|
||||||
gray_975: `hsl(${BLUE_HUE}, 28%, ${scale[1]}%)`,
|
|
||||||
gray_1000: `hsl(${BLUE_HUE}, 28%, ${scale[0]}%)`,
|
|
||||||
|
|
||||||
blue_25: `hsl(${BLUE_HUE}, 99%, 97%)`,
|
|
||||||
blue_50: `hsl(${BLUE_HUE}, 99%, 95%)`,
|
|
||||||
blue_100: `hsl(${BLUE_HUE}, 99%, 90%)`,
|
|
||||||
blue_200: `hsl(${BLUE_HUE}, 99%, 80%)`,
|
|
||||||
blue_300: `hsl(${BLUE_HUE}, 99%, 70%)`,
|
|
||||||
blue_400: `hsl(${BLUE_HUE}, 99%, 60%)`,
|
|
||||||
blue_500: `hsl(${BLUE_HUE}, 99%, 53%)`,
|
|
||||||
blue_600: `hsl(${BLUE_HUE}, 99%, 42%)`,
|
|
||||||
blue_700: `hsl(${BLUE_HUE}, 99%, 34%)`,
|
|
||||||
blue_800: `hsl(${BLUE_HUE}, 99%, 26%)`,
|
|
||||||
blue_900: `hsl(${BLUE_HUE}, 99%, 18%)`,
|
|
||||||
blue_950: `hsl(${BLUE_HUE}, 99%, 10%)`,
|
|
||||||
blue_975: `hsl(${BLUE_HUE}, 99%, 7%)`,
|
|
||||||
|
|
||||||
green_25: `hsl(${GREEN_HUE}, 82%, 97%)`,
|
|
||||||
green_50: `hsl(${GREEN_HUE}, 82%, 95%)`,
|
|
||||||
green_100: `hsl(${GREEN_HUE}, 82%, 90%)`,
|
|
||||||
green_200: `hsl(${GREEN_HUE}, 82%, 80%)`,
|
|
||||||
green_300: `hsl(${GREEN_HUE}, 82%, 70%)`,
|
|
||||||
green_400: `hsl(${GREEN_HUE}, 82%, 60%)`,
|
|
||||||
green_500: `hsl(${GREEN_HUE}, 82%, 50%)`,
|
|
||||||
green_600: `hsl(${GREEN_HUE}, 82%, 42%)`,
|
|
||||||
green_700: `hsl(${GREEN_HUE}, 82%, 34%)`,
|
|
||||||
green_800: `hsl(${GREEN_HUE}, 82%, 26%)`,
|
|
||||||
green_900: `hsl(${GREEN_HUE}, 82%, 18%)`,
|
|
||||||
green_950: `hsl(${GREEN_HUE}, 82%, 10%)`,
|
|
||||||
green_975: `hsl(${GREEN_HUE}, 82%, 7%)`,
|
|
||||||
|
|
||||||
red_25: `hsl(${RED_HUE}, 91%, 97%)`,
|
|
||||||
red_50: `hsl(${RED_HUE}, 91%, 95%)`,
|
|
||||||
red_100: `hsl(${RED_HUE}, 91%, 90%)`,
|
|
||||||
red_200: `hsl(${RED_HUE}, 91%, 80%)`,
|
|
||||||
red_300: `hsl(${RED_HUE}, 91%, 70%)`,
|
|
||||||
red_400: `hsl(${RED_HUE}, 91%, 60%)`,
|
|
||||||
red_500: `hsl(${RED_HUE}, 91%, 50%)`,
|
|
||||||
red_600: `hsl(${RED_HUE}, 91%, 42%)`,
|
|
||||||
red_700: `hsl(${RED_HUE}, 91%, 34%)`,
|
|
||||||
red_800: `hsl(${RED_HUE}, 91%, 26%)`,
|
|
||||||
red_900: `hsl(${RED_HUE}, 91%, 18%)`,
|
|
||||||
red_950: `hsl(${RED_HUE}, 91%, 10%)`,
|
|
||||||
red_975: `hsl(${RED_HUE}, 91%, 7%)`,
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const space = {
|
export const space = {
|
||||||
@@ -178,10 +107,3 @@ export const gradients = {
|
|||||||
hover_value: '#755B62',
|
hover_value: '#755B62',
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type Color = keyof typeof color
|
|
||||||
export type Space = keyof typeof space
|
|
||||||
export type FontSize = keyof typeof fontSize
|
|
||||||
export type LineHeight = keyof typeof lineHeight
|
|
||||||
export type BorderRadius = keyof typeof borderRadius
|
|
||||||
export type FontWeight = keyof typeof fontWeight
|
|
||||||
|
|||||||
+154
-18
@@ -1,21 +1,4 @@
|
|||||||
import {StyleProp, ViewStyle, TextStyle} from 'react-native'
|
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
|
||||||
|
|
||||||
type LiteralToCommon<T extends PropertyKey> = T extends number
|
|
||||||
? number
|
|
||||||
: T extends string
|
|
||||||
? string
|
|
||||||
: T extends symbol
|
|
||||||
? symbol
|
|
||||||
: never
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see https://stackoverflow.com/questions/68249999/use-as-const-in-typescript-without-adding-readonly-modifiers
|
|
||||||
*/
|
|
||||||
export type Mutable<T> = {
|
|
||||||
-readonly [K in keyof T]: T[K] extends PropertyKey
|
|
||||||
? LiteralToCommon<T[K]>
|
|
||||||
: Mutable<T[K]>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TextStyleProp = {
|
export type TextStyleProp = {
|
||||||
style?: StyleProp<TextStyle>
|
style?: StyleProp<TextStyle>
|
||||||
@@ -24,3 +7,156 @@ export type TextStyleProp = {
|
|||||||
export type ViewStyleProp = {
|
export type ViewStyleProp = {
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ThemeName = 'light' | 'dim' | 'dark'
|
||||||
|
export type Palette = {
|
||||||
|
white: string
|
||||||
|
black: string
|
||||||
|
|
||||||
|
contrast_25: string
|
||||||
|
contrast_50: string
|
||||||
|
contrast_100: string
|
||||||
|
contrast_200: string
|
||||||
|
contrast_300: string
|
||||||
|
contrast_400: string
|
||||||
|
contrast_500: string
|
||||||
|
contrast_600: string
|
||||||
|
contrast_700: string
|
||||||
|
contrast_800: string
|
||||||
|
contrast_900: string
|
||||||
|
contrast_950: string
|
||||||
|
contrast_975: string
|
||||||
|
|
||||||
|
primary_25: string
|
||||||
|
primary_50: string
|
||||||
|
primary_100: string
|
||||||
|
primary_200: string
|
||||||
|
primary_300: string
|
||||||
|
primary_400: string
|
||||||
|
primary_500: string
|
||||||
|
primary_600: string
|
||||||
|
primary_700: string
|
||||||
|
primary_800: string
|
||||||
|
primary_900: string
|
||||||
|
primary_950: string
|
||||||
|
primary_975: string
|
||||||
|
|
||||||
|
positive_25: string
|
||||||
|
positive_50: string
|
||||||
|
positive_100: string
|
||||||
|
positive_200: string
|
||||||
|
positive_300: string
|
||||||
|
positive_400: string
|
||||||
|
positive_500: string
|
||||||
|
positive_600: string
|
||||||
|
positive_700: string
|
||||||
|
positive_800: string
|
||||||
|
positive_900: string
|
||||||
|
positive_950: string
|
||||||
|
positive_975: string
|
||||||
|
|
||||||
|
negative_25: string
|
||||||
|
negative_50: string
|
||||||
|
negative_100: string
|
||||||
|
negative_200: string
|
||||||
|
negative_300: string
|
||||||
|
negative_400: string
|
||||||
|
negative_500: string
|
||||||
|
negative_600: string
|
||||||
|
negative_700: string
|
||||||
|
negative_800: string
|
||||||
|
negative_900: string
|
||||||
|
negative_950: string
|
||||||
|
negative_975: string
|
||||||
|
}
|
||||||
|
export type ThemedAtoms = {
|
||||||
|
text: {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
text_contrast_low: {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
text_contrast_medium: {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
text_contrast_high: {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
text_inverted: {
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
bg: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_25: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_50: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_100: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_200: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_300: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_400: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_500: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_600: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_700: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_800: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_900: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_950: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
bg_contrast_975: {
|
||||||
|
backgroundColor: string
|
||||||
|
}
|
||||||
|
border_contrast_low: {
|
||||||
|
borderColor: string
|
||||||
|
}
|
||||||
|
border_contrast_medium: {
|
||||||
|
borderColor: string
|
||||||
|
}
|
||||||
|
border_contrast_high: {
|
||||||
|
borderColor: string
|
||||||
|
}
|
||||||
|
shadow_sm: {
|
||||||
|
shadowRadius: number
|
||||||
|
shadowOpacity: number
|
||||||
|
elevation: number
|
||||||
|
shadowColor: string
|
||||||
|
}
|
||||||
|
shadow_md: {
|
||||||
|
shadowRadius: number
|
||||||
|
shadowOpacity: number
|
||||||
|
elevation: number
|
||||||
|
shadowColor: string
|
||||||
|
}
|
||||||
|
shadow_lg: {
|
||||||
|
shadowRadius: number
|
||||||
|
shadowOpacity: number
|
||||||
|
elevation: number
|
||||||
|
shadowColor: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export type Theme = {
|
||||||
|
name: ThemeName
|
||||||
|
palette: Palette
|
||||||
|
atoms: ThemedAtoms
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ export function generateScale(start: number, end: number) {
|
|||||||
return start + range * stop
|
return start + range * stop
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultScale = generateScale(6, 100)
|
||||||
|
// dim shifted 6% lighter
|
||||||
|
export const dimScale = generateScale(12, 100)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ThemeName} from '#/alf/themes'
|
import {ThemeName} from '#/alf/types'
|
||||||
|
|
||||||
export function select<T>(name: ThemeName, options: Record<ThemeName, T>) {
|
export function select<T>(name: ThemeName, options: Record<ThemeName, T>) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import * as SystemUI from 'expo-system-ui'
|
|||||||
|
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {useThemePrefs} from 'state/shell'
|
import {useThemePrefs} from 'state/shell'
|
||||||
import {dark, dim, light, ThemeName} from '#/alf/themes'
|
import {dark, dim, light} from '#/alf/themes'
|
||||||
|
import {ThemeName} from '#/alf/types'
|
||||||
|
|
||||||
export function useColorModeTheme(): ThemeName {
|
export function useColorModeTheme(): ThemeName {
|
||||||
const theme = useThemeName()
|
const theme = useThemeName()
|
||||||
|
|||||||
+34
-28
@@ -13,7 +13,7 @@ import {
|
|||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
import {LinearGradient} from 'expo-linear-gradient'
|
||||||
|
|
||||||
import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
|
import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
|
||||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||||
import {normalizeTextStyles} from '#/components/Typography'
|
import {normalizeTextStyles} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -152,7 +152,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
const {baseStyles, hoverStyles} = React.useMemo(() => {
|
const {baseStyles, hoverStyles} = React.useMemo(() => {
|
||||||
const baseStyles: ViewStyle[] = []
|
const baseStyles: ViewStyle[] = []
|
||||||
const hoverStyles: ViewStyle[] = []
|
const hoverStyles: ViewStyle[] = []
|
||||||
const light = t.name === 'light'
|
|
||||||
|
|
||||||
if (color === 'primary') {
|
if (color === 'primary') {
|
||||||
if (variant === 'solid') {
|
if (variant === 'solid') {
|
||||||
@@ -165,7 +164,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: t.palette.primary_700,
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.primary_700,
|
||||||
|
dim: t.palette.primary_300,
|
||||||
|
dark: t.palette.primary_300,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (variant === 'outline') {
|
} else if (variant === 'outline') {
|
||||||
@@ -178,24 +181,18 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
borderColor: t.palette.primary_500,
|
borderColor: t.palette.primary_500,
|
||||||
})
|
})
|
||||||
hoverStyles.push(a.border, {
|
hoverStyles.push(a.border, {
|
||||||
backgroundColor: light
|
backgroundColor: t.palette.primary_50,
|
||||||
? t.palette.primary_50
|
|
||||||
: t.palette.primary_950,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push(a.border, {
|
baseStyles.push(a.border, {
|
||||||
borderColor: light
|
borderColor: t.palette.primary_200,
|
||||||
? t.palette.primary_200
|
|
||||||
: t.palette.primary_900,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (variant === 'ghost') {
|
} else if (variant === 'ghost') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push(t.atoms.bg)
|
baseStyles.push(t.atoms.bg)
|
||||||
hoverStyles.push({
|
hoverStyles.push({
|
||||||
backgroundColor: light
|
backgroundColor: t.palette.primary_100,
|
||||||
? t.palette.primary_100
|
|
||||||
: t.palette.primary_900,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,14 +200,26 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
if (variant === 'solid') {
|
if (variant === 'solid') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: t.palette.contrast_25,
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.contrast_25,
|
||||||
|
dim: t.palette.contrast_100,
|
||||||
|
dark: t.palette.contrast_100,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
hoverStyles.push({
|
hoverStyles.push({
|
||||||
backgroundColor: t.palette.contrast_50,
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.contrast_50,
|
||||||
|
dim: t.palette.contrast_200,
|
||||||
|
dark: t.palette.contrast_200,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: t.palette.contrast_100,
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.contrast_100,
|
||||||
|
dim: t.palette.contrast_25,
|
||||||
|
dark: t.palette.contrast_25,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (variant === 'outline') {
|
} else if (variant === 'outline') {
|
||||||
@@ -247,7 +256,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: t.palette.contrast_700,
|
backgroundColor: t.palette.contrast_600,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (variant === 'outline') {
|
} else if (variant === 'outline') {
|
||||||
@@ -284,7 +293,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
backgroundColor: t.palette.negative_700,
|
backgroundColor: select(t.name, {
|
||||||
|
light: t.palette.negative_700,
|
||||||
|
dim: t.palette.negative_300,
|
||||||
|
dark: t.palette.negative_300,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (variant === 'outline') {
|
} else if (variant === 'outline') {
|
||||||
@@ -297,24 +310,18 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
|||||||
borderColor: t.palette.negative_500,
|
borderColor: t.palette.negative_500,
|
||||||
})
|
})
|
||||||
hoverStyles.push(a.border, {
|
hoverStyles.push(a.border, {
|
||||||
backgroundColor: light
|
backgroundColor: t.palette.negative_50,
|
||||||
? t.palette.negative_50
|
|
||||||
: t.palette.negative_975,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push(a.border, {
|
baseStyles.push(a.border, {
|
||||||
borderColor: light
|
borderColor: t.palette.negative_200,
|
||||||
? t.palette.negative_200
|
|
||||||
: t.palette.negative_900,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if (variant === 'ghost') {
|
} else if (variant === 'ghost') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push(t.atoms.bg)
|
baseStyles.push(t.atoms.bg)
|
||||||
hoverStyles.push({
|
hoverStyles.push({
|
||||||
backgroundColor: light
|
backgroundColor: t.palette.negative_100,
|
||||||
? t.palette.negative_100
|
|
||||||
: t.palette.negative_975,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,7 +489,6 @@ export function useSharedButtonTextStyles() {
|
|||||||
const {color, variant, disabled, size} = useButtonContext()
|
const {color, variant, disabled, size} = useButtonContext()
|
||||||
return React.useMemo(() => {
|
return React.useMemo(() => {
|
||||||
const baseStyles: TextStyle[] = []
|
const baseStyles: TextStyle[] = []
|
||||||
const light = t.name === 'light'
|
|
||||||
|
|
||||||
if (color === 'primary') {
|
if (color === 'primary') {
|
||||||
if (variant === 'solid') {
|
if (variant === 'solid') {
|
||||||
@@ -494,7 +500,7 @@ export function useSharedButtonTextStyles() {
|
|||||||
} else if (variant === 'outline') {
|
} else if (variant === 'outline') {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
baseStyles.push({
|
baseStyles.push({
|
||||||
color: light ? t.palette.primary_600 : t.palette.primary_500,
|
color: t.palette.primary_600,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
|
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {logger} from '#/logger'
|
|||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||||
import {useProfilesQuery} from '#/state/queries/profile'
|
import {useProfilesQuery} from '#/state/queries/profile'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||||
import * as userActionHistory from '#/state/userActionHistory'
|
import * as userActionHistory from '#/state/userActionHistory'
|
||||||
import {SeenPost} from '#/state/userActionHistory'
|
import {SeenPost} from '#/state/userActionHistory'
|
||||||
@@ -27,6 +28,8 @@ import * as ProfileCard from '#/components/ProfileCard'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {ProgressGuideList} from './ProgressGuide/List'
|
import {ProgressGuideList} from './ProgressGuide/List'
|
||||||
|
|
||||||
|
const MOBILE_CARD_WIDTH = 300
|
||||||
|
|
||||||
function CardOuter({
|
function CardOuter({
|
||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
@@ -43,7 +46,7 @@ function CardOuter({
|
|||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
!gtMobile && {
|
!gtMobile && {
|
||||||
width: 300,
|
width: MOBILE_CARD_WIDTH,
|
||||||
},
|
},
|
||||||
style,
|
style,
|
||||||
]}>
|
]}>
|
||||||
@@ -125,6 +128,7 @@ function sortSeenPosts(postA: SeenPost, postB: SeenPost): 0 | 1 | -1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useExperimentalSuggestedUsersQuery() {
|
function useExperimentalSuggestedUsersQuery() {
|
||||||
|
const {currentAccount} = useSession()
|
||||||
const userActionSnapshot = userActionHistory.useActionHistorySnapshot()
|
const userActionSnapshot = userActionHistory.useActionHistorySnapshot()
|
||||||
const dids = React.useMemo(() => {
|
const dids = React.useMemo(() => {
|
||||||
const {likes, follows, seen} = userActionSnapshot
|
const {likes, follows, seen} = userActionSnapshot
|
||||||
@@ -136,8 +140,10 @@ function useExperimentalSuggestedUsersQuery() {
|
|||||||
.sort(sortSeenPosts)
|
.sort(sortSeenPosts)
|
||||||
.map(l => new AtUri(l.uri))
|
.map(l => new AtUri(l.uri))
|
||||||
.map(uri => uri.host)
|
.map(uri => uri.host)
|
||||||
return [...new Set([...likeDids, ...seenDids])]
|
return [...new Set([...likeDids, ...seenDids])].filter(
|
||||||
}, [userActionSnapshot])
|
did => did !== currentAccount?.did,
|
||||||
|
)
|
||||||
|
}, [userActionSnapshot, currentAccount])
|
||||||
const {data, isLoading, error} = useProfilesQuery({
|
const {data, isLoading, error} = useProfilesQuery({
|
||||||
handles: dids.slice(0, 16),
|
handles: dids.slice(0, 16),
|
||||||
})
|
})
|
||||||
@@ -210,6 +216,7 @@ export function SuggestedFollows() {
|
|||||||
/>
|
/>
|
||||||
<ProfileCard.FollowButton
|
<ProfileCard.FollowButton
|
||||||
profile={profile}
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
logContext="FeedInterstitial"
|
logContext="FeedInterstitial"
|
||||||
color="secondary_inverted"
|
color="secondary_inverted"
|
||||||
shape="round"
|
shape="round"
|
||||||
@@ -266,7 +273,11 @@ export function SuggestedFollows() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||||
|
decelerationRate="fast">
|
||||||
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
||||||
{content}
|
{content}
|
||||||
|
|
||||||
@@ -392,7 +403,11 @@ export function SuggestedFeeds() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||||
|
decelerationRate="fast">
|
||||||
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
||||||
{content}
|
{content}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import React from 'react'
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useTheme,
|
|
||||||
atoms as a,
|
atoms as a,
|
||||||
ViewStyleProp,
|
|
||||||
TextStyleProp,
|
|
||||||
flatten,
|
flatten,
|
||||||
|
TextStyleProp,
|
||||||
|
useTheme,
|
||||||
|
ViewStyleProp,
|
||||||
} from '#/alf'
|
} from '#/alf'
|
||||||
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
|
||||||
import {Props} from '#/components/icons/common'
|
import {Props} from '#/components/icons/common'
|
||||||
|
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
||||||
|
|
||||||
export function IconCircle({
|
export function IconCircle({
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
@@ -32,8 +32,7 @@ export function IconCircle({
|
|||||||
{
|
{
|
||||||
width: size === 'lg' ? 52 : 64,
|
width: size === 'lg' ? 52 : 64,
|
||||||
height: size === 'lg' ? 52 : 64,
|
height: size === 'lg' ? 52 : 64,
|
||||||
backgroundColor:
|
backgroundColor: t.palette.primary_50,
|
||||||
t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950,
|
|
||||||
},
|
},
|
||||||
flatten(style),
|
flatten(style),
|
||||||
]}>
|
]}>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {Link, LinkProps} from '#/components/Link'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
const AVI_SIZE = 30
|
const AVI_SIZE = 30
|
||||||
|
const AVI_SIZE_SMALL = 20
|
||||||
const AVI_BORDER = 1
|
const AVI_BORDER = 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,10 +31,12 @@ export function KnownFollowers({
|
|||||||
profile,
|
profile,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
onLinkPress,
|
onLinkPress,
|
||||||
|
minimal,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
onLinkPress?: LinkProps['onPress']
|
onLinkPress?: LinkProps['onPress']
|
||||||
|
minimal?: boolean
|
||||||
}) {
|
}) {
|
||||||
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
|
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
|
||||||
new Map(),
|
new Map(),
|
||||||
@@ -59,6 +62,7 @@ export function KnownFollowers({
|
|||||||
cachedKnownFollowers={cachedKnownFollowers}
|
cachedKnownFollowers={cachedKnownFollowers}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
onLinkPress={onLinkPress}
|
onLinkPress={onLinkPress}
|
||||||
|
minimal={minimal}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -71,11 +75,13 @@ function KnownFollowersInner({
|
|||||||
moderationOpts,
|
moderationOpts,
|
||||||
cachedKnownFollowers,
|
cachedKnownFollowers,
|
||||||
onLinkPress,
|
onLinkPress,
|
||||||
|
minimal,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
||||||
onLinkPress?: LinkProps['onPress']
|
onLinkPress?: LinkProps['onPress']
|
||||||
|
minimal?: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -110,6 +116,8 @@ function KnownFollowersInner({
|
|||||||
*/
|
*/
|
||||||
if (slice.length === 0) return null
|
if (slice.length === 0) return null
|
||||||
|
|
||||||
|
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
label={_(
|
label={_(
|
||||||
@@ -120,7 +128,7 @@ function KnownFollowersInner({
|
|||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
a.flex_1,
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.gap_md,
|
minimal ? a.gap_sm : a.gap_md,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
{marginLeft: -AVI_BORDER},
|
{marginLeft: -AVI_BORDER},
|
||||||
]}>
|
]}>
|
||||||
@@ -129,8 +137,8 @@ function KnownFollowersInner({
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
height: AVI_SIZE,
|
height: SIZE,
|
||||||
width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap,
|
width: SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||||
},
|
},
|
||||||
pressed && {
|
pressed && {
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
@@ -145,14 +153,14 @@ function KnownFollowersInner({
|
|||||||
{
|
{
|
||||||
borderWidth: AVI_BORDER,
|
borderWidth: AVI_BORDER,
|
||||||
borderColor: t.atoms.bg.backgroundColor,
|
borderColor: t.atoms.bg.backgroundColor,
|
||||||
width: AVI_SIZE + AVI_BORDER * 2,
|
width: SIZE + AVI_BORDER * 2,
|
||||||
height: AVI_SIZE + AVI_BORDER * 2,
|
height: SIZE + AVI_BORDER * 2,
|
||||||
left: i * a.gap_md.gap,
|
left: i * a.gap_md.gap,
|
||||||
zIndex: AVI_BORDER - i,
|
zIndex: AVI_BORDER - i,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
size={AVI_SIZE}
|
size={SIZE}
|
||||||
avatar={prof.avatar}
|
avatar={prof.avatar}
|
||||||
moderation={moderation.ui('avatar')}
|
moderation={moderation.ui('avatar')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ export function Card({
|
|||||||
<Header>
|
<Header>
|
||||||
<Avatar profile={profile} moderationOpts={moderationOpts} />
|
<Avatar profile={profile} moderationOpts={moderationOpts} />
|
||||||
<NameAndHandle profile={profile} moderationOpts={moderationOpts} />
|
<NameAndHandle profile={profile} moderationOpts={moderationOpts} />
|
||||||
<FollowButton profile={profile} logContext={logContext} />
|
<FollowButton
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
logContext={logContext}
|
||||||
|
/>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<ProfileCardPills
|
<ProfileCardPills
|
||||||
@@ -162,7 +166,9 @@ export function NameAndHandle({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
<Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}>
|
<Text
|
||||||
|
style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]}
|
||||||
|
numberOfLines={1}>
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -261,6 +267,7 @@ export function DescriptionPlaceholder() {
|
|||||||
|
|
||||||
export type FollowButtonProps = {
|
export type FollowButtonProps = {
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
|
moderationOpts: ModerationOpts
|
||||||
logContext: LogEvents['profile:follow']['logContext'] &
|
logContext: LogEvents['profile:follow']['logContext'] &
|
||||||
LogEvents['profile:unfollow']['logContext']
|
LogEvents['profile:unfollow']['logContext']
|
||||||
} & Partial<ButtonProps>
|
} & Partial<ButtonProps>
|
||||||
@@ -273,11 +280,13 @@ export function FollowButton(props: FollowButtonProps) {
|
|||||||
|
|
||||||
export function FollowButtonInner({
|
export function FollowButtonInner({
|
||||||
profile: profileUnshadowed,
|
profile: profileUnshadowed,
|
||||||
|
moderationOpts,
|
||||||
logContext,
|
logContext,
|
||||||
...rest
|
...rest
|
||||||
}: FollowButtonProps) {
|
}: FollowButtonProps) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const profile = useProfileShadow(profileUnshadowed)
|
const profile = useProfileShadow(profileUnshadowed)
|
||||||
|
const moderation = moderateProfile(profile, moderationOpts)
|
||||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
|
||||||
profile,
|
profile,
|
||||||
logContext,
|
logContext,
|
||||||
@@ -289,6 +298,14 @@ export function FollowButtonInner({
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
try {
|
try {
|
||||||
await queueFollow()
|
await queueFollow()
|
||||||
|
Toast.show(
|
||||||
|
_(
|
||||||
|
msg`Following ${sanitizeDisplayName(
|
||||||
|
profile.displayName || profile.handle,
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
)}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.name !== 'AbortError') {
|
if (e?.name !== 'AbortError') {
|
||||||
Toast.show(_(msg`An issue occurred, please try again.`))
|
Toast.show(_(msg`An issue occurred, please try again.`))
|
||||||
@@ -301,6 +318,14 @@ export function FollowButtonInner({
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
try {
|
try {
|
||||||
await queueUnfollow()
|
await queueUnfollow()
|
||||||
|
Toast.show(
|
||||||
|
_(
|
||||||
|
msg`No longer following ${sanitizeDisplayName(
|
||||||
|
profile.displayName || profile.handle,
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
)}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.name !== 'AbortError') {
|
if (e?.name !== 'AbortError') {
|
||||||
Toast.show(_(msg`An issue occurred, please try again.`))
|
Toast.show(_(msg`An issue occurred, please try again.`))
|
||||||
|
|||||||
@@ -462,7 +462,8 @@ function Inner({
|
|||||||
|
|
||||||
<Link to={profileURL} label={_(msg`View profile`)} onPress={hide}>
|
<Link to={profileURL} label={_(msg`View profile`)} onPress={hide}>
|
||||||
<View style={[a.pb_sm, a.flex_1]}>
|
<View style={[a.pb_sm, a.flex_1]}>
|
||||||
<Text style={[a.pt_md, a.pb_xs, a.text_lg, a.font_bold]}>
|
<Text
|
||||||
|
style={[a.pt_md, a.pb_xs, a.text_lg, a.font_bold, a.self_start]}>
|
||||||
{sanitizeDisplayName(
|
{sanitizeDisplayName(
|
||||||
profile.displayName || sanitizeHandle(profile.handle),
|
profile.displayName || sanitizeHandle(profile.handle),
|
||||||
moderation.ui('displayName'),
|
moderation.ui('displayName'),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function ProgressGuideTask({
|
|||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.gap_sm, !subtitle && a.align_center]}>
|
<View style={[a.flex_row, a.gap_sm, !subtitle && a.align_center]}>
|
||||||
{current === total ? (
|
{current === total ? (
|
||||||
<AnimatedCheck playOnMount fill={t.palette.primary_500} width={24} />
|
<AnimatedCheck playOnMount fill={t.palette.primary_500} width={20} />
|
||||||
) : (
|
) : (
|
||||||
<Progress.Circle
|
<Progress.Circle
|
||||||
progress={current / total}
|
progress={current / total}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||||
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -36,6 +37,7 @@ export function SubmitView({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
const gate = useGate()
|
||||||
const [details, setDetails] = React.useState<string>('')
|
const [details, setDetails] = React.useState<string>('')
|
||||||
const [submitting, setSubmitting] = React.useState<boolean>(false)
|
const [submitting, setSubmitting] = React.useState<boolean>(false)
|
||||||
const [selectedServices, setSelectedServices] = React.useState<string[]>([
|
const [selectedServices, setSelectedServices] = React.useState<string[]>([
|
||||||
@@ -60,15 +62,29 @@ export function SubmitView({
|
|||||||
reason: details,
|
reason: details,
|
||||||
}
|
}
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
selectedServices.map(did =>
|
selectedServices.map(did => {
|
||||||
agent
|
if (gate('session_withproxy_fix')) {
|
||||||
.withProxy('atproto_labeler', did)
|
return agent
|
||||||
.createModerationReport(report)
|
.createModerationReport(report, {
|
||||||
.then(
|
encoding: 'application/json',
|
||||||
_ => true,
|
headers: {
|
||||||
_ => false,
|
'atproto-proxy': `${did}#atproto_labeler`,
|
||||||
),
|
},
|
||||||
),
|
})
|
||||||
|
.then(
|
||||||
|
_ => true,
|
||||||
|
_ => false,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return agent
|
||||||
|
.withProxy('atproto_labeler', did)
|
||||||
|
.createModerationReport(report)
|
||||||
|
.then(
|
||||||
|
_ => true,
|
||||||
|
_ => false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
@@ -92,6 +108,7 @@ export function SubmitView({
|
|||||||
onSubmitComplete,
|
onSubmitComplete,
|
||||||
setError,
|
setError,
|
||||||
agent,
|
agent,
|
||||||
|
gate,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export function QrCodeInner({link}: {link: string}) {
|
|||||||
innerEyesOptions={{borderRadius: 3}}
|
innerEyesOptions={{borderRadius: 3}}
|
||||||
logo={{
|
logo={{
|
||||||
href: require('../../../assets/logo.png'),
|
href: require('../../../assets/logo.png'),
|
||||||
scale: 1.2,
|
scale: 0.95,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
hidePieces: true,
|
hidePieces: true,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -78,7 +78,13 @@ function WizardListCard({
|
|||||||
/>
|
/>
|
||||||
<View style={[a.flex_1, a.gap_2xs]}>
|
<View style={[a.flex_1, a.gap_2xs]}>
|
||||||
<Text
|
<Text
|
||||||
style={[a.flex_1, a.font_bold, a.text_md, a.leading_tight]}
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.font_bold,
|
||||||
|
a.text_md,
|
||||||
|
a.leading_tight,
|
||||||
|
a.self_start,
|
||||||
|
]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -357,12 +357,11 @@ function TargetToggle({children}: React.PropsWithChildren<{}>) {
|
|||||||
a.px_sm,
|
a.px_sm,
|
||||||
gtMobile && a.px_md,
|
gtMobile && a.px_md,
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
t.atoms.bg_contrast_50,
|
t.atoms.bg_contrast_25,
|
||||||
(ctx.hovered || ctx.focused) && t.atoms.bg_contrast_100,
|
(ctx.hovered || ctx.focused) && t.atoms.bg_contrast_50,
|
||||||
ctx.selected && [
|
ctx.selected && [
|
||||||
{
|
{
|
||||||
backgroundColor:
|
backgroundColor: t.palette.primary_50,
|
||||||
t.name === 'light' ? t.palette.primary_50 : t.palette.primary_975,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
ctx.disabled && {
|
ctx.disabled && {
|
||||||
|
|||||||
@@ -196,10 +196,7 @@ function Selectable({
|
|||||||
t.atoms.bg_contrast_50,
|
t.atoms.bg_contrast_50,
|
||||||
(hovered || focused) && t.atoms.bg_contrast_100,
|
(hovered || focused) && t.atoms.bg_contrast_100,
|
||||||
isSelected && {
|
isSelected && {
|
||||||
backgroundColor:
|
backgroundColor: t.palette.primary_100,
|
||||||
t.name === 'light'
|
|
||||||
? t.palette.primary_50
|
|
||||||
: t.palette.primary_975,
|
|
||||||
},
|
},
|
||||||
style,
|
style,
|
||||||
]}>
|
]}>
|
||||||
|
|||||||
@@ -72,8 +72,7 @@ let MessageItem = ({
|
|||||||
lastInGroupRef.current = isLastInGroup
|
lastInGroupRef.current = isLastInGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingColor =
|
const pendingColor = t.palette.primary_200
|
||||||
t.name === 'light' ? t.palette.primary_200 : t.palette.primary_800
|
|
||||||
|
|
||||||
const rt = useMemo(() => {
|
const rt = useMemo(() => {
|
||||||
return new RichTextAPI({text: message.text, facets: message.facets})
|
return new RichTextAPI({text: message.text, facets: message.facets})
|
||||||
@@ -110,12 +109,7 @@ let MessageItem = ({
|
|||||||
}>
|
}>
|
||||||
<RichText
|
<RichText
|
||||||
value={rt}
|
value={rt}
|
||||||
style={[
|
style={[a.text_md, isFromSelf && {color: t.palette.white}]}
|
||||||
a.text_md,
|
|
||||||
isFromSelf && {color: t.palette.white},
|
|
||||||
isPending &&
|
|
||||||
t.name !== 'light' && {color: t.palette.primary_300},
|
|
||||||
]}
|
|
||||||
interactiveStyle={a.underline}
|
interactiveStyle={a.underline}
|
||||||
enableTags
|
enableTags
|
||||||
emojiMultiplier={3}
|
emojiMultiplier={3}
|
||||||
|
|||||||
@@ -168,7 +168,12 @@ function HeaderReady({
|
|||||||
</View>
|
</View>
|
||||||
<View style={a.flex_1}>
|
<View style={a.flex_1}>
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_md, a.font_bold, web(a.leading_normal)]}
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.font_bold,
|
||||||
|
a.self_start,
|
||||||
|
web(a.leading_normal),
|
||||||
|
]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ function ProfileCard({
|
|||||||
/>
|
/>
|
||||||
<View style={[a.flex_1, a.gap_2xs]}>
|
<View style={[a.flex_1, a.gap_2xs]}>
|
||||||
<Text
|
<Text
|
||||||
style={[t.atoms.text, a.font_bold, a.leading_tight]}
|
style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -101,16 +101,13 @@ export function useSharedInputStyles() {
|
|||||||
]
|
]
|
||||||
const error: ViewStyle[] = [
|
const error: ViewStyle[] = [
|
||||||
{
|
{
|
||||||
backgroundColor:
|
backgroundColor: t.palette.negative_25,
|
||||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
|
borderColor: t.palette.negative_300,
|
||||||
borderColor:
|
|
||||||
t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
const errorHover: ViewStyle[] = [
|
const errorHover: ViewStyle[] = [
|
||||||
{
|
{
|
||||||
backgroundColor:
|
backgroundColor: t.palette.negative_25,
|
||||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
|
|
||||||
borderColor: t.palette.negative_500,
|
borderColor: t.palette.negative_500,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -281,24 +281,20 @@ export function createSharedToggleStyles({
|
|||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
base.push({
|
base.push({
|
||||||
backgroundColor:
|
backgroundColor: t.palette.primary_25,
|
||||||
t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900,
|
|
||||||
borderColor: t.palette.primary_500,
|
borderColor: t.palette.primary_500,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
baseHover.push({
|
baseHover.push({
|
||||||
backgroundColor:
|
backgroundColor: t.palette.primary_100,
|
||||||
t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800,
|
borderColor: t.palette.primary_600,
|
||||||
borderColor:
|
|
||||||
t.name === 'light' ? t.palette.primary_600 : t.palette.primary_400,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
baseHover.push({
|
baseHover.push({
|
||||||
backgroundColor:
|
backgroundColor: t.palette.contrast_50,
|
||||||
t.name === 'light' ? t.palette.contrast_50 : t.palette.contrast_100,
|
|
||||||
borderColor: t.palette.contrast_500,
|
borderColor: t.palette.contrast_500,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -306,16 +302,13 @@ export function createSharedToggleStyles({
|
|||||||
|
|
||||||
if (isInvalid) {
|
if (isInvalid) {
|
||||||
base.push({
|
base.push({
|
||||||
backgroundColor:
|
backgroundColor: t.palette.negative_25,
|
||||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_975,
|
borderColor: t.palette.negative_300,
|
||||||
borderColor:
|
|
||||||
t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
baseHover.push({
|
baseHover.push({
|
||||||
backgroundColor:
|
backgroundColor: t.palette.negative_25,
|
||||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
|
|
||||||
borderColor: t.palette.negative_600,
|
borderColor: t.palette.negative_600,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import {isAndroid} from 'platform/detection'
|
import {isAndroid} from 'platform/detection'
|
||||||
import {useHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
|
import {useHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
|
||||||
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
|
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
|
||||||
import {DevicePrefs, Referrer} from '../../../modules/expo-bluesky-swiss-army'
|
import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army'
|
||||||
|
|
||||||
export function useStarterPackEntry() {
|
export function useStarterPackEntry() {
|
||||||
const [ready, setReady] = React.useState(false)
|
const [ready, setReady] = React.useState(false)
|
||||||
@@ -39,14 +39,10 @@ export function useStarterPackEntry() {
|
|||||||
uri = createStarterPackLinkFromAndroidReferrer(res.installReferrer)
|
uri = createStarterPackLinkFromAndroidReferrer(res.installReferrer)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const res = await DevicePrefs.getStringValueAsync(
|
const starterPackUri = SharedPrefs.getString('starterPackUri')
|
||||||
'starterPackUri',
|
if (starterPackUri) {
|
||||||
true,
|
uri = httpStarterPackUriToAtUri(starterPackUri)
|
||||||
)
|
SharedPrefs.setValue('starterPackUri', null)
|
||||||
|
|
||||||
if (res) {
|
|
||||||
uri = httpStarterPackUriToAtUri(res)
|
|
||||||
DevicePrefs.setStringValueAsync('starterPackUri', null, true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const VideoClip_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 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2.444h2V13Zm0 4.444h-2V19h2v-1.556ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z',
|
||||||
|
})
|
||||||
@@ -4,7 +4,7 @@ import type {PathProps, SvgProps} from 'react-native-svg'
|
|||||||
import {Defs, LinearGradient, Stop} from 'react-native-svg'
|
import {Defs, LinearGradient, Stop} from 'react-native-svg'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {tokens} from '#/alf'
|
import {tokens, useTheme} from '#/alf'
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
fill?: PathProps['fill']
|
fill?: PathProps['fill']
|
||||||
@@ -22,10 +22,11 @@ export const sizes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useCommonSVGProps(props: Props) {
|
export function useCommonSVGProps(props: Props) {
|
||||||
|
const t = useTheme()
|
||||||
const {fill, size, gradient, ...rest} = props
|
const {fill, size, gradient, ...rest} = props
|
||||||
const style = StyleSheet.flatten(rest.style)
|
const style = StyleSheet.flatten(rest.style)
|
||||||
const _size = Number(size ? sizes[size] : rest.width || sizes.md)
|
const _size = Number(size ? sizes[size] : rest.width || sizes.md)
|
||||||
let _fill = fill || style?.color || tokens.color.blue_500
|
let _fill = fill || style?.color || t.palette.primary_500
|
||||||
let gradientDef = null
|
let gradientDef = null
|
||||||
|
|
||||||
if (gradient && tokens.gradients[gradient]) {
|
if (gradient && tokens.gradients[gradient]) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {useMutation} from '@tanstack/react-query'
|
|||||||
|
|
||||||
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
@@ -201,22 +202,42 @@ function AppealForm({
|
|||||||
const [details, setDetails] = React.useState('')
|
const [details, setDetails] = React.useState('')
|
||||||
const isAccountReport = 'did' in subject
|
const isAccountReport = 'did' in subject
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
const gate = useGate()
|
||||||
|
|
||||||
const {mutate, isPending} = useMutation({
|
const {mutate, isPending} = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const $type = !isAccountReport
|
const $type = !isAccountReport
|
||||||
? 'com.atproto.repo.strongRef'
|
? 'com.atproto.repo.strongRef'
|
||||||
: 'com.atproto.admin.defs#repoRef'
|
: 'com.atproto.admin.defs#repoRef'
|
||||||
await agent
|
if (gate('session_withproxy_fix')) {
|
||||||
.withProxy('atproto_labeler', label.src)
|
await agent.createModerationReport(
|
||||||
.createModerationReport({
|
{
|
||||||
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
|
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
|
||||||
subject: {
|
subject: {
|
||||||
$type,
|
$type,
|
||||||
...subject,
|
...subject,
|
||||||
|
},
|
||||||
|
reason: details,
|
||||||
},
|
},
|
||||||
reason: details,
|
{
|
||||||
})
|
encoding: 'application/json',
|
||||||
|
headers: {
|
||||||
|
'atproto-proxy': `${label.src}#atproto_labeler`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await agent
|
||||||
|
.withProxy('atproto_labeler', label.src)
|
||||||
|
.createModerationReport({
|
||||||
|
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
|
||||||
|
subject: {
|
||||||
|
$type,
|
||||||
|
...subject,
|
||||||
|
},
|
||||||
|
reason: details,
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: err => {
|
onError: err => {
|
||||||
logger.error('Failed to submit label appeal', {message: err})
|
logger.error('Failed to submit label appeal', {message: err})
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, {ReactNode, createContext, useContext} from 'react'
|
import React, {createContext, ReactNode, useContext} from 'react'
|
||||||
import {TextStyle, ViewStyle} from 'react-native'
|
import {TextStyle, ViewStyle} from 'react-native'
|
||||||
|
|
||||||
|
import {ThemeName} from '#/alf/types'
|
||||||
import {darkTheme, defaultTheme, dimTheme} from './themes'
|
import {darkTheme, defaultTheme, dimTheme} from './themes'
|
||||||
import {ThemeName} from '#/alf/themes'
|
|
||||||
|
|
||||||
export type ColorScheme = 'light' | 'dark'
|
export type ColorScheme = 'light' | 'dark'
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download'
|
|||||||
// code and update this number with each release until we can get the
|
// code and update this number with each release until we can get the
|
||||||
// server route done.
|
// server route done.
|
||||||
// -prf
|
// -prf
|
||||||
export const JOINED_THIS_WEEK = 37115 // as of June24 2024
|
export const JOINED_THIS_WEEK = 21797 // as of Jul5 2024
|
||||||
|
|
||||||
const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
|
const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
|
||||||
export function FEEDBACK_FORM_URL({
|
export function FEEDBACK_FORM_URL({
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import * as Linking from 'expo-linking'
|
import * as Linking from 'expo-linking'
|
||||||
|
|
||||||
|
import {logEvent} from 'lib/statsig/statsig'
|
||||||
import {isNative} from 'platform/detection'
|
import {isNative} from 'platform/detection'
|
||||||
import {useComposerControls} from 'state/shell'
|
|
||||||
import {useSession} from 'state/session'
|
import {useSession} from 'state/session'
|
||||||
|
import {useComposerControls} from 'state/shell'
|
||||||
import {useCloseAllActiveElements} from 'state/util'
|
import {useCloseAllActiveElements} from 'state/util'
|
||||||
|
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
|
||||||
|
|
||||||
type IntentType = 'compose'
|
type IntentType = 'compose'
|
||||||
|
|
||||||
@@ -15,6 +18,16 @@ export function useIntentHandler() {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleIncomingURL = (url: string) => {
|
const handleIncomingURL = (url: string) => {
|
||||||
|
Referrer.getReferrerInfoAsync().then(info => {
|
||||||
|
if (info && info.hostname !== 'bsky.app') {
|
||||||
|
logEvent('deepLink:referrerReceived', {
|
||||||
|
to: url,
|
||||||
|
referrer: info?.referrer,
|
||||||
|
hostname: info?.hostname,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// We want to be able to support bluesky:// deeplinks. It's unnatural for someone to use a deeplink with three
|
// We want to be able to support bluesky:// deeplinks. It's unnatural for someone to use a deeplink with three
|
||||||
// slashes, like bluesky:///intent/follow. However, supporting just two slashes causes us to have to take care
|
// slashes, like bluesky:///intent/follow. However, supporting just two slashes causes us to have to take care
|
||||||
// of two cases when parsing the url. If we ensure there is a third slash, we can always ensure the first
|
// of two cases when parsing the url. If we ensure there is a third slash, we can always ensure the first
|
||||||
|
|||||||
@@ -48,6 +48,35 @@ export function usePhotoLibraryPermission() {
|
|||||||
return {requestPhotoAccessIfNeeded}
|
return {requestPhotoAccessIfNeeded}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useVideoLibraryPermission() {
|
||||||
|
const [res, requestPermission] = MediaLibrary.usePermissions({
|
||||||
|
granularPermissions: ['video'],
|
||||||
|
})
|
||||||
|
const requestVideoAccessIfNeeded = async () => {
|
||||||
|
// On the, we use <input type="file"> to produce a filepicker
|
||||||
|
// This does not need any permission granting.
|
||||||
|
if (isWeb) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res?.granted) {
|
||||||
|
return true
|
||||||
|
} else if (!res || res.status === 'undetermined' || res?.canAskAgain) {
|
||||||
|
const {canAskAgain, granted, status} = await requestPermission()
|
||||||
|
|
||||||
|
if (!canAskAgain && status === 'undetermined') {
|
||||||
|
openPermissionAlert('video library')
|
||||||
|
}
|
||||||
|
|
||||||
|
return granted
|
||||||
|
} else {
|
||||||
|
openPermissionAlert('video library')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {requestVideoAccessIfNeeded}
|
||||||
|
}
|
||||||
|
|
||||||
export function useCameraPermission() {
|
export function useCameraPermission() {
|
||||||
const [res, requestPermission] = Camera.useCameraPermissions()
|
const [res, requestPermission] = Camera.useCameraPermissions()
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,11 @@ export function useCameraPermission() {
|
|||||||
|
|
||||||
return {requestCameraAccessIfNeeded}
|
return {requestCameraAccessIfNeeded}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useVideoLibraryPermission() {
|
||||||
|
const requestVideoAccessIfNeeded = async () => {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {requestVideoAccessIfNeeded}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {getVideoMetaData, Video} from 'react-native-compressor'
|
||||||
|
|
||||||
|
export type CompressedVideo = {
|
||||||
|
uri: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compressVideo(
|
||||||
|
file: string,
|
||||||
|
opts?: {
|
||||||
|
getCancellationId?: (id: string) => void
|
||||||
|
onProgress?: (progress: number) => void
|
||||||
|
},
|
||||||
|
): Promise<CompressedVideo> {
|
||||||
|
const {onProgress, getCancellationId} = opts || {}
|
||||||
|
|
||||||
|
const compressed = await Video.compress(
|
||||||
|
file,
|
||||||
|
{
|
||||||
|
getCancellationId,
|
||||||
|
compressionMethod: 'manual',
|
||||||
|
bitrate: 3_000_000, // 3mbps
|
||||||
|
maxSize: 1920,
|
||||||
|
},
|
||||||
|
onProgress,
|
||||||
|
)
|
||||||
|
|
||||||
|
const info = await getVideoMetaData(compressed)
|
||||||
|
return {uri: compressed, size: info.size}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import {VideoTooLargeError} from 'lib/media/video/errors'
|
||||||
|
|
||||||
|
const MAX_VIDEO_SIZE = 1024 * 1024 * 100 // 100MB
|
||||||
|
|
||||||
|
export type CompressedVideo = {
|
||||||
|
uri: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// doesn't actually compress, but throws if >100MB
|
||||||
|
export async function compressVideo(
|
||||||
|
file: string,
|
||||||
|
_callbacks?: {
|
||||||
|
onProgress: (progress: number) => void
|
||||||
|
},
|
||||||
|
): Promise<CompressedVideo> {
|
||||||
|
const blob = await fetch(file).then(res => res.blob())
|
||||||
|
const video = URL.createObjectURL(blob)
|
||||||
|
|
||||||
|
if (blob.size > MAX_VIDEO_SIZE) {
|
||||||
|
throw new VideoTooLargeError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
size: blob.size,
|
||||||
|
uri: video,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export class VideoTooLargeError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('Videos cannot be larger than 100MB')
|
||||||
|
this.name = 'VideoTooLargeError'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,6 @@ export function createSanitizedDisplayName(
|
|||||||
if (profile.displayName != null && profile.displayName !== '') {
|
if (profile.displayName != null && profile.displayName !== '') {
|
||||||
return sanitizeDisplayName(profile.displayName)
|
return sanitizeDisplayName(profile.displayName)
|
||||||
} else {
|
} else {
|
||||||
let sanitizedHandle = sanitizeHandle(profile.handle)
|
return sanitizeHandle(profile.handle, noAt ? '' : '@')
|
||||||
if (!noAt) {
|
|
||||||
sanitizedHandle = `@${sanitizedHandle}`
|
|
||||||
}
|
|
||||||
return sanitizedHandle
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export type CommonNavigatorParams = {
|
|||||||
ProfileLabelerLikedBy: {name: string}
|
ProfileLabelerLikedBy: {name: string}
|
||||||
Debug: undefined
|
Debug: undefined
|
||||||
DebugMod: undefined
|
DebugMod: undefined
|
||||||
|
SharedPreferencesTester: undefined
|
||||||
Log: undefined
|
Log: undefined
|
||||||
Support: undefined
|
Support: undefined
|
||||||
PrivacyPolicy: undefined
|
PrivacyPolicy: undefined
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ export type LogEvents = {
|
|||||||
}
|
}
|
||||||
'state:foreground:sampled': {}
|
'state:foreground:sampled': {}
|
||||||
'router:navigate:sampled': {}
|
'router:navigate:sampled': {}
|
||||||
|
'deepLink:referrerReceived': {
|
||||||
|
to: string
|
||||||
|
referrer: string
|
||||||
|
hostname: string
|
||||||
|
}
|
||||||
|
|
||||||
// Screen events
|
// Screen events
|
||||||
'splash:signInPressed': {}
|
'splash:signInPressed': {}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
export type Gate =
|
export type Gate =
|
||||||
// Keep this alphabetic please.
|
// Keep this alphabetic please.
|
||||||
| 'debug_show_feedcontext'
|
| 'debug_show_feedcontext'
|
||||||
|
| 'explore_page_profile_card_social_proof'
|
||||||
| 'native_pwi_disabled'
|
| 'native_pwi_disabled'
|
||||||
| 'new_user_guided_tour'
|
| 'new_user_guided_tour'
|
||||||
| 'new_user_progress_guide'
|
| 'new_user_progress_guide'
|
||||||
| 'onboarding_minimum_interests'
|
| 'onboarding_minimum_interests'
|
||||||
| 'request_notifications_permission_after_onboarding_v2'
|
| 'request_notifications_permission_after_onboarding_v2'
|
||||||
|
| 'session_withproxy_fix'
|
||||||
| 'show_avi_follow_button'
|
| 'show_avi_follow_button'
|
||||||
| 'show_follow_back_label_v2'
|
| 'show_follow_back_label_v2'
|
||||||
| 'suggested_feeds_interstitial'
|
| 'suggested_feeds_interstitial'
|
||||||
| 'suggested_follows_interstitial'
|
| 'suggested_follows_interstitial'
|
||||||
| 'ungroup_follow_backs'
|
| 'ungroup_follow_backs'
|
||||||
|
| 'videos'
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ type StatsigUser = {
|
|||||||
bundleDate: number
|
bundleDate: number
|
||||||
refSrc: string
|
refSrc: string
|
||||||
refUrl: string
|
refUrl: string
|
||||||
referrer: string
|
|
||||||
referrerHostname: string
|
|
||||||
appLanguage: string
|
appLanguage: string
|
||||||
contentLanguages: string[]
|
contentLanguages: string[]
|
||||||
}
|
}
|
||||||
@@ -37,29 +35,12 @@ type StatsigUser = {
|
|||||||
|
|
||||||
let refSrc = ''
|
let refSrc = ''
|
||||||
let refUrl = ''
|
let refUrl = ''
|
||||||
let referrer = ''
|
|
||||||
let referrerHostname = ''
|
|
||||||
if (isWeb && typeof window !== 'undefined') {
|
if (isWeb && typeof window !== 'undefined') {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
refSrc = params.get('ref_src') ?? ''
|
refSrc = params.get('ref_src') ?? ''
|
||||||
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
|
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
isWeb &&
|
|
||||||
typeof document !== 'undefined' &&
|
|
||||||
document != null &&
|
|
||||||
document.referrer
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const url = new URL(document.referrer)
|
|
||||||
if (url.hostname !== 'bsky.app') {
|
|
||||||
referrer = document.referrer
|
|
||||||
referrerHostname = url.hostname
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type {LogEvents}
|
export type {LogEvents}
|
||||||
|
|
||||||
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
|
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
|
||||||
@@ -222,8 +203,6 @@ function toStatsigUser(did: string | undefined): StatsigUser {
|
|||||||
custom: {
|
custom: {
|
||||||
refSrc,
|
refSrc,
|
||||||
refUrl,
|
refUrl,
|
||||||
referrer,
|
|
||||||
referrerHostname,
|
|
||||||
platform: Platform.OS as 'ios' | 'android' | 'web',
|
platform: Platform.OS as 'ios' | 'android' | 'web',
|
||||||
bundleIdentifier: BUNDLE_IDENTIFIER,
|
bundleIdentifier: BUNDLE_IDENTIFIER,
|
||||||
bundleDate: BUNDLE_DATE,
|
bundleDate: BUNDLE_DATE,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const LEFT_TO_RIGHT_EMBEDDING = '\u202A'
|
||||||
|
const POP_DIRECTIONAL_FORMATTING = '\u202C'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Force LTR directionality in a string.
|
||||||
|
* https://www.unicode.org/reports/tr9/#Directional_Formatting_Characters
|
||||||
|
*/
|
||||||
|
export function forceLTR(str: string) {
|
||||||
|
return LEFT_TO_RIGHT_EMBEDDING + str + POP_DIRECTIONAL_FORMATTING
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const NON_BREAKING_SPACE = '\u00A0'
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
// Regex from the go implementation
|
// Regex from the go implementation
|
||||||
// https://github.com/bluesky-social/indigo/blob/main/atproto/syntax/handle.go#L10
|
// https://github.com/bluesky-social/indigo/blob/main/atproto/syntax/handle.go#L10
|
||||||
|
import {forceLTR} from 'lib/strings/bidi'
|
||||||
|
|
||||||
const VALIDATE_REGEX =
|
const VALIDATE_REGEX =
|
||||||
/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
|
/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
|
||||||
|
|
||||||
@@ -22,7 +24,9 @@ export function isInvalidHandle(handle: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeHandle(handle: string, prefix = ''): string {
|
export function sanitizeHandle(handle: string, prefix = ''): string {
|
||||||
return isInvalidHandle(handle) ? '⚠Invalid Handle' : `${prefix}${handle}`
|
return isInvalidHandle(handle)
|
||||||
|
? '⚠Invalid Handle'
|
||||||
|
: forceLTR(`${prefix}${handle}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsValidHandle {
|
export interface IsValidHandle {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import TLDs from 'tlds'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {BSKY_SERVICE} from 'lib/constants'
|
import {BSKY_SERVICE} from 'lib/constants'
|
||||||
import {isInvalidHandle} from 'lib/strings/handles'
|
import {isInvalidHandle} from 'lib/strings/handles'
|
||||||
|
import {startUriToStarterPackUri} from 'lib/strings/starter-pack'
|
||||||
|
|
||||||
export const BSKY_APP_HOST = 'https://bsky.app'
|
export const BSKY_APP_HOST = 'https://bsky.app'
|
||||||
const BSKY_TRUSTED_HOSTS = [
|
const BSKY_TRUSTED_HOSTS = [
|
||||||
@@ -187,6 +188,11 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
|
|||||||
if (isBskyAppUrl(url)) {
|
if (isBskyAppUrl(url)) {
|
||||||
try {
|
try {
|
||||||
const urlp = new URL(url)
|
const urlp = new URL(url)
|
||||||
|
|
||||||
|
if (isBskyStartUrl(url)) {
|
||||||
|
return startUriToStarterPackUri(urlp.pathname)
|
||||||
|
}
|
||||||
|
|
||||||
return urlp.pathname
|
return urlp.pathname
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e)
|
console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e)
|
||||||
|
|||||||
+7
-7
@@ -1,8 +1,8 @@
|
|||||||
import {Platform} from 'react-native'
|
import {Platform} from 'react-native'
|
||||||
import type {Theme} from './ThemeContext'
|
|
||||||
import {colors} from './styles'
|
|
||||||
|
|
||||||
import {darkPalette, lightPalette, dimPalette} from '#/alf/themes'
|
import {darkPalette, dimPalette, lightPalette} from '#/alf/themes'
|
||||||
|
import {colors} from './styles'
|
||||||
|
import type {Theme} from './ThemeContext'
|
||||||
|
|
||||||
export const defaultTheme: Theme = {
|
export const defaultTheme: Theme = {
|
||||||
colorScheme: 'light',
|
colorScheme: 'light',
|
||||||
@@ -308,8 +308,8 @@ export const darkTheme: Theme = {
|
|||||||
textVeryLight: darkPalette.contrast_400,
|
textVeryLight: darkPalette.contrast_400,
|
||||||
replyLine: darkPalette.contrast_200,
|
replyLine: darkPalette.contrast_200,
|
||||||
replyLineDot: darkPalette.contrast_200,
|
replyLineDot: darkPalette.contrast_200,
|
||||||
unreadNotifBg: darkPalette.primary_975,
|
unreadNotifBg: darkPalette.primary_25,
|
||||||
unreadNotifBorder: darkPalette.primary_900,
|
unreadNotifBorder: darkPalette.primary_100,
|
||||||
postCtrl: darkPalette.contrast_500,
|
postCtrl: darkPalette.contrast_500,
|
||||||
brandText: darkPalette.primary_500,
|
brandText: darkPalette.primary_500,
|
||||||
emptyStateIcon: darkPalette.contrast_300,
|
emptyStateIcon: darkPalette.contrast_300,
|
||||||
@@ -357,8 +357,8 @@ export const dimTheme: Theme = {
|
|||||||
textVeryLight: dimPalette.contrast_400,
|
textVeryLight: dimPalette.contrast_400,
|
||||||
replyLine: dimPalette.contrast_200,
|
replyLine: dimPalette.contrast_200,
|
||||||
replyLineDot: dimPalette.contrast_200,
|
replyLineDot: dimPalette.contrast_200,
|
||||||
unreadNotifBg: dimPalette.primary_975,
|
unreadNotifBg: dimPalette.primary_25,
|
||||||
unreadNotifBorder: dimPalette.primary_900,
|
unreadNotifBorder: dimPalette.primary_100,
|
||||||
postCtrl: dimPalette.contrast_500,
|
postCtrl: dimPalette.contrast_500,
|
||||||
brandText: dimPalette.primary_500,
|
brandText: dimPalette.primary_500,
|
||||||
emptyStateIcon: dimPalette.contrast_300,
|
emptyStateIcon: dimPalette.contrast_300,
|
||||||
|
|||||||
+730
-559
File diff suppressed because it is too large
Load Diff
+566
-395
File diff suppressed because it is too large
Load Diff
+566
-395
File diff suppressed because it is too large
Load Diff
+566
-395
File diff suppressed because it is too large
Load Diff
+566
-395
File diff suppressed because it is too large
Load Diff
+576
-429
File diff suppressed because it is too large
Load Diff
+566
-395
File diff suppressed because it is too large
Load Diff
+566
-395
File diff suppressed because it is too large
Load Diff
+974
-803
File diff suppressed because it is too large
Load Diff
+566
-395
File diff suppressed because it is too large
Load Diff
+572
-405
File diff suppressed because it is too large
Load Diff
+637
-498
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+566
-395
File diff suppressed because it is too large
Load Diff
+566
-395
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user