Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89a22e9e89 | |||
| 3c04d9bd84 | |||
| 99d1a881f2 | |||
| 134fcd35d8 | |||
| 7df2327424 | |||
| ae883e2df7 | |||
| db7a744433 | |||
| 75c19b2dc2 | |||
| 88f879ffe9 | |||
| 6f450b4982 | |||
| 8367542138 | |||
| 65d6e561d4 | |||
| c2131bb039 | |||
| ab0da7c892 | |||
| 0a9782ac19 | |||
| 5bfe5aa503 | |||
| dd0d50a6f0 | |||
| 0f993a09c2 | |||
| cb574b7be3 | |||
| a4f0c9c753 | |||
| 4350dbc853 | |||
| e782db33dc | |||
| c1af767fa6 | |||
| a0756c53be | |||
| 3ea8eb6013 | |||
| e7a0055a85 | |||
| ae25cb3391 | |||
| 85fe95c988 | |||
| e1df2b0b16 | |||
| f3a9f874d9 | |||
| 4b71950d99 | |||
| 2174feed44 | |||
| f1031d100b | |||
| 1e3b2d6f42 | |||
| af5262682e | |||
| a864f69849 | |||
| 00fea10782 | |||
| b3092413dd | |||
| 1b02f81cb8 | |||
| fff2c079c2 | |||
| b701e8c68c | |||
| 753a233408 | |||
| 5845e08eee | |||
| b291a1ed8a | |||
| 686d5ebb53 | |||
| 966f6c511f | |||
| 5bf7f3769d | |||
| 74b0318d89 | |||
| 18b423396b | |||
| fb278384c6 | |||
| 6298e6897f | |||
| c3d8beee6d | |||
| 293ac6fab2 | |||
| 7f292abf51 | |||
| f056cb646e | |||
| c78e9e3147 | |||
| 388c157c36 | |||
| b0e130a4d8 | |||
| d2e88cc623 | |||
| 70ffd387e3 | |||
| 576cef88b5 | |||
| c75bb65bef | |||
| c3e77b56ff | |||
| 8ddb28d3c5 | |||
| 43ba0f21f6 | |||
| 4437b9a557 | |||
| 50769b0b6f | |||
| 043e5cea64 | |||
| 35165e3d9b | |||
| 1d827cebe4 | |||
| 783fd351ba | |||
| e7b485f185 | |||
| ac1538baad | |||
| 3914025227 | |||
| 401e92ed42 | |||
| 00240b95b9 | |||
| 4ec999cab7 | |||
| 4291711f1d | |||
| fac1af43b0 | |||
| a03622dd55 | |||
| 2e7398b7c3 | |||
| 8588a2ad51 | |||
| 7a0aa661a7 | |||
| 86ac3d687c | |||
| 11f2415942 | |||
| bfb7f6efef | |||
| efde018b13 | |||
| 8fe5ddfa49 | |||
| cfb8a3160e | |||
| 9bd8393685 | |||
| 4af6bcb11a | |||
| 67ce18a113 | |||
| 3755807b5e | |||
| abb709d209 | |||
| 27d712290a | |||
| 59bafb426f | |||
| 3627a249ff | |||
| 7c1c24ef1b | |||
| f85df425f7 | |||
| 306f162639 | |||
| f021c06468 | |||
| cd9bba4b69 | |||
| a71134f467 | |||
| 8b121af2e4 | |||
| 83e8522e0a | |||
| 2397104ad6 | |||
| b433469ab9 | |||
| 119b4d3e0a | |||
| 74186950b2 | |||
| ea0586cd67 | |||
| 15a00b1864 | |||
| 65b5d31b58 | |||
| fa64d9393b | |||
| 9b9e09d002 | |||
| ce0bf867ff | |||
| 7a47656816 | |||
| 2d0eefebc3 | |||
| 4360087ced | |||
| ca0a327052 | |||
| a3c43a7471 | |||
| f8a59e10dd | |||
| d5503d17de | |||
| ea7afecf28 | |||
| ea37298cdd | |||
| a6b3c97df0 | |||
| 6b5e3181fb |
@@ -0,0 +1,2 @@
|
||||
[*.{kt,kts}]
|
||||
indent_size=2
|
||||
@@ -10,7 +10,7 @@ Get the app itself:
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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.
|
||||
- Stay away from PRs like...
|
||||
- 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.
|
||||
|
||||
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
|
||||
|
||||
@@ -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?
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -132,7 +132,8 @@ appId: xyz.blueskyweb.app
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "e2eGotoFeeds"
|
||||
- tapOn: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "saved-feed-Good Ppl"
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
@@ -168,7 +169,7 @@ appId: xyz.blueskyweb.app
|
||||
id: "profilePager-selector"
|
||||
direction: LEFT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-5"
|
||||
id: "profilePager-selector-6"
|
||||
- tapOn: "Good Ppl"
|
||||
|
||||
- tapOn:
|
||||
|
||||
@@ -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"
|
||||
@@ -221,6 +221,7 @@ module.exports = function (config) {
|
||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||
'./plugins/withAppDelegateReferrer.js',
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-5a2 2 0 0 1-2-2V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 392 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM4 13a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 369 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 4a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14a1 1 0 0 1-1-1Zm-9 9a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2v-5a1 1 0 0 1 1-1Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 384 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-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-1V4Zm11.543 7.293a1 1 0 0 1 1.414 0 1 1 0 0 0 1.414-1.414 3 3 0 1 0 0 4.242 1 1 0 0 0-1.414-1.414 1 1 0 0 1-1.414-1.414Zm-6 0a1 1 0 0 1 1.414 0 1 1 0 0 0 1.414-1.414 3 3 0 1 0 0 4.243 1 1 0 0 0-1.414-1.415 1 1 0 0 1-1.414-1.414Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 440 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-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 1v14h14V5H5Zm10.957 6.293a1 1 0 1 0 0 1.414 1 1 0 0 1 1.414 1.414 3 3 0 1 1 0-4.242 1 1 0 0 1-1.414 1.414Zm-6.331-.22a1 1 0 1 0 .331 1.634 1 1 0 0 1 1.414 1.414 3 3 0 1 1 0-4.242 1 1 0 0 1-1.414 1.414.994.994 0 0 0-.331-.22Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 443 B |
@@ -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="M12.097 2.53a1 1 0 0 1-.041 1.07 6 6 0 0 0 8.345 8.344 1 1 0 0 1 1.563.908c-.434 5.122-4.728 9.144-9.962 9.144-5.522 0-9.998-4.476-9.998-9.998 0-5.234 4.021-9.528 9.144-9.962a1 1 0 0 1 .949.494ZM9.424 4.424a7.998 7.998 0 1 0 10.152 10.152A8 8 0 0 1 9.424 4.424Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 403 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4ZM14 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 247 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6ZM14 6a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 183 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4Zm2 1v14h2V5H6Zm8-1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Zm2 1v14h2V5h-2Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 315 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Zm7 1a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 326 B |
@@ -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="M5 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v16a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H8Zm2 2a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.514 2.143A1 1 0 0 0 5 3v18a1 1 0 0 0 1.514.858l15-9a1 1 0 0 0 0-1.716l-15-9Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 184 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z"/></svg>
|
||||
|
After Width: | Height: | Size: 239 B |
@@ -0,0 +1 @@
|
||||
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.507 2.13a1 1 0 0 1 1.008.013l15 9a1 1 0 0 1 0 1.714l-15 9A1 1 0 0 1 5 21V3a1 1 0 0 1 .507-.87ZM7 4.766v14.468L19.056 12 7 4.766Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -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="M5 5.086C5 2.736 7.578 1.3 9.576 2.534L20.77 9.448c1.899 1.172 1.899 3.932 0 5.104L9.576 21.466C7.578 22.701 5 21.263 5 18.914V5.086Zm3.525-.85A1 1 0 0 0 7 5.085v13.828a1 1 0 0 0 1.525.85l11.194-6.913a1 1 0 0 0 0-1.702L8.525 4.235Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 374 B |
@@ -41,10 +41,10 @@ func run(args []string) {
|
||||
EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ogcard-host",
|
||||
Usage: "scheme, hostname, and port of ogcard service",
|
||||
Name: "ogcard-host",
|
||||
Usage: "scheme, hostname, and port of ogcard service",
|
||||
Required: false,
|
||||
EnvVars: []string{"OGCARD_HOST"},
|
||||
EnvVars: []string{"OGCARD_HOST"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-address",
|
||||
@@ -67,6 +67,13 @@ func run(args []string) {
|
||||
Required: false,
|
||||
EnvVars: []string{"DEBUG"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "basic-auth-password",
|
||||
Usage: "optional password to restrict access to web interface",
|
||||
Required: false,
|
||||
Value: "",
|
||||
EnvVars: []string{"BASIC_AUTH_PASSWORD"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -48,6 +49,7 @@ func serve(cctx *cli.Context) error {
|
||||
appviewHost := cctx.String("appview-host")
|
||||
ogcardHost := cctx.String("ogcard-host")
|
||||
linkHost := cctx.String("link-host")
|
||||
basicAuthPassword := cctx.String("basic-auth-password")
|
||||
|
||||
// Echo
|
||||
e := echo.New()
|
||||
@@ -140,6 +142,18 @@ func serve(cctx *cli.Context) error {
|
||||
},
|
||||
}))
|
||||
|
||||
// optional password gating of entire web interface
|
||||
if basicAuthPassword != "" {
|
||||
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
|
||||
// Be careful to use constant time comparison to prevent timing attacks
|
||||
if subtle.ConstantTimeCompare([]byte(username), []byte("admin")) == 1 &&
|
||||
subtle.ConstantTimeCompare([]byte(password), []byte(basicAuthPassword)) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}))
|
||||
}
|
||||
|
||||
// redirect trailing slash to non-trailing slash.
|
||||
// all of our current endpoints have no trailing slash.
|
||||
e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{
|
||||
@@ -197,6 +211,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/search", server.WebGeneric)
|
||||
e.GET("/feeds", server.WebGeneric)
|
||||
e.GET("/notifications", server.WebGeneric)
|
||||
e.GET("/notifications/settings", server.WebGeneric)
|
||||
e.GET("/lists", server.WebGeneric)
|
||||
e.GET("/moderation", server.WebGeneric)
|
||||
e.GET("/moderation/modlists", server.WebGeneric)
|
||||
@@ -210,6 +225,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/settings/threads", server.WebGeneric)
|
||||
e.GET("/settings/external-embeds", server.WebGeneric)
|
||||
e.GET("/settings/accessibility", server.WebGeneric)
|
||||
e.GET("/settings/appearance", server.WebGeneric)
|
||||
e.GET("/sys/debug", server.WebGeneric)
|
||||
e.GET("/sys/debug-mod", server.WebGeneric)
|
||||
e.GET("/sys/log", server.WebGeneric)
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import 'react-native-gesture-handler' // must be first
|
||||
import {LogBox} from 'react-native'
|
||||
|
||||
import '#/platform/polyfills'
|
||||
import {IS_TEST} from '#/env'
|
||||
|
||||
import {LogBox} from 'react-native'
|
||||
import {registerRootComponent} from 'expo'
|
||||
import {doPolyfill} from '#/lib/api/api-polyfill'
|
||||
|
||||
import App from '#/App'
|
||||
|
||||
doPolyfill()
|
||||
import {IS_TEST} from '#/env'
|
||||
|
||||
if (IS_TEST) {
|
||||
LogBox.ignoreAllLogs() // suppress all logs in tests
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import '#/platform/markBundleStartTime'
|
||||
|
||||
import '#/platform/polyfills'
|
||||
|
||||
import {registerRootComponent} from 'expo'
|
||||
import {doPolyfill} from '#/lib/api/api-polyfill'
|
||||
|
||||
import App from '#/App'
|
||||
|
||||
doPolyfill()
|
||||
registerRootComponent(App)
|
||||
|
||||
@@ -95,3 +95,16 @@ jest.mock('expo-application', () => ({
|
||||
nativeApplicationVersion: '1.0.0',
|
||||
nativeBuildVersion: '1',
|
||||
}))
|
||||
|
||||
jest.mock('expo-modules-core', () => ({
|
||||
requireNativeModule: jest.fn().mockImplementation(moduleName => {
|
||||
if (moduleName === 'ExpoPlatformInfo') {
|
||||
return {
|
||||
getIsReducedMotionEnabled: () => false,
|
||||
}
|
||||
}
|
||||
}),
|
||||
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
|
||||
return () => null
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -156,7 +156,7 @@ class Mocker {
|
||||
}
|
||||
|
||||
async createUser(name: string) {
|
||||
const agent = new BskyAgent({service: this.agent.service})
|
||||
const agent = new BskyAgent({service: this.service})
|
||||
|
||||
const inviteRes = await agent.api.com.atproto.server.createInviteCode(
|
||||
{useCount: 1},
|
||||
@@ -332,7 +332,7 @@ class Mocker {
|
||||
}
|
||||
|
||||
async createInvite(forAccount: string) {
|
||||
const agent = new BskyAgent({service: this.agent.service})
|
||||
const agent = new BskyAgent({service: this.service})
|
||||
await agent.api.com.atproto.server.createInviteCode(
|
||||
{useCount: 1, forAccount},
|
||||
{
|
||||
|
||||
@@ -13,43 +13,43 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if reason == "chat-message" {
|
||||
mutateWithChatMessage(bestAttempt)
|
||||
} else {
|
||||
mutateWithBadge(bestAttempt)
|
||||
}
|
||||
|
||||
|
||||
contentHandler(bestAttempt)
|
||||
}
|
||||
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
// If for some reason the alloted time expires, we don't actually want to display a notification
|
||||
}
|
||||
|
||||
|
||||
func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
|
||||
return content.mutableCopy() as? UNMutableNotificationContent
|
||||
}
|
||||
|
||||
|
||||
func mutateWithBadge(_ content: UNMutableNotificationContent) {
|
||||
var count = prefs?.integer(forKey: "badgeCount") ?? 0
|
||||
count += 1
|
||||
|
||||
|
||||
// Set the new badge number for the notification, then store that value for using later
|
||||
content.badge = NSNumber(value: count)
|
||||
prefs?.setValue(count, forKey: "badgeCount")
|
||||
}
|
||||
|
||||
|
||||
func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
|
||||
if self.prefs?.bool(forKey: "playSoundChat") == true {
|
||||
mutateWithDmSound(content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func mutateWithDefaultSound(_ content: UNMutableNotificationContent) {
|
||||
content.sound = UNNotificationSound.default
|
||||
}
|
||||
|
||||
|
||||
func mutateWithDmSound(_ content: UNMutableNotificationContent) {
|
||||
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
|
||||
}
|
||||
|
||||
@@ -30,12 +30,11 @@ class ShareViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleText(item: NSItemProvider) async -> Void {
|
||||
private func handleText(item: NSItemProvider) async {
|
||||
do {
|
||||
if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String {
|
||||
if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
||||
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
|
||||
{
|
||||
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
|
||||
_ = self.openURL(url)
|
||||
}
|
||||
}
|
||||
@@ -45,12 +44,11 @@ class ShareViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleUrl(item: NSItemProvider) async -> Void {
|
||||
private func handleUrl(item: NSItemProvider) async {
|
||||
do {
|
||||
if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL {
|
||||
if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
|
||||
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
|
||||
{
|
||||
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
|
||||
_ = 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]
|
||||
if items.count < 4 {
|
||||
firstFourItems = items
|
||||
@@ -72,7 +70,7 @@ class ShareViewController: UIViewController {
|
||||
var imageUris = ""
|
||||
|
||||
for (index, item) in firstFourItems.enumerated() {
|
||||
var imageUriInfo: String? = nil
|
||||
var imageUriInfo: String?
|
||||
|
||||
do {
|
||||
if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL {
|
||||
@@ -100,8 +98,7 @@ class ShareViewController: UIViewController {
|
||||
|
||||
if valid,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -119,13 +116,11 @@ class ShareViewController: UIViewController {
|
||||
// extension does.
|
||||
if let dir = FileManager()
|
||||
.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: "group.app.bsky")
|
||||
{
|
||||
forSecurityApplicationGroupIdentifier: "group.app.bsky") {
|
||||
let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"
|
||||
|
||||
if let newUri = URL(string: filePath),
|
||||
let jpegData = image.jpegData(compressionQuality: 1)
|
||||
{
|
||||
let jpegData = image.jpegData(compressionQuality: 1) {
|
||||
try jpegData.write(to: newUri)
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.google.firebase.messaging.RemoteMessage
|
||||
|
||||
class BackgroundNotificationHandler(
|
||||
private val context: Context,
|
||||
private val notifInterface: BackgroundNotificationHandlerInterface
|
||||
private val notifInterface: BackgroundNotificationHandlerInterface,
|
||||
) {
|
||||
fun handleMessage(remoteMessage: RemoteMessage) {
|
||||
if (ExpoBackgroundNotificationHandlerModule.isForegrounded) {
|
||||
|
||||
@@ -8,67 +8,68 @@ class ExpoBackgroundNotificationHandlerModule : Module() {
|
||||
var isForegrounded = false
|
||||
}
|
||||
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ExpoBackgroundNotificationHandler")
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("ExpoBackgroundNotificationHandler")
|
||||
|
||||
OnCreate {
|
||||
NotificationPrefs(appContext.reactContext).initialize()
|
||||
}
|
||||
OnCreate {
|
||||
NotificationPrefs(appContext.reactContext).initialize()
|
||||
}
|
||||
|
||||
OnActivityEntersForeground {
|
||||
isForegrounded = true
|
||||
}
|
||||
OnActivityEntersForeground {
|
||||
isForegrounded = true
|
||||
}
|
||||
|
||||
OnActivityEntersBackground {
|
||||
isForegrounded = false
|
||||
}
|
||||
OnActivityEntersBackground {
|
||||
isForegrounded = false
|
||||
}
|
||||
|
||||
AsyncFunction("getAllPrefsAsync") {
|
||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs()
|
||||
}
|
||||
AsyncFunction("getAllPrefsAsync") {
|
||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs()
|
||||
}
|
||||
|
||||
AsyncFunction("getBoolAsync") { forKey: String ->
|
||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey)
|
||||
}
|
||||
AsyncFunction("getBoolAsync") { forKey: String ->
|
||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey)
|
||||
}
|
||||
|
||||
AsyncFunction("getStringAsync") { forKey: String ->
|
||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey)
|
||||
}
|
||||
AsyncFunction("getStringAsync") { forKey: String ->
|
||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey)
|
||||
}
|
||||
|
||||
AsyncFunction("getStringArrayAsync") { forKey: String ->
|
||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey)
|
||||
}
|
||||
AsyncFunction("getStringArrayAsync") { forKey: String ->
|
||||
return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey)
|
||||
}
|
||||
|
||||
AsyncFunction("setBoolAsync") { forKey: String, value: Boolean ->
|
||||
NotificationPrefs(appContext.reactContext).setBoolean(forKey, value)
|
||||
}
|
||||
AsyncFunction("setBoolAsync") { forKey: String, value: Boolean ->
|
||||
NotificationPrefs(appContext.reactContext).setBoolean(forKey, value)
|
||||
}
|
||||
|
||||
AsyncFunction("setStringAsync") { forKey: String, value: String ->
|
||||
NotificationPrefs(appContext.reactContext).setString(forKey, value)
|
||||
}
|
||||
AsyncFunction("setStringAsync") { forKey: String, value: String ->
|
||||
NotificationPrefs(appContext.reactContext).setString(forKey, value)
|
||||
}
|
||||
|
||||
AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> ->
|
||||
NotificationPrefs(appContext.reactContext).setStringArray(forKey, value)
|
||||
}
|
||||
AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> ->
|
||||
NotificationPrefs(appContext.reactContext).setStringArray(forKey, value)
|
||||
}
|
||||
|
||||
AsyncFunction("addToStringArrayAsync") { forKey: String, string: String ->
|
||||
NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string)
|
||||
}
|
||||
AsyncFunction("addToStringArrayAsync") { forKey: String, string: String ->
|
||||
NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string)
|
||||
}
|
||||
|
||||
AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String ->
|
||||
NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string)
|
||||
}
|
||||
AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String ->
|
||||
NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string)
|
||||
}
|
||||
|
||||
AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> ->
|
||||
NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
|
||||
}
|
||||
AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> ->
|
||||
NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
|
||||
}
|
||||
|
||||
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
|
||||
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
|
||||
}
|
||||
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
|
||||
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
|
||||
}
|
||||
|
||||
AsyncFunction("setBadgeCountAsync") { _: Int ->
|
||||
// This does nothing on Android
|
||||
AsyncFunction("setBadgeCountAsync") { _: Int ->
|
||||
// This does nothing on Android
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,24 @@ package expo.modules.backgroundnotificationhandler
|
||||
|
||||
import android.content.Context
|
||||
|
||||
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,
|
||||
"mutedThreads" to mapOf<String, List<String>>()
|
||||
)
|
||||
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,
|
||||
"mutedThreads" to mapOf<String, List<String>>(),
|
||||
)
|
||||
|
||||
class NotificationPrefs (private val context: Context?) {
|
||||
private val prefs = context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
|
||||
?: throw Error("Context is null")
|
||||
class NotificationPrefs(
|
||||
private val context: Context?,
|
||||
) {
|
||||
private val prefs =
|
||||
context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
|
||||
?: throw Error("Context is null")
|
||||
|
||||
fun initialize() {
|
||||
prefs
|
||||
@@ -41,94 +45,99 @@ class NotificationPrefs (private val context: Context?) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.apply()
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun getAllPrefs(): MutableMap<String, *> {
|
||||
return prefs.all
|
||||
}
|
||||
fun getAllPrefs(): MutableMap<String, *> = prefs.all
|
||||
|
||||
fun getBoolean(key: String): Boolean {
|
||||
return prefs.getBoolean(key, false)
|
||||
}
|
||||
fun getBoolean(key: String): Boolean = prefs.getBoolean(key, false)
|
||||
|
||||
fun getString(key: String): String? {
|
||||
return prefs.getString(key, null)
|
||||
}
|
||||
fun getString(key: String): String? = prefs.getString(key, null)
|
||||
|
||||
fun getStringArray(key: String): Array<String>? {
|
||||
return prefs.getStringSet(key, null)?.toTypedArray()
|
||||
}
|
||||
fun getStringArray(key: String): Array<String>? = prefs.getStringSet(key, null)?.toTypedArray()
|
||||
|
||||
fun setBoolean(key: String, value: Boolean) {
|
||||
fun setBoolean(
|
||||
key: String,
|
||||
value: Boolean,
|
||||
) {
|
||||
prefs
|
||||
.edit()
|
||||
.apply {
|
||||
putBoolean(key, value)
|
||||
}
|
||||
.apply()
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun setString(key: String, value: String) {
|
||||
fun setString(
|
||||
key: String,
|
||||
value: String,
|
||||
) {
|
||||
prefs
|
||||
.edit()
|
||||
.apply {
|
||||
putString(key, value)
|
||||
}
|
||||
.apply()
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun setStringArray(key: String, value: Array<String>) {
|
||||
fun setStringArray(
|
||||
key: String,
|
||||
value: Array<String>,
|
||||
) {
|
||||
prefs
|
||||
.edit()
|
||||
.apply {
|
||||
putStringSet(key, value.toSet())
|
||||
}
|
||||
.apply()
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun addToStringArray(key: String, string: String) {
|
||||
fun addToStringArray(
|
||||
key: String,
|
||||
string: String,
|
||||
) {
|
||||
prefs
|
||||
.edit()
|
||||
.apply {
|
||||
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
||||
set.add(string)
|
||||
putStringSet(key, set)
|
||||
}
|
||||
.apply()
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun removeFromStringArray(key: String, string: String) {
|
||||
fun removeFromStringArray(
|
||||
key: String,
|
||||
string: String,
|
||||
) {
|
||||
prefs
|
||||
.edit()
|
||||
.apply {
|
||||
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
||||
set.remove(string)
|
||||
putStringSet(key, set)
|
||||
}
|
||||
.apply()
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun addManyToStringArray(key: String, strings: Array<String>) {
|
||||
fun addManyToStringArray(
|
||||
key: String,
|
||||
strings: Array<String>,
|
||||
) {
|
||||
prefs
|
||||
.edit()
|
||||
.apply {
|
||||
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
||||
set.addAll(strings.toSet())
|
||||
putStringSet(key, set)
|
||||
}
|
||||
.apply()
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun removeManyFromStringArray(key: String, strings: Array<String>) {
|
||||
fun removeManyFromStringArray(
|
||||
key: String,
|
||||
strings: Array<String>,
|
||||
) {
|
||||
prefs
|
||||
.edit()
|
||||
.apply {
|
||||
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
||||
set.removeAll(strings.toSet())
|
||||
putStringSet(key, set)
|
||||
}
|
||||
.apply()
|
||||
}.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@ import ExpoModulesCore
|
||||
|
||||
let APP_GROUP = "group.app.bsky"
|
||||
|
||||
let DEFAULTS: [String:Any] = [
|
||||
"playSoundChat" : true,
|
||||
let DEFAULTS: [String: Any] = [
|
||||
"playSoundChat": true,
|
||||
"playSoundFollow": false,
|
||||
"playSoundLike": false,
|
||||
"playSoundMention": false,
|
||||
"playSoundQuote": false,
|
||||
"playSoundReply": false,
|
||||
"playSoundRepost": false,
|
||||
"mutedThreads": [:] as! [String:[String]],
|
||||
"badgeCount": 0,
|
||||
"mutedThreads": [:] as! [String: [String]],
|
||||
"badgeCount": 0
|
||||
]
|
||||
|
||||
/*
|
||||
@@ -23,10 +23,10 @@ let DEFAULTS: [String:Any] = [
|
||||
*/
|
||||
public class ExpoBackgroundNotificationHandlerModule: Module {
|
||||
let userDefaults = UserDefaults(suiteName: APP_GROUP)
|
||||
|
||||
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoBackgroundNotificationHandler")
|
||||
|
||||
|
||||
OnCreate {
|
||||
DEFAULTS.forEach { p in
|
||||
if userDefaults?.value(forKey: p.key) == nil {
|
||||
@@ -34,57 +34,56 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AsyncFunction("getAllPrefsAsync") { () -> [String:Any]? in
|
||||
|
||||
AsyncFunction("getAllPrefsAsync") { () -> [String: Any]? in
|
||||
var keys: [String] = []
|
||||
DEFAULTS.forEach { p in
|
||||
keys.append(p.key)
|
||||
}
|
||||
return userDefaults?.dictionaryWithValues(forKeys: keys)
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("getBoolAsync") { (forKey: String) -> Bool in
|
||||
if let pref = userDefaults?.bool(forKey: forKey) {
|
||||
return pref
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("getStringAsync") { (forKey: String) -> String? in
|
||||
if let pref = userDefaults?.string(forKey: forKey) {
|
||||
return pref
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("getStringArrayAsync") { (forKey: String) -> [String]? in
|
||||
if let pref = userDefaults?.stringArray(forKey: forKey) {
|
||||
return pref
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) -> Void in
|
||||
|
||||
AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) in
|
||||
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)
|
||||
}
|
||||
|
||||
AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) -> Void in
|
||||
|
||||
AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) in
|
||||
userDefaults?.setValue(value, forKey: forKey)
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in
|
||||
if var curr = userDefaults?.stringArray(forKey: forKey),
|
||||
!curr.contains(string)
|
||||
{
|
||||
!curr.contains(string) {
|
||||
curr.append(string)
|
||||
userDefaults?.setValue(curr, forKey: forKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("removeFromStringArrayAsync") { (forKey: String, string: String) in
|
||||
if var curr = userDefaults?.stringArray(forKey: forKey) {
|
||||
curr.removeAll { s in
|
||||
@@ -93,7 +92,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
|
||||
userDefaults?.setValue(curr, forKey: forKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in
|
||||
if var curr = userDefaults?.stringArray(forKey: forKey) {
|
||||
strings.forEach { s in
|
||||
@@ -104,7 +103,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
|
||||
userDefaults?.setValue(curr, forKey: forKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in
|
||||
if var curr = userDefaults?.stringArray(forKey: forKey) {
|
||||
strings.forEach { s in
|
||||
@@ -113,7 +112,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
|
||||
userDefaults?.setValue(curr, forKey: forKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("setBadgeCountAsync") { (count: Int) in
|
||||
userDefaults?.setValue(count, forKey: "badgeCount")
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import android.graphics.Canvas
|
||||
import android.graphics.drawable.Animatable
|
||||
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) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
@@ -34,4 +37,4 @@ class AppCompatImageViewExtended(context: Context, private val parent: GifView):
|
||||
drawable.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,49 +6,50 @@ import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class ExpoBlueskyGifViewModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ExpoBlueskyGifView")
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("ExpoBlueskyGifView")
|
||||
|
||||
AsyncFunction("prefetchAsync") { sources: List<String> ->
|
||||
val activity = appContext.currentActivity ?: return@AsyncFunction
|
||||
val glide = Glide.with(activity)
|
||||
AsyncFunction("prefetchAsync") { sources: List<String> ->
|
||||
val activity = appContext.currentActivity ?: return@AsyncFunction
|
||||
val glide = Glide.with(activity)
|
||||
|
||||
sources.forEach { source ->
|
||||
glide
|
||||
.download(source)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.submit()
|
||||
sources.forEach { source ->
|
||||
glide
|
||||
.download(source)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package expo.modules.blueskygifview
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
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.views.ExpoView
|
||||
|
||||
class GifView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
||||
class GifView(
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
) : ExpoView(context, appContext) {
|
||||
// Events
|
||||
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 {
|
||||
this.setBackgroundColor(Color.TRANSPARENT)
|
||||
@@ -70,80 +71,82 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
//</editor-fold>
|
||||
// </editor-fold>
|
||||
|
||||
//<editor-fold desc="Loading">
|
||||
// <editor-fold desc="Loading">
|
||||
|
||||
private fun load() {
|
||||
if (placeholderSource == null || source == null) {
|
||||
return
|
||||
}
|
||||
|
||||
this.webpRequest = glide.load(source)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.skipMemoryCache(false)
|
||||
.listener(object: RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: com.bumptech.glide.load.DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
if (placeholderRequest != null) {
|
||||
glide.clear(placeholderRequest)
|
||||
}
|
||||
return false
|
||||
}
|
||||
this.webpRequest =
|
||||
glide
|
||||
.load(source)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.skipMemoryCache(false)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: com.bumptech.glide.load.DataSource?,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
if (placeholderRequest != null) {
|
||||
glide.clear(placeholderRequest)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
.into(this.imageView)
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean = true
|
||||
},
|
||||
).into(this.imageView)
|
||||
|
||||
if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
|
||||
this.placeholderRequest = glide.load(placeholderSource)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
// Let's not bloat the memory cache with placeholders
|
||||
.skipMemoryCache(true)
|
||||
.listener(object: RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: com.bumptech.glide.load.DataSource?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
// Incase this request finishes after the webp, let's just not set
|
||||
// the drawable. This shouldn't happen because the request should get cancelled
|
||||
if (imageView.drawable == null) {
|
||||
imageView.setImageDrawable(resource)
|
||||
}
|
||||
return true
|
||||
}
|
||||
this.placeholderRequest =
|
||||
glide
|
||||
.load(placeholderSource)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
// Let's not bloat the memory cache with placeholders
|
||||
.skipMemoryCache(true)
|
||||
.listener(
|
||||
object : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: com.bumptech.glide.load.DataSource?,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
// Incase this request finishes after the webp, let's just not set
|
||||
// the drawable. This shouldn't happen because the request should get cancelled
|
||||
if (imageView.drawable == null) {
|
||||
imageView.setImageDrawable(resource)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
.submit()
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean = true
|
||||
},
|
||||
).submit()
|
||||
}
|
||||
}
|
||||
|
||||
//</editor-fold>
|
||||
// </editor-fold>
|
||||
|
||||
//<editor-fold desc="Controls">
|
||||
// <editor-fold desc="Controls">
|
||||
|
||||
fun 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() {
|
||||
onPlayerStateChange(mapOf(
|
||||
"isPlaying" to this.isPlaying,
|
||||
"isLoaded" to this.isLoaded,
|
||||
))
|
||||
onPlayerStateChange(
|
||||
mapOf(
|
||||
"isPlaying" to this.isPlaying,
|
||||
"isLoaded" to this.isLoaded,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
//</editor-fold>
|
||||
// </editor-fold>
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import SDWebImageWebPCoder
|
||||
public class ExpoBlueskyGifViewModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoBlueskyGifView")
|
||||
|
||||
|
||||
OnCreate {
|
||||
SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared)
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("prefetchAsync") { (sources: [URL]) in
|
||||
SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil)
|
||||
}
|
||||
@@ -18,27 +18,27 @@ public class ExpoBlueskyGifViewModule: Module {
|
||||
Events(
|
||||
"onPlayerStateChange"
|
||||
)
|
||||
|
||||
|
||||
Prop("source") { (view: GifView, prop: String) in
|
||||
view.source = prop
|
||||
}
|
||||
|
||||
|
||||
Prop("placeholderSource") { (view: GifView, prop: String) in
|
||||
view.placeholderSource = prop
|
||||
}
|
||||
|
||||
|
||||
Prop("autoplay") { (view: GifView, prop: Bool) in
|
||||
view.autoplay = prop
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("toggleAsync") { (view: GifView) in
|
||||
view.toggle()
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("playAsync") { (view: GifView) in
|
||||
view.play()
|
||||
}
|
||||
|
||||
|
||||
AsyncFunction("pauseAsync") { (view: GifView) in
|
||||
view.pause()
|
||||
}
|
||||
|
||||
@@ -16,14 +16,14 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
|
||||
)
|
||||
private var isPlaying = true
|
||||
private var isLoaded = false
|
||||
|
||||
|
||||
// Requests
|
||||
private var webpOperation: SDWebImageCombinedOperation?
|
||||
private var placeholderOperation: SDWebImageCombinedOperation?
|
||||
|
||||
// Props
|
||||
var source: String? = nil
|
||||
var placeholderSource: String? = nil
|
||||
var source: String?
|
||||
var placeholderSource: String?
|
||||
var autoplay = true {
|
||||
didSet {
|
||||
if !autoplay {
|
||||
@@ -78,8 +78,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
|
||||
// See:
|
||||
// https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently
|
||||
if !SDImageCache.shared.diskImageDataExists(withKey: source),
|
||||
let url = URL(string: placeholderSource)
|
||||
{
|
||||
let url = URL(string: placeholderSource) {
|
||||
self.placeholderOperation = imageManager.loadImage(
|
||||
with: url,
|
||||
options: [.retryFailed],
|
||||
@@ -132,8 +131,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
|
||||
if let placeholderSource = self.placeholderSource,
|
||||
imageUrl?.absoluteString == placeholderSource,
|
||||
self.imageView.image == nil,
|
||||
let image = image
|
||||
{
|
||||
let image = image {
|
||||
self.setImage(image)
|
||||
return
|
||||
}
|
||||
@@ -142,8 +140,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
|
||||
imageUrl?.absoluteString == source,
|
||||
// UIImage perf suckssss if the image is animated
|
||||
let data = data,
|
||||
let animatedImage = SDAnimatedImage(data: data)
|
||||
{
|
||||
let animatedImage = SDAnimatedImage(data: data) {
|
||||
self.placeholderOperation?.cancel()
|
||||
self.isPlaying = self.autoplay
|
||||
self.isLoaded = true
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package expo.modules.blueskyswissarmy.platforminfo
|
||||
|
||||
import android.provider.Settings
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class ExpoPlatformInfoModule : Module() {
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("ExpoPlatformInfo")
|
||||
|
||||
// See https://github.com/software-mansion/react-native-reanimated/blob/7df5fd57d608fe25724608835461cd925ff5151d/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeProxy/NativeProxyCommon.java#L242
|
||||
Function("getIsReducedMotionEnabled") {
|
||||
val resolver = appContext.reactContext?.contentResolver ?: return@Function false
|
||||
val scale = Settings.Global.getString(resolver, Settings.Global.TRANSITION_ANIMATION_SCALE) ?: return@Function false
|
||||
|
||||
try {
|
||||
return@Function scale.toFloat() == 0f
|
||||
} catch (_: Error) {
|
||||
return@Function false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,103 @@
|
||||
package expo.modules.blueskyswissarmy.referrer
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.android.installreferrer.api.InstallReferrerClient
|
||||
import com.android.installreferrer.api.InstallReferrerStateListener
|
||||
import expo.modules.kotlin.Promise
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
import expo.modules.kotlin.Promise
|
||||
|
||||
class ExpoBlueskyReferrerModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ExpoBlueskyReferrer")
|
||||
private var intent: Intent? = null
|
||||
private var activityReferrer: Uri? = 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.")
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("ExpoBlueskyReferrer")
|
||||
|
||||
val response = referrerClient.installReferrer
|
||||
Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}")
|
||||
OnNewIntent {
|
||||
intent = it
|
||||
activityReferrer = appContext.currentActivity?.referrer
|
||||
}
|
||||
|
||||
promise.resolve(
|
||||
mapOf(
|
||||
"installReferrer" to response.installReferrer,
|
||||
"clickTimestamp" to response.referrerClickTimestampSeconds,
|
||||
"installTimestamp" to response.installBeginTimestampSeconds
|
||||
)
|
||||
)
|
||||
Function("getReferrerInfo") {
|
||||
val intentReferrer =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent?.getParcelableExtra(Intent.EXTRA_REFERRER, Uri::class.java)
|
||||
} 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")
|
||||
)
|
||||
intent?.getParcelableExtra(Intent.EXTRA_REFERRER)
|
||||
}
|
||||
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@Function res
|
||||
}
|
||||
|
||||
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")
|
||||
)
|
||||
// In all other cases, we'll just record the app that sent the intent.
|
||||
if (activityReferrer != null) {
|
||||
// referrer could become null here. `.toString()` though can be called on null
|
||||
val res =
|
||||
mapOf(
|
||||
"referrer" to activityReferrer.toString(),
|
||||
"hostname" to (activityReferrer?.host ?: ""),
|
||||
)
|
||||
activityReferrer = null
|
||||
return@Function res
|
||||
}
|
||||
})
|
||||
|
||||
return@Function 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"),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package expo.modules.blueskyswissarmy.visibilityview
|
||||
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class ExpoBlueskyVisibilityViewModule : Module() {
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("ExpoBlueskyVisibilityView")
|
||||
|
||||
AsyncFunction("updateActiveViewAsync") {
|
||||
VisibilityViewManager.updateActiveView()
|
||||
}
|
||||
|
||||
View(VisibilityView::class) {
|
||||
Events(arrayOf("onChangeStatus"))
|
||||
|
||||
Prop("enabled") { view: VisibilityView, prop: Boolean ->
|
||||
view.isViewEnabled = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package expo.modules.blueskyswissarmy.visibilityview
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
class VisibilityView(
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
) : ExpoView(context, appContext) {
|
||||
var isViewEnabled: Boolean = false
|
||||
|
||||
private val onChangeStatus by EventDispatcher()
|
||||
|
||||
private var isCurrentlyActive = false
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
VisibilityViewManager.addView(this)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
VisibilityViewManager.removeView(this)
|
||||
}
|
||||
|
||||
fun setIsCurrentlyActive(isActive: Boolean) {
|
||||
if (isCurrentlyActive == isActive) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isCurrentlyActive = isActive
|
||||
this.onChangeStatus(
|
||||
mapOf(
|
||||
"isActive" to isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getPositionOnScreen(): Rect? {
|
||||
if (!this.isShown) {
|
||||
return null
|
||||
}
|
||||
|
||||
val screenPosition = intArrayOf(0, 0)
|
||||
this.getLocationInWindow(screenPosition)
|
||||
return Rect(
|
||||
screenPosition[0],
|
||||
screenPosition[1],
|
||||
screenPosition[0] + this.width,
|
||||
screenPosition[1] + this.height,
|
||||
)
|
||||
}
|
||||
|
||||
fun isViewableEnough(): Boolean {
|
||||
val positionOnScreen = this.getPositionOnScreen() ?: return false
|
||||
val visibleArea = positionOnScreen.width() * positionOnScreen.height()
|
||||
val totalArea = this.width * this.height
|
||||
return visibleArea >= 0.5 * totalArea
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package expo.modules.blueskyswissarmy.visibilityview
|
||||
|
||||
import android.graphics.Rect
|
||||
|
||||
class VisibilityViewManager {
|
||||
companion object {
|
||||
private val views = HashMap<Int, VisibilityView>()
|
||||
private var currentlyActiveView: VisibilityView? = null
|
||||
private var prevCount = 0
|
||||
|
||||
fun addView(view: VisibilityView) {
|
||||
this.views[view.id] = view
|
||||
|
||||
if (this.prevCount == 0) {
|
||||
this.updateActiveView()
|
||||
}
|
||||
this.prevCount = this.views.count()
|
||||
}
|
||||
|
||||
fun removeView(view: VisibilityView) {
|
||||
this.views.remove(view.id)
|
||||
this.prevCount = this.views.count()
|
||||
}
|
||||
|
||||
fun updateActiveView() {
|
||||
var activeView: VisibilityView? = null
|
||||
val count = this.views.count()
|
||||
|
||||
if (count == 1) {
|
||||
val view = this.views.values.first()
|
||||
if (view.isViewableEnough()) {
|
||||
activeView = view
|
||||
}
|
||||
} else if (count > 1) {
|
||||
val views = this.views.values
|
||||
var mostVisibleView: VisibilityView? = null
|
||||
var mostVisiblePosition: Rect? = null
|
||||
|
||||
views.forEach { view ->
|
||||
if (!view.isViewableEnough()) {
|
||||
return
|
||||
}
|
||||
|
||||
val position = view.getPositionOnScreen() ?: return@forEach
|
||||
val topY = position.centerY() - (position.height() / 2)
|
||||
|
||||
if (topY >= 150) {
|
||||
if (mostVisiblePosition == null) {
|
||||
mostVisiblePosition = position
|
||||
}
|
||||
|
||||
if (position.centerY() <= mostVisiblePosition!!.centerY()) {
|
||||
mostVisibleView = view
|
||||
mostVisiblePosition = position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeView = mostVisibleView
|
||||
}
|
||||
|
||||
if (activeView == this.currentlyActiveView) {
|
||||
return
|
||||
}
|
||||
|
||||
this.clearActiveView()
|
||||
if (activeView != null) {
|
||||
this.setActiveView(activeView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearActiveView() {
|
||||
this.currentlyActiveView?.setIsCurrentlyActive(false)
|
||||
this.currentlyActiveView = null
|
||||
}
|
||||
|
||||
private fun setActiveView(view: VisibilityView) {
|
||||
view.setIsCurrentlyActive(true)
|
||||
this.currentlyActiveView = view
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
{
|
||||
"platforms": ["ios", "tvos", "android", "web"],
|
||||
"ios": {
|
||||
"modules": ["ExpoBlueskyDevicePrefsModule", "ExpoBlueskyReferrerModule"]
|
||||
"modules": [
|
||||
"ExpoBlueskySharedPrefsModule",
|
||||
"ExpoBlueskyReferrerModule",
|
||||
"ExpoBlueskyVisibilityViewModule",
|
||||
"ExpoPlatformInfoModule"
|
||||
]
|
||||
},
|
||||
"android": {
|
||||
"modules": [
|
||||
"expo.modules.blueskyswissarmy.deviceprefs.ExpoBlueskyDevicePrefsModule",
|
||||
"expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule"
|
||||
"expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule",
|
||||
"expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule",
|
||||
"expo.modules.blueskyswissarmy.visibilityview.ExpoBlueskyVisibilityViewModule",
|
||||
"expo.modules.blueskyswissarmy.platforminfo.ExpoPlatformInfoModule"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import * as DevicePrefs from './src/DevicePrefs'
|
||||
import * as PlatformInfo from './src/PlatformInfo'
|
||||
import {AudioCategory} from './src/PlatformInfo/types'
|
||||
import * as Referrer from './src/Referrer'
|
||||
import * as SharedPrefs from './src/SharedPrefs'
|
||||
import VisibilityView from './src/VisibilityView'
|
||||
|
||||
export {DevicePrefs, Referrer}
|
||||
export {AudioCategory, PlatformInfo, Referrer, SharedPrefs, VisibilityView}
|
||||
|
||||
@@ -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,29 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class ExpoPlatformInfoModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoPlatformInfo")
|
||||
|
||||
Function("getIsReducedMotionEnabled") {
|
||||
return UIAccessibility.isReduceMotionEnabled
|
||||
}
|
||||
|
||||
Function("setAudioCategory") { (audioCategoryString: String) in
|
||||
let audioCategory = AVAudioSession.Category(rawValue: audioCategoryString)
|
||||
try? AVAudioSession.sharedInstance().setCategory(audioCategory)
|
||||
}
|
||||
|
||||
Function("setAudioActive") { (active: Bool) in
|
||||
if active {
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
} else {
|
||||
try? AVAudioSession
|
||||
.sharedInstance()
|
||||
.setActive(
|
||||
false,
|
||||
options: [.notifyOthersOnDeactivation]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class ExpoBlueskyVisibilityViewModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoBlueskyVisibilityView")
|
||||
|
||||
AsyncFunction("updateActiveViewAsync") {
|
||||
VisibilityViewManager.shared.updateActiveView()
|
||||
}
|
||||
|
||||
View(VisibilityView.self) {
|
||||
Events([
|
||||
"onChangeStatus"
|
||||
])
|
||||
|
||||
Prop("enabled") { (view: VisibilityView, prop: Bool) in
|
||||
view.enabled = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import Foundation
|
||||
|
||||
class VisibilityViewManager {
|
||||
static let shared = VisibilityViewManager()
|
||||
|
||||
private let views = NSHashTable<VisibilityView>(options: .weakMemory)
|
||||
private var currentlyActiveView: VisibilityView?
|
||||
private var screenHeight: CGFloat = UIScreen.main.bounds.height
|
||||
private var prevCount = 0
|
||||
|
||||
func addView(_ view: VisibilityView) {
|
||||
self.views.add(view)
|
||||
|
||||
if self.prevCount == 0 {
|
||||
self.updateActiveView()
|
||||
}
|
||||
self.prevCount = self.views.count
|
||||
}
|
||||
|
||||
func removeView(_ view: VisibilityView) {
|
||||
self.views.remove(view)
|
||||
self.prevCount = self.views.count
|
||||
}
|
||||
|
||||
func updateActiveView() {
|
||||
DispatchQueue.main.async {
|
||||
var activeView: VisibilityView?
|
||||
|
||||
if self.views.count == 1 {
|
||||
let view = self.views.allObjects[0]
|
||||
if view.isViewableEnough() {
|
||||
activeView = view
|
||||
}
|
||||
} else if self.views.count > 1 {
|
||||
let views = self.views.allObjects
|
||||
var mostVisibleView: VisibilityView?
|
||||
var mostVisiblePosition: CGRect?
|
||||
|
||||
views.forEach { view in
|
||||
if !view.isViewableEnough() {
|
||||
return
|
||||
}
|
||||
|
||||
guard let position = view.getPositionOnScreen() else {
|
||||
return
|
||||
}
|
||||
|
||||
if position.minY >= 150 {
|
||||
if mostVisiblePosition == nil {
|
||||
mostVisiblePosition = position
|
||||
}
|
||||
|
||||
if let unwrapped = mostVisiblePosition,
|
||||
position.minY <= unwrapped.minY {
|
||||
mostVisibleView = view
|
||||
mostVisiblePosition = position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeView = mostVisibleView
|
||||
}
|
||||
|
||||
if activeView == self.currentlyActiveView {
|
||||
return
|
||||
}
|
||||
|
||||
self.clearActiveView()
|
||||
if let view = activeView {
|
||||
self.setActiveView(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func clearActiveView() {
|
||||
if let currentlyActiveView = self.currentlyActiveView {
|
||||
currentlyActiveView.setIsCurrentlyActive(isActive: false)
|
||||
self.currentlyActiveView = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func setActiveView(_ view: VisibilityView) {
|
||||
view.setIsCurrentlyActive(isActive: true)
|
||||
self.currentlyActiveView = view
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
class VisibilityView: ExpoView {
|
||||
var enabled = false {
|
||||
didSet {
|
||||
if enabled {
|
||||
VisibilityViewManager.shared.removeView(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let onChangeStatus = EventDispatcher()
|
||||
private var isCurrentlyActiveView = false
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
}
|
||||
|
||||
public override func willMove(toWindow newWindow: UIWindow?) {
|
||||
super.willMove(toWindow: newWindow)
|
||||
|
||||
if !self.enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if newWindow == nil {
|
||||
VisibilityViewManager.shared.removeView(self)
|
||||
} else {
|
||||
VisibilityViewManager.shared.addView(self)
|
||||
}
|
||||
}
|
||||
|
||||
func setIsCurrentlyActive(isActive: Bool) {
|
||||
if isCurrentlyActiveView == isActive {
|
||||
return
|
||||
}
|
||||
self.isCurrentlyActiveView = isActive
|
||||
self.onChangeStatus([
|
||||
"isActive": isActive
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// 🚨 DANGER 🚨
|
||||
// These functions need to be called from the main thread. Xcode will warn you if you call one of them
|
||||
// off the main thread, so pay attention!
|
||||
extension UIView {
|
||||
func getPositionOnScreen() -> CGRect? {
|
||||
if let window = self.window {
|
||||
return self.convert(self.bounds, to: window)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isViewableEnough() -> Bool {
|
||||
guard let window = self.window else {
|
||||
return false
|
||||
}
|
||||
|
||||
let viewFrameOnScreen = self.convert(self.bounds, to: window)
|
||||
let screenBounds = window.bounds
|
||||
let intersection = viewFrameOnScreen.intersection(screenBounds)
|
||||
|
||||
let viewHeight = viewFrameOnScreen.height
|
||||
let intersectionHeight = intersection.height
|
||||
|
||||
return intersectionHeight >= 0.5 * viewHeight
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Platform} from 'react-native'
|
||||
import {requireNativeModule} from 'expo-modules-core'
|
||||
|
||||
import {AudioCategory} from './types'
|
||||
|
||||
const NativeModule = requireNativeModule('ExpoPlatformInfo')
|
||||
|
||||
export function getIsReducedMotionEnabled(): boolean {
|
||||
return NativeModule.getIsReducedMotionEnabled()
|
||||
}
|
||||
|
||||
export function setAudioActive(active: boolean): void {
|
||||
if (Platform.OS !== 'ios') return
|
||||
NativeModule.setAudioActive(active)
|
||||
}
|
||||
|
||||
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||
if (Platform.OS !== 'ios') return
|
||||
NativeModule.setAudioCategory(audioCategory)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {AudioCategory} from './types'
|
||||
|
||||
export function getIsReducedMotionEnabled(): boolean {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the app's audio should mix with other apps' audio. Will also resume background music playback when `false`
|
||||
* if it was previously playing.
|
||||
* @param mixWithOthers
|
||||
* @see https://developer.apple.com/documentation/avfaudio/avaudiosession/setactiveoptions/1616603-notifyothersondeactivation
|
||||
*/
|
||||
export function setAudioActive(active: boolean): void {
|
||||
throw new NotImplementedError({active})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the audio category for the app.
|
||||
* @param audioCategory
|
||||
* @platform ios
|
||||
*/
|
||||
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||
throw new NotImplementedError({audioCategory})
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {AudioCategory} from './types'
|
||||
|
||||
export function getIsReducedMotionEnabled(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false
|
||||
}
|
||||
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
}
|
||||
|
||||
export function setAudioActive(active: boolean): void {
|
||||
throw new NotImplementedError({active})
|
||||
}
|
||||
|
||||
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||
throw new NotImplementedError({audioCategory})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Sets the audio session category on iOS. In general, we should only need to use this for the `playback` and `ambient`
|
||||
* categories. This enum however includes other categories that are available in the native API for clarity and
|
||||
* potential future use.
|
||||
* @see https://developer.apple.com/documentation/avfoundation/avaudiosession/category
|
||||
* @platform ios
|
||||
*/
|
||||
export enum AudioCategory {
|
||||
Ambient = 'AVAudioSessionCategoryAmbient',
|
||||
Playback = 'AVAudioSessionCategoryPlayback',
|
||||
_SoloAmbient = 'AVAudioSessionCategorySoloAmbient',
|
||||
_Record = 'AVAudioSessionCategoryRecord',
|
||||
_PlayAndRecord = 'AVAudioSessionCategoryPlayAndRecord',
|
||||
_MultiRoute = 'AVAudioSessionCategoryMultiRoute',
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import {requireNativeModule} from 'expo'
|
||||
|
||||
import {GooglePlayReferrerInfo} from './types'
|
||||
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||
|
||||
export const NativeModule = requireNativeModule('ExpoBlueskyReferrer')
|
||||
|
||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo | null> {
|
||||
return NativeModule.getGooglePlayReferrerInfoAsync()
|
||||
}
|
||||
|
||||
export function getReferrerInfo(): Promise<ReferrerInfo | null> {
|
||||
return NativeModule.getReferrerInfo()
|
||||
}
|
||||
|
||||
@@ -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 getReferrerInfo(): 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: referrer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {GooglePlayReferrerInfo} from './types'
|
||||
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||
|
||||
// @ts-ignore throws
|
||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
export function getReferrerInfo(): ReferrerInfo | null {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import {Platform} from 'react-native'
|
||||
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
|
||||
|
||||
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
export function getReferrerInfo(): 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 =
|
||||
| {
|
||||
installReferrer?: string
|
||||
clickTimestamp?: number
|
||||
installTimestamp?: number
|
||||
}
|
||||
| undefined
|
||||
export type GooglePlayReferrerInfo = {
|
||||
installReferrer?: string
|
||||
clickTimestamp?: number
|
||||
installTimestamp?: number
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, ViewStyle} from 'react-native'
|
||||
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
|
||||
|
||||
import {VisibilityViewProps} from './types'
|
||||
const NativeView: React.ComponentType<{
|
||||
onChangeStatus: (e: {nativeEvent: {isActive: boolean}}) => void
|
||||
children: React.ReactNode
|
||||
enabled: Boolean
|
||||
style: StyleProp<ViewStyle>
|
||||
}> = requireNativeViewManager('ExpoBlueskyVisibilityView')
|
||||
|
||||
const NativeModule = requireNativeModule('ExpoBlueskyVisibilityView')
|
||||
|
||||
export async function updateActiveViewAsync() {
|
||||
await NativeModule.updateActiveViewAsync()
|
||||
}
|
||||
|
||||
export default function VisibilityView({
|
||||
children,
|
||||
onChangeStatus: onChangeStatusOuter,
|
||||
enabled,
|
||||
}: VisibilityViewProps) {
|
||||
const onChangeStatus = React.useCallback(
|
||||
(e: {nativeEvent: {isActive: boolean}}) => {
|
||||
onChangeStatusOuter(e.nativeEvent.isActive)
|
||||
},
|
||||
[onChangeStatusOuter],
|
||||
)
|
||||
|
||||
return (
|
||||
<NativeView
|
||||
onChangeStatus={onChangeStatus}
|
||||
enabled={enabled}
|
||||
style={{flex: 1}}>
|
||||
{children}
|
||||
</NativeView>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {VisibilityViewProps} from './types'
|
||||
|
||||
export async function updateActiveViewAsync() {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
export default function VisibilityView({children}: VisibilityViewProps) {
|
||||
return children
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from 'react'
|
||||
export interface VisibilityViewProps {
|
||||
children: React.ReactNode
|
||||
onChangeStatus: (isActive: boolean) => void
|
||||
enabled: boolean
|
||||
}
|
||||
@@ -13,16 +13,17 @@ import java.io.FileOutputStream
|
||||
import java.net.URLEncoder
|
||||
|
||||
class ExpoReceiveAndroidIntentsModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("ExpoReceiveAndroidIntents")
|
||||
override fun definition() =
|
||||
ModuleDefinition {
|
||||
Name("ExpoReceiveAndroidIntents")
|
||||
|
||||
OnNewIntent {
|
||||
handleIntent(it)
|
||||
OnNewIntent {
|
||||
handleIntent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.type == "text/plain") {
|
||||
@@ -40,7 +41,7 @@ class ExpoReceiveAndroidIntentsModule : Module() {
|
||||
private fun handleTextIntent(intent: Intent) {
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
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)
|
||||
appContext.currentActivity?.startActivity(newIntent)
|
||||
}
|
||||
@@ -48,11 +49,12 @@ class ExpoReceiveAndroidIntentsModule : Module() {
|
||||
}
|
||||
|
||||
private fun handleImageIntent(intent: Intent) {
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
}
|
||||
val uri =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||
}
|
||||
if (uri == null) return
|
||||
|
||||
handleImageIntents(listOf(uri))
|
||||
@@ -76,16 +78,16 @@ class ExpoReceiveAndroidIntentsModule : Module() {
|
||||
uris.forEachIndexed { index, uri ->
|
||||
val info = getImageInfo(uri)
|
||||
val params = buildUriData(info)
|
||||
allParams = "${allParams}${params}"
|
||||
allParams = "${allParams}$params"
|
||||
|
||||
if (index < uris.count() - 1) {
|
||||
allParams = "${allParams},"
|
||||
allParams = "$allParams,"
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
appContext.currentActivity?.startActivity(newIntent)
|
||||
}
|
||||
@@ -104,7 +106,7 @@ class ExpoReceiveAndroidIntentsModule : Module() {
|
||||
return mapOf(
|
||||
"width" to bitmap.width,
|
||||
"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 width = info.getValue("width")
|
||||
val height = info.getValue("height")
|
||||
return "file://${path}|${width}|${height}"
|
||||
return "file://$path|$width|$height"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import ExpoModulesCore
|
||||
public class ExpoScrollForwarderModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("ExpoScrollForwarder")
|
||||
|
||||
|
||||
View(ExpoScrollForwarderView.self) {
|
||||
Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in
|
||||
view.scrollViewTag = prop
|
||||
|
||||
@@ -8,17 +8,17 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||||
self.tryFindScrollView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var rctScrollView: RCTScrollView?
|
||||
private var rctRefreshCtrl: RCTRefreshControl?
|
||||
private var cancelGestureRecognizers: [UIGestureRecognizer]?
|
||||
private var animTimer: Timer?
|
||||
private var initialOffset: CGFloat = 0.0
|
||||
private var didImpact: Bool = false
|
||||
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
|
||||
|
||||
let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:)))
|
||||
pg.delegate = self
|
||||
self.addGestureRecognizer(pg)
|
||||
@@ -34,28 +34,27 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||||
|
||||
self.cancelGestureRecognizers = [lpg, tg]
|
||||
}
|
||||
|
||||
|
||||
// We don't want to recognize the scroll pan gesture and the swipe back gesture together
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will
|
||||
// interfere with the native swipe back gesture.
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
let velocity = gestureRecognizer.velocity(in: self)
|
||||
return abs(velocity.y) > abs(velocity.x)
|
||||
}
|
||||
|
||||
|
||||
// This will be used to cancel the scroll animation whenever we tap inside of the header. We don't need another
|
||||
// recognizer for this one.
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
@@ -64,32 +63,32 @@ 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
|
||||
// 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()
|
||||
}
|
||||
|
||||
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) -> Void {
|
||||
|
||||
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
|
||||
guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else {
|
||||
return
|
||||
}
|
||||
|
||||
let translation = sender.translation(in: self).y
|
||||
|
||||
|
||||
if sender.state == .began {
|
||||
if sv.contentOffset.y < 0 {
|
||||
sv.contentOffset.y = 0
|
||||
}
|
||||
|
||||
|
||||
self.initialOffset = sv.contentOffset.y
|
||||
}
|
||||
|
||||
if sender.state == .changed {
|
||||
sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset)
|
||||
|
||||
|
||||
if sv.contentOffset.y <= -130, !didImpact {
|
||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||
generator.impactOccurred()
|
||||
|
||||
|
||||
self.didImpact = true
|
||||
}
|
||||
}
|
||||
@@ -97,7 +96,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||||
if sender.state == .ended {
|
||||
let velocity = sender.velocity(in: self).y
|
||||
self.didImpact = false
|
||||
|
||||
|
||||
if sv.contentOffset.y <= -130 {
|
||||
self.rctRefreshCtrl?.forwarderBeginRefreshing()
|
||||
return
|
||||
@@ -108,40 +107,40 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||||
if abs(velocity) < 250, sv.contentOffset.y >= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
self.startDecayAnimation(translation, velocity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) {
|
||||
guard let sv = self.rctScrollView?.scrollView else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var velocity = velocity
|
||||
|
||||
|
||||
self.enableCancelGestureRecognizers()
|
||||
|
||||
|
||||
if velocity > 0 {
|
||||
velocity = min(velocity, 5000)
|
||||
} else {
|
||||
velocity = max(velocity, -5000)
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
animTranslation = (-velocity / 120) + animTranslation
|
||||
|
||||
|
||||
let nextOffset = self.dampenOffset(animTranslation + self.initialOffset)
|
||||
|
||||
|
||||
if nextOffset <= 0 {
|
||||
if self.initialOffset <= 1 {
|
||||
self.scrollToOffset(0)
|
||||
} else {
|
||||
sv.contentOffset.y = 0
|
||||
}
|
||||
|
||||
|
||||
self.stopTimer()
|
||||
return
|
||||
} else {
|
||||
@@ -153,61 +152,60 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func dampenOffset(_ offset: CGFloat) -> CGFloat {
|
||||
if offset < 0 {
|
||||
return offset - (offset * 0.55)
|
||||
}
|
||||
|
||||
|
||||
return offset
|
||||
}
|
||||
|
||||
|
||||
func tryFindScrollView() {
|
||||
guard let scrollViewTag = scrollViewTag else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer.
|
||||
// Otherwise we might end up with duplicates when we switch back to that scrollview.
|
||||
self.removeCancelGestureRecognizers()
|
||||
|
||||
|
||||
self.rctScrollView = self.appContext?
|
||||
.findView(withTag: scrollViewTag, ofType: RCTScrollView.self)
|
||||
self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl
|
||||
|
||||
|
||||
self.addCancelGestureRecognizers()
|
||||
}
|
||||
|
||||
|
||||
func addCancelGestureRecognizers() {
|
||||
self.cancelGestureRecognizers?.forEach { r in
|
||||
self.rctScrollView?.scrollView?.addGestureRecognizer(r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func removeCancelGestureRecognizers() {
|
||||
self.cancelGestureRecognizers?.forEach { r in
|
||||
self.rctScrollView?.scrollView?.removeGestureRecognizer(r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func enableCancelGestureRecognizers() {
|
||||
self.cancelGestureRecognizers?.forEach { r in
|
||||
r.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func disableCancelGestureRecognizers() {
|
||||
self.cancelGestureRecognizers?.forEach { r in
|
||||
r.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func stopTimer() -> Void {
|
||||
func stopTimer() {
|
||||
self.disableCancelGestureRecognizers()
|
||||
self.animTimer?.invalidate()
|
||||
self.animTimer = nil
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.88.0",
|
||||
"version": "1.90.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -29,9 +29,12 @@
|
||||
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
|
||||
"test-coverage": "NODE_ENV=test jest --coverage",
|
||||
"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",
|
||||
"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-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
|
||||
"e2e:run": "maestro test __e2e__",
|
||||
"perf:test": "NODE_ENV=test maestro test",
|
||||
"perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml",
|
||||
@@ -49,7 +52,7 @@
|
||||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.12.23",
|
||||
"@atproto/api": "0.13.0",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
@@ -136,10 +139,11 @@
|
||||
"expo-system-ui": "~3.0.4",
|
||||
"expo-task-manager": "~11.8.1",
|
||||
"expo-updates": "~0.25.14",
|
||||
"expo-video": "^1.1.10",
|
||||
"expo-video": "^1.2.4",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
"hls.js": "^1.5.11",
|
||||
"js-sha256": "^0.9.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lande": "^1.0.10",
|
||||
@@ -204,7 +208,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/dev-env": "^0.3.5",
|
||||
"@atproto/dev-env": "^0.3.39",
|
||||
"@babel/core": "^7.23.2",
|
||||
"@babel/preset-env": "^7.20.0",
|
||||
"@babel/runtime": "^7.20.0",
|
||||
@@ -266,7 +270,7 @@
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript": "^5.5.4",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
diff --git a/node_modules/@atproto/lexicon/dist/validators/complex.js b/node_modules/@atproto/lexicon/dist/validators/complex.js
|
||||
index 32d7798..9d688b7 100644
|
||||
--- a/node_modules/@atproto/lexicon/dist/validators/complex.js
|
||||
+++ b/node_modules/@atproto/lexicon/dist/validators/complex.js
|
||||
@@ -113,7 +113,22 @@ function object(lexicons, path, def, value) {
|
||||
if (value[key] === null && nullableProps.has(key)) {
|
||||
continue;
|
||||
}
|
||||
- const propDef = def.properties[key];
|
||||
+ const propDef = def.properties[key]
|
||||
+ if (typeof value[key] === 'undefined' && !requiredProps.has(key)) {
|
||||
+ // Fast path for non-required undefined props.
|
||||
+ if (
|
||||
+ propDef.type === 'integer' ||
|
||||
+ propDef.type === 'boolean' ||
|
||||
+ propDef.type === 'string'
|
||||
+ ) {
|
||||
+ if (typeof propDef.default === 'undefined') {
|
||||
+ continue
|
||||
+ }
|
||||
+ } else {
|
||||
+ // Other types have no defaults.
|
||||
+ continue
|
||||
+ }
|
||||
+ }
|
||||
const propPath = `${path}/${key}`;
|
||||
const validated = (0, util_1.validateOneOf)(lexicons, propPath, propDef, value[key]);
|
||||
const propValue = validated.success ? validated.value : value[key];
|
||||
@@ -12,3 +12,15 @@ index bb74e80..0aa0202 100644
|
||||
|
||||
Map<String, Object> constants = new HashMap<>(3);
|
||||
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
|
||||
diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
|
||||
index 109d3fe..c7fce9e 100644
|
||||
--- a/node_modules/expo-modules-core/build/uuid/uuid.js
|
||||
+++ b/node_modules/expo-modules-core/build/uuid/uuid.js
|
||||
@@ -1,5 +1,7 @@
|
||||
import bytesToUuid from './lib/bytesToUuid';
|
||||
import { Uuidv5Namespace } from './uuid.types';
|
||||
+import { ensureNativeModulesAreInstalled } from '../ensureNativeModulesAreInstalled';
|
||||
+ensureNativeModulesAreInstalled();
|
||||
const nativeUuidv4 = globalThis?.expo?.uuidv4;
|
||||
const nativeUuidv5 = globalThis?.expo?.uuidv5;
|
||||
function uuidv4() {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||
index 9905e13..47342ff 100644
|
||||
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||
@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole
|
||||
setShowPreviousButton(!requireLinearPlayback)
|
||||
setShowNextButton(!requireLinearPlayback)
|
||||
setTimeBarInteractive(requireLinearPlayback)
|
||||
+ setShowSubtitleButton(true)
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
|
||||
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||
+ val fullscreenButton =
|
||||
+ findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||
fullscreenButton?.visibility = if (visible) {
|
||||
android.view.View.VISIBLE
|
||||
} else {
|
||||
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
|
||||
index ec3da2a..5a1397a 100644
|
||||
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
|
||||
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
|
||||
@@ -43,7 +43,9 @@ class VideoModule : Module() {
|
||||
View(VideoView::class) {
|
||||
Events(
|
||||
"onPictureInPictureStart",
|
||||
- "onPictureInPictureStop"
|
||||
+ "onPictureInPictureStop",
|
||||
+ "onEnterFullscreen",
|
||||
+ "onExitFullscreen"
|
||||
)
|
||||
|
||||
Prop("player") { view: VideoView, player: VideoPlayer ->
|
||||
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
|
||||
index a951d80..3932535 100644
|
||||
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
|
||||
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
|
||||
@@ -36,6 +36,8 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||
val playerView: PlayerView = PlayerView(context.applicationContext)
|
||||
val onPictureInPictureStart by EventDispatcher<Unit>()
|
||||
val onPictureInPictureStop by EventDispatcher<Unit>()
|
||||
+ val onEnterFullscreen by EventDispatcher()
|
||||
+ val onExitFullscreen by EventDispatcher()
|
||||
|
||||
var willEnterPiP: Boolean = false
|
||||
var isInFullscreen: Boolean = false
|
||||
@@ -154,6 +156,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||
@Suppress("DEPRECATION")
|
||||
currentActivity.overridePendingTransition(0, 0)
|
||||
}
|
||||
+ onEnterFullscreen(mapOf())
|
||||
isInFullscreen = true
|
||||
}
|
||||
|
||||
@@ -162,6 +165,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||
val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
|
||||
fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
|
||||
videoPlayer?.changePlayerView(playerView)
|
||||
+ this.onExitFullscreen(mapOf())
|
||||
isInFullscreen = false
|
||||
}
|
||||
|
||||
diff --git a/node_modules/expo-video/build/VideoView.types.d.ts b/node_modules/expo-video/build/VideoView.types.d.ts
|
||||
index cb9ca6d..60e9f4e 100644
|
||||
--- a/node_modules/expo-video/build/VideoView.types.d.ts
|
||||
+++ b/node_modules/expo-video/build/VideoView.types.d.ts
|
||||
@@ -89,5 +89,8 @@ export interface VideoViewProps extends ViewProps {
|
||||
* @platform ios 16.0+
|
||||
*/
|
||||
allowsVideoFrameAnalysis?: boolean;
|
||||
+
|
||||
+ onEnterFullscreen?: () => void;
|
||||
+ onExitFullscreen?: () => void;
|
||||
}
|
||||
//# sourceMappingURL=VideoView.types.d.ts.map
|
||||
diff --git a/node_modules/expo-video/ios/VideoModule.swift b/node_modules/expo-video/ios/VideoModule.swift
|
||||
index c537a12..e4a918f 100644
|
||||
--- a/node_modules/expo-video/ios/VideoModule.swift
|
||||
+++ b/node_modules/expo-video/ios/VideoModule.swift
|
||||
@@ -16,7 +16,9 @@ public final class VideoModule: Module {
|
||||
View(VideoView.self) {
|
||||
Events(
|
||||
"onPictureInPictureStart",
|
||||
- "onPictureInPictureStop"
|
||||
+ "onPictureInPictureStop",
|
||||
+ "onEnterFullscreen",
|
||||
+ "onExitFullscreen"
|
||||
)
|
||||
|
||||
Prop("player") { (view, player: VideoPlayer?) in
|
||||
diff --git a/node_modules/expo-video/ios/VideoView.swift b/node_modules/expo-video/ios/VideoView.swift
|
||||
index f4579e4..10c5908 100644
|
||||
--- a/node_modules/expo-video/ios/VideoView.swift
|
||||
+++ b/node_modules/expo-video/ios/VideoView.swift
|
||||
@@ -41,6 +41,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||
|
||||
let onPictureInPictureStart = EventDispatcher()
|
||||
let onPictureInPictureStop = EventDispatcher()
|
||||
+ let onEnterFullscreen = EventDispatcher()
|
||||
+ let onExitFullscreen = EventDispatcher()
|
||||
|
||||
public override var bounds: CGRect {
|
||||
didSet {
|
||||
@@ -163,6 +165,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||
_ playerViewController: AVPlayerViewController,
|
||||
willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
||||
) {
|
||||
+ onEnterFullscreen()
|
||||
isFullscreen = true
|
||||
}
|
||||
|
||||
@@ -179,6 +182,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||
if wasPlaying {
|
||||
self.player?.pointer.play()
|
||||
}
|
||||
+ self.onExitFullscreen()
|
||||
self.isFullscreen = false
|
||||
}
|
||||
}
|
||||
diff --git a/node_modules/expo-video/src/VideoView.types.ts b/node_modules/expo-video/src/VideoView.types.ts
|
||||
index 29fe5db..e1fbf59 100644
|
||||
--- a/node_modules/expo-video/src/VideoView.types.ts
|
||||
+++ b/node_modules/expo-video/src/VideoView.types.ts
|
||||
@@ -100,4 +100,7 @@ export interface VideoViewProps extends ViewProps {
|
||||
* @platform ios 16.0+
|
||||
*/
|
||||
allowsVideoFrameAnalysis?: boolean;
|
||||
+
|
||||
+ onEnterFullscreen?: () => void;
|
||||
+ onExitFullscreen?: () => void;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
## uwu woad beawing, do not wemove
|
||||
|
||||
## `expo-video` Patch
|
||||
|
||||
This patch adds two props to `VideoView`: `onEnterFullscreen` and `onExitFullscreen` which do exactly what they say on
|
||||
the tin.
|
||||
@@ -207,31 +207,3 @@ index 88b3fdf..2488ebc 100644
|
||||
|
||||
const { layout, entering, exiting, sharedTransitionTag } = this.props;
|
||||
if (
|
||||
diff --git a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
index ac9be5d..86d4605 100644
|
||||
--- a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
+++ b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
|
||||
@@ -47,4 +47,5 @@ export { LayoutAnimationConfig } from './component/LayoutAnimationConfig';
|
||||
export { PerformanceMonitor } from './component/PerformanceMonitor';
|
||||
export { startMapper, stopMapper } from './mappers';
|
||||
export { startScreenTransition, finishScreenTransition, ScreenTransition } from './screenTransition';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
//# sourceMappingURL=index.js.map
|
||||
diff --git a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
index f01dc57..161ef22 100644
|
||||
--- a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
+++ b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
|
||||
@@ -36,3 +36,4 @@ export type { FlatListPropsWithLayout } from './component/FlatList';
|
||||
export { startMapper, stopMapper } from './mappers';
|
||||
export { startScreenTransition, finishScreenTransition, ScreenTransition, } from './screenTransition';
|
||||
export type { AnimatedScreenTransition, GoBackGesture, ScreenTransitionConfig, } from './screenTransition';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
diff --git a/node_modules/react-native-reanimated/src/reanimated2/index.ts b/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
index 5885fa1..a3c693f 100644
|
||||
--- a/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
+++ b/node_modules/react-native-reanimated/src/reanimated2/index.ts
|
||||
@@ -284,3 +284,4 @@ export type {
|
||||
GoBackGesture,
|
||||
ScreenTransitionConfig,
|
||||
} from './screenTransition';
|
||||
+export { isReducedMotion } from './PlatformChecker';
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -23,10 +23,12 @@ import {
|
||||
} from '#/lib/statsig/statsig'
|
||||
import {s} from '#/lib/styles'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -49,6 +51,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||
import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Shell} from '#/view/shell'
|
||||
import {ThemeProvider as Alf} from '#/alf'
|
||||
@@ -58,8 +61,7 @@ import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Splash} from '#/Splash'
|
||||
import {Provider as TourProvider} from '#/tours'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {listenSessionDropped} from './state/events'
|
||||
import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
@@ -95,7 +97,10 @@ function InnerApp() {
|
||||
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
|
||||
Toast.show(
|
||||
_(msg`Sorry! Your session expired. Please log in again.`),
|
||||
'info',
|
||||
)
|
||||
})
|
||||
}, [_])
|
||||
|
||||
@@ -104,42 +109,44 @@ function InnerApp() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Splash isReady={isReady && hasCheckedReferrer}>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</RootSiblingParent>
|
||||
<ActiveVideoProvider>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
</RootSiblingParent>
|
||||
</ActiveVideoProvider>
|
||||
</Splash>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
@@ -151,6 +158,8 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
||||
PlatformInfo.setAudioActive(false)
|
||||
initPersistedState().then(() => setReady(true))
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ import {useIntentHandler} from '#/lib/hooks/useIntentHandler'
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {ThemeProvider} from '#/lib/ThemeContext'
|
||||
import I18nProvider from '#/locale/i18nProvider'
|
||||
import {logger} from '#/logger'
|
||||
import {Provider as A11yProvider} from '#/state/a11y'
|
||||
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
|
||||
import {Provider as DialogStateProvider} from '#/state/dialogs'
|
||||
import {listenSessionDropped} from '#/state/events'
|
||||
import {Provider as InvitesStateProvider} from '#/state/invites'
|
||||
import {Provider as LightboxStateProvider} from '#/state/lightbox'
|
||||
import {MessagesProvider} from '#/state/messages'
|
||||
@@ -37,6 +39,7 @@ import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {ToastContainer} from '#/view/com/util/Toast.web'
|
||||
import {Shell} from '#/view/shell/index'
|
||||
@@ -46,8 +49,6 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as TourProvider} from '#/tours'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {listenSessionDropped} from './state/events'
|
||||
|
||||
function InnerApp() {
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
@@ -77,7 +78,10 @@ function InnerApp() {
|
||||
|
||||
useEffect(() => {
|
||||
return listenSessionDropped(() => {
|
||||
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
|
||||
Toast.show(
|
||||
_(msg`Sorry! Your session expired. Please log in again.`),
|
||||
'info',
|
||||
)
|
||||
})
|
||||
}, [_])
|
||||
|
||||
@@ -89,39 +93,41 @@ function InnerApp() {
|
||||
<Alf theme={theme}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<RootSiblingParent>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
<ActiveVideoProvider>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<QueryProvider currentDid={currentAccount?.did}>
|
||||
<StatsigProvider>
|
||||
<MessagesProvider>
|
||||
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
|
||||
<LabelDefsProvider>
|
||||
<ModerationOptsProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<TourProvider>
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</ModerationOptsProvider>
|
||||
</LabelDefsProvider>
|
||||
</MessagesProvider>
|
||||
</StatsigProvider>
|
||||
</QueryProvider>
|
||||
</React.Fragment>
|
||||
<ToastContainer />
|
||||
</ActiveVideoProvider>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</Alf>
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
} from 'lib/routes/types'
|
||||
import {RouteParams, State} from 'lib/routes/types'
|
||||
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 {AppPasswords} from 'view/screens/AppPasswords'
|
||||
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
|
||||
@@ -39,15 +39,18 @@ import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
|
||||
import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed'
|
||||
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
|
||||
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
||||
import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
|
||||
import HashtagScreen from '#/screens/Hashtag'
|
||||
import {ModerationScreen} from '#/screens/Moderation'
|
||||
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
||||
import {
|
||||
StarterPackScreen,
|
||||
StarterPackScreenShort,
|
||||
} from '#/screens/StarterPack/StarterPackScreen'
|
||||
import {Wizard} from '#/screens/StarterPack/Wizard'
|
||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||
import {init as initAnalytics} from './lib/analytics/analytics'
|
||||
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
||||
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
|
||||
@@ -74,6 +77,7 @@ import {LogScreen} from './view/screens/Log'
|
||||
import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
|
||||
import {NotFoundScreen} from './view/screens/NotFound'
|
||||
import {NotificationsScreen} from './view/screens/Notifications'
|
||||
import {NotificationsSettingsScreen} from './view/screens/NotificationsSettings'
|
||||
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
||||
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
|
||||
import {PostThreadScreen} from './view/screens/PostThread'
|
||||
@@ -233,6 +237,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => DebugModScreen}
|
||||
options={{title: title(msg`Moderation states`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="SharedPreferencesTester"
|
||||
getComponent={() => SharedPreferencesTesterScreen}
|
||||
options={{title: title(msg`Shared Preferences Tester`)}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Log"
|
||||
getComponent={() => LogScreen}
|
||||
@@ -302,6 +311,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AppearanceSettings"
|
||||
getComponent={() => AppearanceSettingsScreen}
|
||||
options={{
|
||||
title: title(msg`Appearance Settings`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Hashtag"
|
||||
getComponent={() => HashtagScreen}
|
||||
@@ -317,6 +334,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => MessagesSettingsScreen}
|
||||
options={{title: title(msg`Chat settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NotificationsSettings"
|
||||
getComponent={() => NotificationsSettingsScreen}
|
||||
options={{title: title(msg`Notification settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Feeds"
|
||||
getComponent={() => FeedsScreen}
|
||||
@@ -763,6 +785,17 @@ function logModuleInitTime() {
|
||||
initMs,
|
||||
})
|
||||
|
||||
if (isWeb) {
|
||||
const referrerInfo = Referrer.getReferrerInfo()
|
||||
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
||||
logEvent('deepLink:referrerReceived', {
|
||||
to: window.location.href,
|
||||
referrer: referrerInfo?.referrer,
|
||||
hostname: referrerInfo?.hostname,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
// This log is noisy, so keep false committed
|
||||
const shouldLog = false
|
||||
|
||||
@@ -2,7 +2,6 @@ import {Platform, StyleSheet} from 'react-native'
|
||||
|
||||
import * as tokens from '#/alf/tokens'
|
||||
import {native, web} from '#/alf/util/platform'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
|
||||
export const atoms = {
|
||||
/*
|
||||
@@ -145,6 +144,9 @@ export const atoms = {
|
||||
flex_shrink: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
flex_shrink_0: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
justify_start: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
@@ -281,19 +283,19 @@ export const atoms = {
|
||||
borderWidth: 0,
|
||||
},
|
||||
border: {
|
||||
borderWidth: hairlineWidth,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_t: {
|
||||
borderTopWidth: hairlineWidth,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_b: {
|
||||
borderBottomWidth: hairlineWidth,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_l: {
|
||||
borderLeftWidth: hairlineWidth,
|
||||
borderLeftWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
border_r: {
|
||||
borderRightWidth: hairlineWidth,
|
||||
borderRightWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react'
|
||||
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 * as tokens from '#/alf/tokens'
|
||||
@@ -39,8 +41,8 @@ function getActiveBreakpoints({width}: {width: number}) {
|
||||
* Context
|
||||
*/
|
||||
export const Context = React.createContext<{
|
||||
themeName: themes.ThemeName
|
||||
theme: themes.Theme
|
||||
themeName: ThemeName
|
||||
theme: Theme
|
||||
breakpoints: {
|
||||
active: BreakpointName | undefined
|
||||
gtPhone: boolean
|
||||
@@ -49,7 +51,7 @@ export const Context = React.createContext<{
|
||||
}
|
||||
}>({
|
||||
themeName: 'light',
|
||||
theme: themes.light,
|
||||
theme: defaultTheme,
|
||||
breakpoints: {
|
||||
active: undefined,
|
||||
gtPhone: false,
|
||||
@@ -61,7 +63,16 @@ export const Context = React.createContext<{
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
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 [breakpoints, setBreakpoints] = React.useState(() =>
|
||||
getActiveBreakpoints({width: Dimensions.get('window').width}),
|
||||
|
||||
@@ -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 = {
|
||||
trueBlack: '#000000',
|
||||
|
||||
temp_purple: 'rgb(105 0 255)',
|
||||
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
|
||||
|
||||
export const space = {
|
||||
@@ -178,10 +107,3 @@ export const gradients = {
|
||||
hover_value: '#755B62',
|
||||
},
|
||||
} 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
|
||||
|
||||
@@ -1,21 +1,4 @@
|
||||
import {StyleProp, ViewStyle, TextStyle} 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]>
|
||||
}
|
||||
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
|
||||
|
||||
export type TextStyleProp = {
|
||||
style?: StyleProp<TextStyle>
|
||||
@@ -24,3 +7,156 @@ export type TextStyleProp = {
|
||||
export type ViewStyleProp = {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
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>) {
|
||||
switch (name) {
|
||||
|
||||
@@ -4,7 +4,8 @@ import * as SystemUI from 'expo-system-ui'
|
||||
|
||||
import {isWeb} from 'platform/detection'
|
||||
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 {
|
||||
const theme = useThemeName()
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from 'react-native'
|
||||
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 {normalizeTextStyles} from '#/components/Typography'
|
||||
|
||||
@@ -70,6 +70,9 @@ export type ButtonProps = Pick<
|
||||
AccessibilityProps &
|
||||
VariantProps & {
|
||||
testID?: string
|
||||
/**
|
||||
* For a11y, try to make this descriptive and clear
|
||||
*/
|
||||
label: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
hoverStyle?: StyleProp<ViewStyle>
|
||||
@@ -152,7 +155,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
const {baseStyles, hoverStyles} = React.useMemo(() => {
|
||||
const baseStyles: ViewStyle[] = []
|
||||
const hoverStyles: ViewStyle[] = []
|
||||
const light = t.name === 'light'
|
||||
|
||||
if (color === 'primary') {
|
||||
if (variant === 'solid') {
|
||||
@@ -165,7 +167,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
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') {
|
||||
@@ -178,40 +184,28 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
borderColor: t.palette.primary_500,
|
||||
})
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: light
|
||||
? t.palette.primary_50
|
||||
: t.palette.primary_950,
|
||||
backgroundColor: t.palette.primary_50,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: light
|
||||
? t.palette.primary_200
|
||||
: t.palette.primary_900,
|
||||
borderColor: t.palette.primary_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: light
|
||||
? t.palette.primary_100
|
||||
: t.palette.primary_900,
|
||||
backgroundColor: t.palette.primary_100,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (color === 'secondary') {
|
||||
if (variant === 'solid') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
})
|
||||
baseStyles.push(t.atoms.bg_contrast_25)
|
||||
hoverStyles.push(t.atoms.bg_contrast_50)
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
})
|
||||
baseStyles.push(t.atoms.bg_contrast_100)
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
baseStyles.push(a.border, t.atoms.bg, {
|
||||
@@ -247,7 +241,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.contrast_700,
|
||||
backgroundColor: t.palette.contrast_600,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
@@ -284,7 +278,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
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') {
|
||||
@@ -297,24 +295,18 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
borderColor: t.palette.negative_500,
|
||||
})
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: light
|
||||
? t.palette.negative_50
|
||||
: t.palette.negative_975,
|
||||
backgroundColor: t.palette.negative_50,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: light
|
||||
? t.palette.negative_200
|
||||
: t.palette.negative_900,
|
||||
borderColor: t.palette.negative_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: light
|
||||
? t.palette.negative_100
|
||||
: t.palette.negative_975,
|
||||
backgroundColor: t.palette.negative_100,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -482,7 +474,6 @@ export function useSharedButtonTextStyles() {
|
||||
const {color, variant, disabled, size} = useButtonContext()
|
||||
return React.useMemo(() => {
|
||||
const baseStyles: TextStyle[] = []
|
||||
const light = t.name === 'light'
|
||||
|
||||
if (color === 'primary') {
|
||||
if (variant === 'solid') {
|
||||
@@ -494,7 +485,7 @@ export function useSharedButtonTextStyles() {
|
||||
} else if (variant === 'outline') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: light ? t.palette.primary_600 : t.palette.primary_500,
|
||||
color: t.palette.primary_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
|
||||
|
||||
@@ -40,7 +40,7 @@ type Props = {
|
||||
export function Default(props: Props) {
|
||||
const {view} = props
|
||||
return (
|
||||
<Link label={view.displayName} {...props}>
|
||||
<Link {...props}>
|
||||
<Outer>
|
||||
<Header>
|
||||
<Avatar src={view.avatar} />
|
||||
@@ -58,7 +58,7 @@ export function Link({
|
||||
view,
|
||||
children,
|
||||
...props
|
||||
}: Props & Omit<LinkProps, 'to'>) {
|
||||
}: Props & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
@@ -70,7 +70,11 @@ export function Link({
|
||||
}, [view, queryClient])
|
||||
|
||||
return (
|
||||
<InternalLink to={href} style={[a.flex_col]} {...props}>
|
||||
<InternalLink
|
||||
label={view.displayName}
|
||||
to={href}
|
||||
style={[a.flex_col]}
|
||||
{...props}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
)
|
||||
@@ -264,9 +268,9 @@ function SaveButtonInner({
|
||||
])
|
||||
}
|
||||
Toast.show(_(msg`Feeds updated!`))
|
||||
} catch (e: any) {
|
||||
logger.error(e, {context: `FeedCard: failed to update feeds`, pin})
|
||||
Toast.show(_(msg`Failed to update feeds`))
|
||||
} catch (err: any) {
|
||||
logger.error(err, {context: `FeedCard: failed to update feeds`, pin})
|
||||
Toast.show(_(msg`Failed to update feeds`), 'xmark')
|
||||
}
|
||||
},
|
||||
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig, type],
|
||||
|
||||
@@ -13,6 +13,7 @@ import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {SeenPost} from '#/state/userActionHistory'
|
||||
@@ -91,14 +92,16 @@ function getRank(seenPost: SeenPost): string {
|
||||
tier = 'a'
|
||||
} else if (seenPost.feedContext?.startsWith('cluster')) {
|
||||
tier = 'b'
|
||||
} else if (seenPost.feedContext?.startsWith('ntpc')) {
|
||||
} else if (seenPost.feedContext === 'popcluster') {
|
||||
tier = 'c'
|
||||
} else if (seenPost.feedContext?.startsWith('t-')) {
|
||||
} else if (seenPost.feedContext?.startsWith('ntpc')) {
|
||||
tier = 'd'
|
||||
} else if (seenPost.feedContext === 'nettop') {
|
||||
} else if (seenPost.feedContext?.startsWith('t-')) {
|
||||
tier = 'e'
|
||||
} else {
|
||||
} else if (seenPost.feedContext === 'nettop') {
|
||||
tier = 'f'
|
||||
} else {
|
||||
tier = 'g'
|
||||
}
|
||||
let score = Math.round(
|
||||
Math.log(
|
||||
@@ -127,19 +130,32 @@ function sortSeenPosts(postA: SeenPost, postB: SeenPost): 0 | 1 | -1 {
|
||||
}
|
||||
|
||||
function useExperimentalSuggestedUsersQuery() {
|
||||
const {currentAccount} = useSession()
|
||||
const userActionSnapshot = userActionHistory.useActionHistorySnapshot()
|
||||
const dids = React.useMemo(() => {
|
||||
const {likes, follows, seen} = userActionSnapshot
|
||||
const {likes, follows, followSuggestions, seen} = userActionSnapshot
|
||||
const likeDids = likes
|
||||
.map(l => new AtUri(l))
|
||||
.map(uri => uri.host)
|
||||
.filter(did => !follows.includes(did))
|
||||
let suggestedDids: string[] = []
|
||||
if (followSuggestions.length > 0) {
|
||||
suggestedDids = [
|
||||
// It's ok if these will pick the same item (weighed by its frequency)
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
followSuggestions[Math.floor(Math.random() * followSuggestions.length)],
|
||||
]
|
||||
}
|
||||
const seenDids = seen
|
||||
.sort(sortSeenPosts)
|
||||
.map(l => new AtUri(l.uri))
|
||||
.map(uri => uri.host)
|
||||
return [...new Set([...likeDids, ...seenDids])]
|
||||
}, [userActionSnapshot])
|
||||
return [...new Set([...suggestedDids, ...likeDids, ...seenDids])].filter(
|
||||
did => did !== currentAccount?.did,
|
||||
)
|
||||
}, [userActionSnapshot, currentAccount])
|
||||
const {data, isLoading, error} = useProfilesQuery({
|
||||
handles: dids.slice(0, 16),
|
||||
})
|
||||
@@ -186,7 +202,7 @@ export function SuggestedFollows() {
|
||||
{profiles.slice(0, maxLength).map(profile => (
|
||||
<ProfileCard.Link
|
||||
key={profile.did}
|
||||
did={profile.handle}
|
||||
profile={profile}
|
||||
onPress={() => {
|
||||
logEvent('feed:interstitial:profileCard:press', {})
|
||||
}}
|
||||
@@ -262,7 +278,10 @@ export function SuggestedFollows() {
|
||||
a.pt_xs,
|
||||
a.gap_md,
|
||||
]}>
|
||||
<InlineLinkText to="/search" style={[t.atoms.text_contrast_medium]}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Browse more suggestions`)}
|
||||
to="/search"
|
||||
style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Browse more suggestions</Trans>
|
||||
</InlineLinkText>
|
||||
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
@@ -392,7 +411,10 @@ export function SuggestedFeeds() {
|
||||
a.pt_xs,
|
||||
a.gap_md,
|
||||
]}>
|
||||
<InlineLinkText to="/search" style={[t.atoms.text_contrast_medium]}>
|
||||
<InlineLinkText
|
||||
label={_(msg`Browse more suggestions`)}
|
||||
to="/search"
|
||||
style={[t.atoms.text_contrast_medium]}>
|
||||
<Trans>Browse more suggestions</Trans>
|
||||
</InlineLinkText>
|
||||
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
|
||||
@@ -2,14 +2,14 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {
|
||||
useTheme,
|
||||
atoms as a,
|
||||
ViewStyleProp,
|
||||
TextStyleProp,
|
||||
flatten,
|
||||
TextStyleProp,
|
||||
useTheme,
|
||||
ViewStyleProp,
|
||||
} from '#/alf'
|
||||
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
||||
import {Props} from '#/components/icons/common'
|
||||
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
||||
|
||||
export function IconCircle({
|
||||
icon: Icon,
|
||||
@@ -32,8 +32,7 @@ export function IconCircle({
|
||||
{
|
||||
width: size === 'lg' ? 52 : 64,
|
||||
height: size === 'lg' ? 52 : 64,
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950,
|
||||
backgroundColor: t.palette.primary_50,
|
||||
},
|
||||
flatten(style),
|
||||
]}>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {Link, LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const AVI_SIZE = 30
|
||||
const AVI_SIZE_SMALL = 20
|
||||
const AVI_BORDER = 1
|
||||
|
||||
/**
|
||||
@@ -30,10 +31,12 @@ export function KnownFollowers({
|
||||
profile,
|
||||
moderationOpts,
|
||||
onLinkPress,
|
||||
minimal,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
moderationOpts: ModerationOpts
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
}) {
|
||||
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
|
||||
new Map(),
|
||||
@@ -59,6 +62,7 @@ export function KnownFollowers({
|
||||
cachedKnownFollowers={cachedKnownFollowers}
|
||||
moderationOpts={moderationOpts}
|
||||
onLinkPress={onLinkPress}
|
||||
minimal={minimal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -71,11 +75,13 @@ function KnownFollowersInner({
|
||||
moderationOpts,
|
||||
cachedKnownFollowers,
|
||||
onLinkPress,
|
||||
minimal,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
moderationOpts: ModerationOpts
|
||||
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
@@ -110,6 +116,8 @@ function KnownFollowersInner({
|
||||
*/
|
||||
if (slice.length === 0) return null
|
||||
|
||||
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
|
||||
|
||||
return (
|
||||
<Link
|
||||
label={_(
|
||||
@@ -120,7 +128,7 @@ function KnownFollowersInner({
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
minimal ? a.gap_sm : a.gap_md,
|
||||
a.align_center,
|
||||
{marginLeft: -AVI_BORDER},
|
||||
]}>
|
||||
@@ -129,8 +137,8 @@ function KnownFollowersInner({
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
height: AVI_SIZE,
|
||||
width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||
height: SIZE,
|
||||
width: SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||
},
|
||||
pressed && {
|
||||
opacity: 0.5,
|
||||
@@ -145,14 +153,14 @@ function KnownFollowersInner({
|
||||
{
|
||||
borderWidth: AVI_BORDER,
|
||||
borderColor: t.atoms.bg.backgroundColor,
|
||||
width: AVI_SIZE + AVI_BORDER * 2,
|
||||
height: AVI_SIZE + AVI_BORDER * 2,
|
||||
width: SIZE + AVI_BORDER * 2,
|
||||
height: SIZE + AVI_BORDER * 2,
|
||||
left: i * a.gap_md.gap,
|
||||
zIndex: AVI_BORDER - i,
|
||||
},
|
||||
]}>
|
||||
<UserAvatar
|
||||
size={AVI_SIZE}
|
||||
size={SIZE}
|
||||
avatar={prof.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
|
||||
@@ -40,11 +40,6 @@ type BaseLinkProps = Pick<
|
||||
> & {
|
||||
testID?: string
|
||||
|
||||
/**
|
||||
* Label for a11y. Defaults to the href.
|
||||
*/
|
||||
label?: string
|
||||
|
||||
/**
|
||||
* The React Navigation `StackAction` to perform when the link is pressed.
|
||||
*/
|
||||
@@ -197,7 +192,7 @@ export function useLink({
|
||||
}
|
||||
|
||||
export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> &
|
||||
Omit<ButtonProps, 'onPress' | 'disabled' | 'label'>
|
||||
Omit<ButtonProps, 'onPress' | 'disabled'>
|
||||
|
||||
/**
|
||||
* A interactive element that renders as a `<a>` tag on the web. On mobile it
|
||||
@@ -224,7 +219,6 @@ export function Link({
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={href}
|
||||
{...rest}
|
||||
style={[a.justify_start, flatten(rest.style)]}
|
||||
role="link"
|
||||
@@ -249,7 +243,8 @@ export function Link({
|
||||
|
||||
export type InlineLinkProps = React.PropsWithChildren<
|
||||
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
|
||||
>
|
||||
> &
|
||||
Pick<ButtonProps, 'label'>
|
||||
|
||||
export function InlineLinkText({
|
||||
children,
|
||||
@@ -291,7 +286,7 @@ export function InlineLinkText({
|
||||
<Text
|
||||
selectable={selectable}
|
||||
accessibilityHint=""
|
||||
accessibilityLabel={label || href}
|
||||
accessibilityLabel={label}
|
||||
{...rest}
|
||||
style={[
|
||||
{color: t.palette.primary_500},
|
||||
|
||||
@@ -44,7 +44,7 @@ type Props = {
|
||||
export function Default(props: Props) {
|
||||
const {view, showPinButton} = props
|
||||
return (
|
||||
<Link label={view.name} {...props}>
|
||||
<Link {...props}>
|
||||
<Outer>
|
||||
<Header>
|
||||
<Avatar src={view.avatar} />
|
||||
@@ -67,7 +67,7 @@ export function Link({
|
||||
view,
|
||||
children,
|
||||
...props
|
||||
}: Props & Omit<LinkProps, 'to'>) {
|
||||
}: Props & Omit<LinkProps, 'to' | 'label'>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
@@ -79,7 +79,7 @@ export function Link({
|
||||
}, [view, queryClient])
|
||||
|
||||
return (
|
||||
<InternalLink to={href} {...props}>
|
||||
<InternalLink label={view.name} to={href} {...props}>
|
||||
{children}
|
||||
</InternalLink>
|
||||
)
|
||||
|
||||
@@ -122,8 +122,16 @@ export function ListHeaderDesktop({
|
||||
if (!gtTablet) return null
|
||||
|
||||
return (
|
||||
<View style={[a.w_full, a.py_lg, a.px_xl, a.gap_xs]}>
|
||||
<Text style={[a.text_3xl, a.font_bold]}>{title}</Text>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.py_sm,
|
||||
a.px_xl,
|
||||
a.gap_xs,
|
||||
a.justify_center,
|
||||
{minHeight: 50},
|
||||
]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>{title}</Text>
|
||||
{subtitle ? (
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
{subtitle}
|
||||
@@ -189,7 +197,7 @@ let ListMaybePlaceholder = ({
|
||||
return (
|
||||
<Error
|
||||
title={errorTitle ?? _(msg`Oops!`)}
|
||||
message={errorMessage ?? _(`Something went wrong!`)}
|
||||
message={errorMessage ?? _(msg`Something went wrong!`)}
|
||||
onRetry={onRetry}
|
||||
onGoBack={onGoBack}
|
||||
sideBorders={sideBorders}
|
||||
|
||||