Compare commits
548 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b77ce2e6f0 | |||
| 9302940a23 | |||
| 26a4f85fda | |||
| 0d8a7cd816 | |||
| c9c4a107f2 | |||
| 43bc272615 | |||
| d563beb239 | |||
| 94771b5c7f | |||
| a941cc7758 | |||
| 759f022351 | |||
| 59b096196c | |||
| f39a27ed66 | |||
| d0bca3c576 | |||
| 03b44c0e45 | |||
| 10c9b26475 | |||
| 3e6a6960d4 | |||
| 5635bde707 | |||
| 1a8c6ac71e | |||
| 3bb220f8be | |||
| 8187a07aee | |||
| d054a69bcf | |||
| 4b1fe49132 | |||
| b2c139dbc0 | |||
| 02488f06d8 | |||
| b0d367a3b8 | |||
| 19c9ffa352 | |||
| cfc7d17db3 | |||
| 71b24952c7 | |||
| d7aed0b199 | |||
| 2823524388 | |||
| fd8083350c | |||
| 1411f33393 | |||
| 8840293c92 | |||
| 83fa99a4fe | |||
| 64473f5c98 | |||
| a45424fdb1 | |||
| 14d3389a63 | |||
| 311c7b52c8 | |||
| a8fd747187 | |||
| 259610ab37 | |||
| 44b4ec22d7 | |||
| f3dd6a89a1 | |||
| a60473b93f | |||
| c8c5ab05cd | |||
| 38219721c9 | |||
| 9d6e23b6e7 | |||
| ce69d13ec4 | |||
| 4875c2edaf | |||
| e39759a9a4 | |||
| 547b663bb6 | |||
| 6ab5776f95 | |||
| 9b40d4c8cc | |||
| a0a81da164 | |||
| 0220adaefb | |||
| 481dee52b1 | |||
| 78d74a3d7c | |||
| 83ca9059fa | |||
| 8da491cc3b | |||
| 8d1b3adb6a | |||
| 626f85651f | |||
| 2acc7ed107 | |||
| 14a2be33af | |||
| a642a3fe8f | |||
| b90f7eaefb | |||
| 455515cae1 | |||
| e3fc3b997a | |||
| 3c529e743f | |||
| 9d7f968cdd | |||
| b5769f9aed | |||
| ac98b322ca | |||
| c499f5f96a | |||
| d719b502d3 | |||
| 02e24c23a6 | |||
| c57ae4cea4 | |||
| 2536d6bf10 | |||
| 060ce2c725 | |||
| a8d2935cf2 | |||
| d6fb1528a0 | |||
| 2e3d170d1e | |||
| 4690e28cde | |||
| 66a754ad22 | |||
| 83a74f0f6e | |||
| 811b8cfc4c | |||
| a81d9cfca6 | |||
| c7ef5cd741 | |||
| 41f6afc528 | |||
| 37cb821d6b | |||
| 203bfc4c7c | |||
| 779556f36d | |||
| cf575cca68 | |||
| 97e6bdbe40 | |||
| 5f0101a360 | |||
| a3bb305229 | |||
| 808b33e7a7 | |||
| ac917ba829 | |||
| 6c3f2d3505 | |||
| 742a43658f | |||
| 0307e2e932 | |||
| 7c1add25a1 | |||
| feeab9205b | |||
| 1023653d97 | |||
| 077f2fe093 | |||
| d636ec6339 | |||
| 98c9a6ad32 | |||
| 33a4d2cbe5 | |||
| 8a5bc60819 | |||
| f492fd4d41 | |||
| ed7204ab71 | |||
| 9aeaa38f3b | |||
| b3f9ed7981 | |||
| b8ab064fcc | |||
| 58bf9cb8cd | |||
| 8a67e8d102 | |||
| 5d6fc56349 | |||
| 77ed2129d3 | |||
| fcebe8e0b0 | |||
| 6df617003a | |||
| 6953bf9d22 | |||
| 9396595595 | |||
| cf13b5cd4c | |||
| e24ab48b73 | |||
| bbee08eb22 | |||
| c68f291128 | |||
| 32b3575074 | |||
| 11f972c640 | |||
| 61cf754fd5 | |||
| 00b50c71b5 | |||
| 66eb6f2888 | |||
| bc571ee998 | |||
| c758996034 | |||
| 1f26494c94 | |||
| 74bfc10e36 | |||
| e194e5de91 | |||
| 09f96da982 | |||
| 6e48d63dda | |||
| eec636897c | |||
| 777f30562b | |||
| 16e4bc66bd | |||
| 98c5312a29 | |||
| 9971f1f035 | |||
| dec9d97171 | |||
| 355749e605 | |||
| 6268736bfa | |||
| a270d750c8 | |||
| 0eb4c7495e | |||
| b438318b92 | |||
| 01aaa6123f | |||
| 486a1f082c | |||
| f943cbd04a | |||
| c54fa5b2e4 | |||
| 54c0115181 | |||
| bb3a2fddfc | |||
| 5135a13812 | |||
| 059c3b362d | |||
| 090ceb4c53 | |||
| 7796203c25 | |||
| 44ae514d89 | |||
| 12d8e06a1e | |||
| de65d9198a | |||
| 41bf8e15cd | |||
| adeb1d5cec | |||
| 61511d5161 | |||
| aee205b0ab | |||
| e59763b50a | |||
| 5168d44926 | |||
| def3156c33 | |||
| a290891f70 | |||
| a96562e89c | |||
| 279d41fcd1 | |||
| 4932505c5e | |||
| 629b7d8c9b | |||
| 5c71f8e32a | |||
| 395a8838be | |||
| 17903b817d | |||
| 54f05bfcf6 | |||
| b1a058d098 | |||
| 27657541f2 | |||
| 73ea67f5c0 | |||
| d065b62f52 | |||
| eb03170736 | |||
| 6f734f7978 | |||
| 9235d6c13b | |||
| bc54c8d7c6 | |||
| 3b8dbbff12 | |||
| e8bb545023 | |||
| f0b30e26d4 | |||
| cbfa9f62a4 | |||
| fefd8cc8a0 | |||
| 09765a5d36 | |||
| e325b72c62 | |||
| 055a4d8e98 | |||
| 1e28ae6d2a | |||
| 16343a2936 | |||
| 18cc2183cc | |||
| 5e711c799b | |||
| ca4b9d3a60 | |||
| ebd03ffe2a | |||
| ee4f0673f1 | |||
| 33145f2e71 | |||
| 721fd84a7e | |||
| a06fd7dfd4 | |||
| bd6bb4df06 | |||
| bb4dc3c3a5 | |||
| 7b7fe7ccd8 | |||
| 316679ae68 | |||
| b34cae6db4 | |||
| 52e31394ab | |||
| 84f18e03fa | |||
| 30761594ec | |||
| 87258a2439 | |||
| 6e31e928a9 | |||
| d0587c81f8 | |||
| 29018f06b9 | |||
| 171cb6b3d3 | |||
| 3e37c27310 | |||
| 06c3ebe2d4 | |||
| d336cd58b1 | |||
| 684f67a8e0 | |||
| a7b23613d3 | |||
| a6db1a9ee3 | |||
| 0f065b2c27 | |||
| 7399e37bba | |||
| 7ce4c821e5 | |||
| 68dd96128a | |||
| 6e55149281 | |||
| 3aca890da6 | |||
| 4798d44110 | |||
| 37b2b40aa1 | |||
| f9e287b12c | |||
| 96dc9e398b | |||
| 2f0157dd33 | |||
| f608712308 | |||
| 3807f216fe | |||
| 6b1a5374ae | |||
| 71b7158ff9 | |||
| 26f06d768c | |||
| 383a40bc03 | |||
| 8413fccdb5 | |||
| 3161703d8a | |||
| 4c097d89c5 | |||
| 0b50ae3e61 | |||
| 063583bb6f | |||
| f2da9f2d0a | |||
| fc7fa8a354 | |||
| 709a970cdc | |||
| 5d7814897c | |||
| 1e2ee001a8 | |||
| 11423fc25c | |||
| 160fc31b47 | |||
| 3389e0dc4a | |||
| ae8162974e | |||
| b33269a535 | |||
| d8280b6ad9 | |||
| 935cc8f675 | |||
| 3bd6e9c84c | |||
| f1b9114af0 | |||
| 95e5b169c8 | |||
| 09791a3b3c | |||
| c7a5642e48 | |||
| 6c23972620 | |||
| 211af98aaf | |||
| bc404572cb | |||
| b824ac710b | |||
| 372cd80175 | |||
| d0c9f95c7e | |||
| 91a06f2390 | |||
| ffd333164b | |||
| a6e8bb39b8 | |||
| 8d87fbd56a | |||
| a129ddba27 | |||
| 211b9fc2f2 | |||
| 8b6bb6999d | |||
| 524cb062a2 | |||
| be71989e45 | |||
| edb5a8ddb2 | |||
| fb461c0344 | |||
| a8608d0170 | |||
| 628f69605c | |||
| 537770b39d | |||
| 44eca8a550 | |||
| 3a79e46a4a | |||
| 6f56a8c539 | |||
| 2ac28aa2de | |||
| 53c55b2d7a | |||
| 31e0fa898a | |||
| 1ec46a00e2 | |||
| d98fbbbe4d | |||
| ef02ec9ab1 | |||
| 72fe122005 | |||
| 4b960bc320 | |||
| 090ea1a3f4 | |||
| 983f2349e0 | |||
| 8af98b4a52 | |||
| d821803050 | |||
| 35ead0155e | |||
| 5b908f5358 | |||
| 099fea6624 | |||
| a2ef0b5a68 | |||
| 38c17e06bb | |||
| c7c0c04b66 | |||
| bc7364acca | |||
| 1e9d265403 | |||
| 02689c5533 | |||
| d0f6da2a51 | |||
| 2e9a858e3d | |||
| 8e46204617 | |||
| b844e479a1 | |||
| f7ccf14211 | |||
| 814ddb194a | |||
| d7103412e5 | |||
| ad4c413ae4 | |||
| f0ed5d7c28 | |||
| e8304bb77d | |||
| 5ae10f17c5 | |||
| 4b8de11046 | |||
| 27f94052e9 | |||
| 8004d038ab | |||
| ff9d7346ec | |||
| c0298dc7b7 | |||
| ab0f4f8288 | |||
| 148fa29875 | |||
| f13c42c078 | |||
| a079605049 | |||
| 56d98abe64 | |||
| dab5a36a58 | |||
| 65e80556e8 | |||
| b60406f960 | |||
| 4d2c2c5f2a | |||
| deb7c1bba7 | |||
| 8b1134dc75 | |||
| a83da7367c | |||
| ec3ba2ebc8 | |||
| ab4f828865 | |||
| c72e1b6635 | |||
| 4e245f8471 | |||
| 652362b0c2 | |||
| abd6ffdbf5 | |||
| 0dc1c904ed | |||
| a8bdb4a288 | |||
| 9ec2712728 | |||
| 64609575e7 | |||
| 09411967e8 | |||
| fbec25412b | |||
| ffe51591e8 | |||
| f11ef7ba0b | |||
| 0439674aa9 | |||
| e609f8dadb | |||
| 6af2e1d162 | |||
| 35e46a5b60 | |||
| 14ca7602e9 | |||
| 7c719f3af8 | |||
| 547965b528 | |||
| 85a425bf3e | |||
| d9d9344fcc | |||
| 67fb6b23da | |||
| 2a4391af97 | |||
| fec3aa2e2f | |||
| 4fcc0accc7 | |||
| 3da90128f0 | |||
| 038556cc58 | |||
| f42ff8e4cf | |||
| d073049ed2 | |||
| b165d076f1 | |||
| c16e1b8fb1 | |||
| a527c1086a | |||
| 6be082e728 | |||
| f99cdba6dd | |||
| 397ace5c11 | |||
| 924c76f11b | |||
| d875933412 | |||
| c5a81dcc71 | |||
| 28a9fe85d5 | |||
| 29d5cef912 | |||
| 0e935a50e7 | |||
| 61de9aaea6 | |||
| 83a533a0dc | |||
| 2e7da95e5b | |||
| 93a7667c83 | |||
| 51f78cef9a | |||
| bf7581e8e3 | |||
| aa7a5964f2 | |||
| 30c0a5f4b1 | |||
| a3c3b1cd27 | |||
| cc775dcdc0 | |||
| 5d9c21c2a5 | |||
| 9a19607f3f | |||
| 3d0eeb01f8 | |||
| 310ec3a574 | |||
| 96d12df021 | |||
| f382d6f3a5 | |||
| 6de99fd964 | |||
| bc76c7ffb8 | |||
| 20ed8da853 | |||
| aaf10bfdbf | |||
| 36b9020f36 | |||
| 5ec8fcf350 | |||
| 194c6dc0fd | |||
| aadec58087 | |||
| 1b1883aa30 | |||
| 020a0bc882 | |||
| f2bf75e85e | |||
| 957f42698e | |||
| fd436dcd9b | |||
| 9e32b64d57 | |||
| 7d39cb5f6b | |||
| a8cd419b08 | |||
| c20d941a77 | |||
| 93741ac50e | |||
| f83739fdb9 | |||
| daf91112f5 | |||
| d406b3a5c2 | |||
| 09b8ae0fed | |||
| 683a456b5c | |||
| 265af0d720 | |||
| 30f6636a95 | |||
| 03980afe98 | |||
| a3f2c28c13 | |||
| 2850887c71 | |||
| 8a403407cb | |||
| 73f1f464d8 | |||
| 16c06cd997 | |||
| 0f1a59641a | |||
| bd62ab2f92 | |||
| 4727fb25ce | |||
| 0947970514 | |||
| 04b081bbd7 | |||
| 3a6bb19cad | |||
| 77650f328b | |||
| 424a08c4a0 | |||
| 18d96ce0ce | |||
| 60f4d64af6 | |||
| 5664ecbd0c | |||
| 4fa74c0d38 | |||
| 6b3780f6d1 | |||
| 7d90638188 | |||
| 28c2591e0b | |||
| 2b58550d36 | |||
| 22e619d0ea | |||
| a32b067057 | |||
| 482f848281 | |||
| 6f97f8a84b | |||
| 0168efd737 | |||
| bb1d0ab0d9 | |||
| 0fae45e8df | |||
| 34203fcbce | |||
| e64fe9d36e | |||
| 11c5000b5b | |||
| 1e5046c679 | |||
| 9344f7f20d | |||
| b724b0d3eb | |||
| 6703f0c068 | |||
| 88345d7cb5 | |||
| 266cd69348 | |||
| 1ae5318af0 | |||
| 992b79384f | |||
| f9fa6229b2 | |||
| 2c5b988de0 | |||
| feac39e657 | |||
| 1fa4419da1 | |||
| f49f8b6495 | |||
| c122f8a9d7 | |||
| 91dd25f7b1 | |||
| 0d1cdb7a38 | |||
| 9238504db9 | |||
| 774a3a653e | |||
| 497155b84c | |||
| b03cc5862b | |||
| b76a6109cf | |||
| 97d8b7cf23 | |||
| 48158e05cb | |||
| 20890227c9 | |||
| ed86125d87 | |||
| 4b8795bfea | |||
| 07a234e0f2 | |||
| bf42123576 | |||
| 37016ccf9f | |||
| 47b8b93c2c | |||
| c5f744870f | |||
| 993a273d76 | |||
| 6ee93cdbc4 | |||
| 9a57f5de5e | |||
| 94f365f564 | |||
| 01515de17d | |||
| f074d6017d | |||
| 5b96be04b4 | |||
| de7951074a | |||
| 990b3c7084 | |||
| 7de013f467 | |||
| ba417935f0 | |||
| a77988a984 | |||
| 5b8d0957b3 | |||
| a913745b8f | |||
| 2f7e3da138 | |||
| 13ce232455 | |||
| a67b4847ca | |||
| 49beea6cc8 | |||
| ee81540c64 | |||
| ccd3492e1e | |||
| 0a37c34704 | |||
| 60c2805c01 | |||
| debfb16520 | |||
| 76782b6ff1 | |||
| 1ccb0e8123 | |||
| d9cf32f0ac | |||
| 521e691138 | |||
| be9d89e0cc | |||
| b93b57c83e | |||
| 6bad17e353 | |||
| eaabfb8250 | |||
| 1001c5b4c4 | |||
| 2441cbfde6 | |||
| b2b6df7a7e | |||
| d954f6d905 | |||
| 3269f048b3 | |||
| 5c07a74c55 | |||
| 744a21ec57 | |||
| c55711a1d9 | |||
| ddcc804518 | |||
| 56f983fd4e | |||
| d85751caf0 | |||
| 79a5064072 | |||
| cb083f3836 | |||
| 3677f3be4b | |||
| fc835be1e6 | |||
| 40fdd27698 | |||
| d46de5b819 | |||
| 9627e9981d | |||
| 5852f17bed | |||
| 80c4566baf | |||
| 8b286a2179 | |||
| 9abeb86168 | |||
| 0963e67325 | |||
| 8dba18b67c | |||
| 4e56f11d06 | |||
| 39f13f01bc | |||
| 2ed9ea85c6 | |||
| 61b5a0cbe9 | |||
| c3831bd799 | |||
| 53deb48e2d | |||
| 2eac309f86 | |||
| 558f8010eb | |||
| 36e3697fd6 | |||
| 42f1c5d45d | |||
| 00c4940352 | |||
| 3075ded77f | |||
| 94fd697148 | |||
| a3df278f7d | |||
| 135a0456a2 |
-101
@@ -1,101 +0,0 @@
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
*/fastlane/report.xml
|
||||
*/fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
# Ruby / CocoaPods
|
||||
/ios/Pods/
|
||||
/vendor/bundle/
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
junit.xml
|
||||
artifacts
|
||||
|
||||
# Web bundle outputs
|
||||
/dist/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# gitignore and github actions
|
||||
!.gitignore
|
||||
!.github
|
||||
|
||||
|
||||
# Android & iOS folders
|
||||
/android/
|
||||
/ios/
|
||||
|
||||
# environment variables
|
||||
.env.*
|
||||
|
||||
# Firebase (Android) Google services
|
||||
# INCLUDED: google-services.json
|
||||
@@ -1,2 +0,0 @@
|
||||
[*.{kt,kts}]
|
||||
indent_size=2
|
||||
@@ -1,8 +0,0 @@
|
||||
# Copy this to `.env` and `.env.test` files
|
||||
|
||||
SENTRY_AUTH_TOKEN=
|
||||
EXPO_PUBLIC_ENV=development
|
||||
EXPO_PUBLIC_LOG_LEVEL=debug
|
||||
EXPO_PUBLIC_LOG_DEBUG=
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
|
||||
EXPO_PUBLIC_BUNDLE_DATE=0
|
||||
+10
-100
@@ -1,111 +1,21 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'@react-native-community',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-native-a11y/ios',
|
||||
'prettier',
|
||||
],
|
||||
extends: '@react-native-community',
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'react',
|
||||
'lingui',
|
||||
'simple-import-sort',
|
||||
'bsky-internal',
|
||||
'eslint-plugin-react-compiler',
|
||||
],
|
||||
rules: {
|
||||
// Temporary until https://github.com/facebook/react-native/pull/43756 gets into a release.
|
||||
'prettier/prettier': 0,
|
||||
'react/no-unescaped-entities': 0,
|
||||
'react-native/no-inline-styles': 0,
|
||||
'bsky-internal/avoid-unwrapped-text': [
|
||||
'error',
|
||||
{
|
||||
impliedTextComponents: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'],
|
||||
impliedTextProps: [],
|
||||
suggestedTextWrappers: {
|
||||
Button: 'ButtonText',
|
||||
'ToggleButton.Button': 'ToggleButton.ButtonText',
|
||||
},
|
||||
},
|
||||
],
|
||||
'bsky-internal/use-exact-imports': 'error',
|
||||
'bsky-internal/use-typed-gates': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'warn',
|
||||
{
|
||||
groups: [
|
||||
// Side effect imports.
|
||||
['^\\u0000'],
|
||||
// Node.js builtins prefixed with `node:`.
|
||||
['^node:'],
|
||||
// Packages.
|
||||
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
|
||||
// React/React Native priortized, followed by expo
|
||||
// Followed by all packages excluding unprefixed relative ones
|
||||
[
|
||||
'^(react\\/(.*)$)|^(react$)|^(react-native(.*)$)',
|
||||
'^(expo(.*)$)|^(expo$)',
|
||||
'^(?!(?:alf|components|lib|locale|logger|platform|screens|state|view)(?:$|\\/))@?\\w',
|
||||
],
|
||||
// Relative imports.
|
||||
// Ideally, anything that starts with a dot or #
|
||||
// due to unprefixed relative imports being used, we whitelist the relative paths we use
|
||||
// (?:$|\\/) matches end of string or /
|
||||
[
|
||||
'^(?:#\\/)?(?:lib|state|logger|platform|locale)(?:$|\\/)',
|
||||
'^(?:#\\/)?view(?:$|\\/)',
|
||||
'^(?:#\\/)?screens(?:$|\\/)',
|
||||
'^(?:#\\/)?alf(?:$|\\/)',
|
||||
'^(?:#\\/)?components(?:$|\\/)',
|
||||
'^#\\/',
|
||||
'^\\.',
|
||||
],
|
||||
// anything else - hopefully we don't have any of these
|
||||
['^'],
|
||||
],
|
||||
},
|
||||
],
|
||||
'simple-import-sort/exports': 'warn',
|
||||
// TODO: Reenable when we figure out why it gets stuck on CI.
|
||||
// 'react-compiler/react-compiler': 'error',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@atproto/api',
|
||||
importNames: ['moderatePost'],
|
||||
message:
|
||||
'Please use `moderatePost_wrapped` from `#/lib/moderatePost_wrapped` instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: ['@typescript-eslint'],
|
||||
ignorePatterns: [
|
||||
'**/__mocks__/*.ts',
|
||||
'src/platform/polyfills.ts',
|
||||
'src/third-party',
|
||||
'ios',
|
||||
'android',
|
||||
'coverage',
|
||||
'*.lock',
|
||||
'.husky',
|
||||
'patches',
|
||||
'bskyweb',
|
||||
'*.html',
|
||||
'bskyweb',
|
||||
'src/locale/locales/_build/',
|
||||
'src/locale/locales/**/*.js',
|
||||
],
|
||||
settings: {
|
||||
componentWrapperFunctions: ['observer'],
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.js', '*.mjs', '*.ts', '*.tsx'],
|
||||
rules: {
|
||||
semi: [2, 'never'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1.
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Details**
|
||||
|
||||
- Platform: <!-- desktop chrome windows, mobile safari, iOS, Android -->
|
||||
- Platform version:
|
||||
- App version:
|
||||
|
||||
**Additional context**
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature-request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
@@ -1,62 +0,0 @@
|
||||
name: build-and-push-bskyweb-aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}
|
||||
IMAGE_NAME: bskyweb
|
||||
|
||||
jobs:
|
||||
bskyweb-container-aws:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ env.USERNAME}}
|
||||
password: ${{ env.PASSWORD }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
|
||||
- name: Set outputs
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: ./Dockerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.vars.outputs.sha_short }}
|
||||
@@ -1,57 +0,0 @@
|
||||
name: build-and-push-bskyweb-ghcr
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- jake/bskyweb-additions
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
USERNAME: ${{ github.actor }}
|
||||
PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
bskyweb-container-ghcr:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ env.USERNAME }}
|
||||
password: ${{ env.PASSWORD }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=bskyweb:,suffix=,format=long
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: ./Dockerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -1,57 +0,0 @@
|
||||
name: build-and-push-embedr-aws
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- bnewbold/embedr
|
||||
- bnewbold/embedr-rebase
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}
|
||||
IMAGE_NAME: embed
|
||||
|
||||
jobs:
|
||||
embedr-container-aws:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ env.USERNAME}}
|
||||
password: ${{ env.PASSWORD }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: ./Dockerfile.embedr
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -1,53 +0,0 @@
|
||||
name: build-and-push-link-aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}
|
||||
IMAGE_NAME: bskylink
|
||||
|
||||
jobs:
|
||||
link-container-aws:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ env.USERNAME}}
|
||||
password: ${{ env.PASSWORD }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: ./Dockerfile.bskylink
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -1,53 +0,0 @@
|
||||
name: build-and-push-ogcard-aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
PASSWORD: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_PASSWORD }}
|
||||
IMAGE_NAME: bskyogcard
|
||||
|
||||
jobs:
|
||||
ogcard-container-aws:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Docker buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ env.USERNAME}}
|
||||
password: ${{ env.PASSWORD }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: ./Dockerfile.bskyogcard
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -1,166 +0,0 @@
|
||||
---
|
||||
name: Build and Submit Android
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
profile:
|
||||
type: choice
|
||||
description: Build profile to use
|
||||
options:
|
||||
- testflight-android
|
||||
- production
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Submit Android
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup EAS local builds
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive
|
||||
|
||||
- name: ✍️ Rename Testflight bundle
|
||||
if: ${{ inputs.profile != 'production' }}
|
||||
run: mv build.aab build.apk
|
||||
|
||||
- name: ⏰ Get a timestamp
|
||||
id: timestamp
|
||||
uses: nanzm/get-time-action@master
|
||||
with:
|
||||
format: 'MM-DD-HH-mm-ss'
|
||||
|
||||
- name: 🚀 Upload Production Artifact
|
||||
id: upload-artifact-production
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 30
|
||||
compression-level: 6
|
||||
name: build-${{ steps.timestamp.outputs.time }}.aab
|
||||
path: build.aab
|
||||
|
||||
- name: 🚀 Upload Testflight Artifact
|
||||
id: upload-artifact-testflight
|
||||
if: ${{ inputs.profile != 'production' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 30
|
||||
compression-level: 6
|
||||
name: build-${{ steps.timestamp.outputs.time }}.apk
|
||||
path: build.apk
|
||||
|
||||
- name: 📚 Get version from package.json
|
||||
id: get-build-info
|
||||
run: bash scripts/setGitHubOutput.sh
|
||||
|
||||
- name: 🔔 Notify Slack of Production Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Android production build for Google Play Store submission is ready!\n```Artifact: ${{ steps.upload-artifact-production.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: 🔔 Notify Slack of Testflight Build
|
||||
if: ${{ inputs.profile != 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact-testflight.outputs.artifact-url }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: 🏗️ Build Production APK
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
run: yarn use-build-number-with-bump eas build -p android --profile production-apk --local --output build.apk --non-interactive
|
||||
|
||||
- name: 🚀 Upload Production APK Artifact
|
||||
id: upload-artifact-production-apk
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 30
|
||||
compression-level: 6
|
||||
name: build-${{ steps.timestamp.outputs.time }}.apk
|
||||
path: build.apk
|
||||
|
||||
- name: 🔔 Notify Slack of Production APK Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Android production build for GitHub/Obtanium is ready!\n```Artifact: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
@@ -1,113 +0,0 @@
|
||||
---
|
||||
name: Build and Submit iOS
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
profile:
|
||||
type: choice
|
||||
description: Build profile to use
|
||||
options:
|
||||
- testflight
|
||||
- production
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup EAS local builds
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '15.3'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
version: 1.14.3
|
||||
|
||||
- name: 💾 Cache Pods
|
||||
uses: actions/cache@v3
|
||||
id: pods-cache
|
||||
with:
|
||||
path: ./ios/Pods
|
||||
# We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not
|
||||
# change unless the yarn version changes as well.
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
|
||||
- name: 📚 Get version from package.json
|
||||
id: get-build-info
|
||||
run: bash scripts/setGitHubOutput.sh
|
||||
|
||||
- name: 🔔 Notify Slack of Production Build
|
||||
if: ${{ inputs.profile == 'production' }}
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }}```"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.profile == 'testflight' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
@@ -1,317 +0,0 @@
|
||||
---
|
||||
name: Bundle and Deploy EAS Update
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
type: choice
|
||||
description: Deployment channel to use
|
||||
options:
|
||||
- testflight
|
||||
- production
|
||||
runtimeVersion:
|
||||
type: string
|
||||
description: Runtime version (in x.x.x format) that this update is for
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
bundleDeploy:
|
||||
name: Bundle and Deploy EAS Update
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
|
||||
cancel-in-progress: true
|
||||
outputs:
|
||||
changes-detected: ${{ steps.fingerprint.outputs.includes-changes }}
|
||||
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate the version if one is supplied. This should generally happen if the update is for a production client
|
||||
- name: 🧐 Validate version
|
||||
if: ${{ inputs.runtimeVersion }}
|
||||
run: |
|
||||
if [ -z "${{ inputs.runtimeVersion }}" ]; then
|
||||
[[ "${{ inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
|
||||
fi
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⬇️ Fetch commits from base branch
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
run: git fetch origin main:main --depth 100
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 📷 Check fingerprint and install dependencies
|
||||
id: fingerprint
|
||||
uses: bluesky-social/github-actions/fingerprint-native@main
|
||||
with:
|
||||
profile: ${{ inputs.channel || 'testflight' }}
|
||||
previous-commit-tag: ${{ inputs.runtimeVersion }}
|
||||
|
||||
- name: Lint check
|
||||
run: yarn lint
|
||||
|
||||
- name: Prettier check
|
||||
run: yarn prettier --check .
|
||||
|
||||
- name: Check & compile i18n
|
||||
run: yarn intl:build
|
||||
|
||||
- name: Type check
|
||||
run: yarn typecheck
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup Expo
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ Create Bundle
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
|
||||
|
||||
- name: 📦 Package Bundle and 🚀 Deploy
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: yarn use-build-number bash scripts/bundleUpdate.sh
|
||||
env:
|
||||
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
|
||||
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
||||
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
# GitHub actions are horrible so let's just copy paste this in
|
||||
buildIfNecessaryIOS:
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-14
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
||||
cancel-in-progress: true
|
||||
needs: [bundleDeploy]
|
||||
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||
# available here
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup EAS local builds
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '15.3'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
version: 1.14.3
|
||||
|
||||
- name: 💾 Cache Pods
|
||||
uses: actions/cache@v3
|
||||
id: pods-cache
|
||||
with:
|
||||
path: ./ios/Pods
|
||||
# We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not
|
||||
# change unless the yarn version changes as well.
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive
|
||||
|
||||
- name: 🚀 Deploy
|
||||
run: eas submit -p ios --non-interactive --path build.ipa
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.channel == 'testflight' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.channel == 'testflight' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
|
||||
buildIfNecessaryAndroid:
|
||||
name: Build and Submit Android
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-android
|
||||
cancel-in-progress: false
|
||||
needs: [ bundleDeploy ]
|
||||
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||
# available here
|
||||
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
|
||||
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 5
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup EAS local builds
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build
|
||||
|
||||
- name: ✏️ Write environment variables
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive
|
||||
|
||||
- name: ⏰ Get a timestamp
|
||||
id: timestamp
|
||||
uses: nanzm/get-time-action@master
|
||||
with:
|
||||
format: 'MM-DD-HH-mm-ss'
|
||||
|
||||
- name: 🚀 Upload Artifact
|
||||
id: upload-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
name: build-${{ steps.timestamp.outputs.time }}.apk
|
||||
path: build.apk
|
||||
|
||||
- name: 🔔 Notify Slack
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||
|
||||
- name: ⬇️ Restore Cache
|
||||
id: get-base-commit
|
||||
uses: actions/cache@v4
|
||||
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
|
||||
with:
|
||||
path: most-recent-testflight-commit.txt
|
||||
key: most-recent-testflight-commit
|
||||
|
||||
- name: ✏️ Write commit hash to cache
|
||||
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
|
||||
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
|
||||
@@ -1,43 +0,0 @@
|
||||
name: golang
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Git Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Go tooling
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.22'
|
||||
- name: Dummy Static Files
|
||||
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/media/blah.txt
|
||||
- name: Check
|
||||
run: cd bskyweb/ && make check
|
||||
- name: Build (binary)
|
||||
run: cd bskyweb/ && make build
|
||||
- name: Test
|
||||
run: cd bskyweb/ && make test
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Git Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Go tooling
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.22'
|
||||
- name: Dummy Static Files
|
||||
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/media/blah.txt
|
||||
- name: Lint
|
||||
run: cd bskyweb/ && make lint
|
||||
+14
-29
@@ -2,12 +2,8 @@ name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
linting:
|
||||
@@ -15,39 +11,28 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
- name: Yarn install
|
||||
uses: Wandalen/wretry.action@master
|
||||
run: yarn
|
||||
- name: Lint Reporter
|
||||
uses: wearerequired/lint-action@v2.2.0
|
||||
with:
|
||||
command: yarn --frozen-lockfile
|
||||
attempt_limit: 3
|
||||
attempt_delay: 2000
|
||||
- name: Lint check
|
||||
eslint: true
|
||||
prettier: true
|
||||
- name: Typescript & Lint check
|
||||
run: yarn lint
|
||||
- name: Prettier check
|
||||
run: yarn prettier --check .
|
||||
- name: Check & compile i18n
|
||||
run: yarn intl:build
|
||||
- name: Type check
|
||||
run: yarn typecheck
|
||||
testing:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install node 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
uses: actions/checkout@v2
|
||||
- name: Yarn install
|
||||
uses: Wandalen/wretry.action@master
|
||||
with:
|
||||
command: yarn --frozen-lockfile
|
||||
attempt_limit: 3
|
||||
attempt_delay: 2000
|
||||
- name: Check & compile i18n
|
||||
run: yarn intl:build
|
||||
run: yarn
|
||||
- name: Run tests
|
||||
run: |
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test --forceExit
|
||||
yarn test --forceExit
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
# Credit for fingerprint action https://github.com/expo/expo
|
||||
# https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml
|
||||
---
|
||||
name: PR Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
webpack-analyzer:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: Ensure tracking relevant branches and checkout base
|
||||
run: |
|
||||
git checkout ${{ github.head_ref }}
|
||||
git checkout ${{ github.base_ref }}
|
||||
|
||||
- name: Get the base commit
|
||||
id: base-commit
|
||||
run: echo base-commit=$(git log -n 1 ${{ github.base_ref }} --pretty=format:'%H') >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Merge PR commit
|
||||
run: |
|
||||
# Have to set a git config for the merge to work
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git merge --no-edit ${{ github.head_ref }}
|
||||
yarn install
|
||||
|
||||
- name: 🔦 Generate stats file for PR
|
||||
run: |
|
||||
yarn generate-webpack-stats-file
|
||||
mv stats.json ../stats-new.json
|
||||
|
||||
- name: ⬇️ Get base stats from cache
|
||||
id: get-base-stats
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: stats-base.json
|
||||
key: stats-base-${{ steps.base-commit.outputs.base-commit }}
|
||||
|
||||
- name: Restore to base commit
|
||||
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
|
||||
run: |
|
||||
git reset HEAD~
|
||||
git restore .
|
||||
|
||||
- name: 🔦 Generate stats file from base commit
|
||||
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
|
||||
run: |
|
||||
yarn install
|
||||
yarn generate-webpack-stats-file
|
||||
mv stats.json stats-base.json
|
||||
|
||||
- name: % Get diff
|
||||
id: get-diff
|
||||
uses: NejcZdovc/bundle-size-diff@v1
|
||||
with:
|
||||
base_path: 'stats-base.json'
|
||||
pr_path: '../stats-new.json'
|
||||
excluded_assets: '(.+).chunk.js|(.+).js.map|(.+).json|(.+).png'
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: bundle-diff
|
||||
message: |
|
||||
| Old size | New size | Diff |
|
||||
|----------|----------|-----------------------|
|
||||
| ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}%) |
|
||||
---
|
||||
|
||||
fingerprint-native:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
|
||||
steps:
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 100
|
||||
|
||||
- name: ⬇️ Fetch commits from base branch
|
||||
run: git fetch origin main:main --depth 100
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: 📷 Check fingerprint and install dependencies
|
||||
id: fingerprint
|
||||
uses: bluesky-social/github-actions/fingerprint-native@main
|
||||
with:
|
||||
profile: pull-request
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: ${{ steps.fingerprint.outputs.includes-changes }}
|
||||
with:
|
||||
header: fingerprint-diff
|
||||
message: |
|
||||
The Pull Request introduced fingerprint changes against the base commit:
|
||||
<details><summary>Fingerprint diff</summary>
|
||||
|
||||
```json
|
||||
${{ steps.fingerprint.outputs.diff }}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
|
||||
- name: 💬 Delete comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
with:
|
||||
header: fingerprint-diff
|
||||
delete: true
|
||||
|
||||
+2
-57
@@ -18,6 +18,7 @@ xcuserdata
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
|
||||
# Android/IntelliJ
|
||||
@@ -61,60 +62,4 @@ buck-out/
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
junit.xml
|
||||
artifacts
|
||||
|
||||
# Web bundle outputs
|
||||
/dist/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
stats.json
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# gitignore and github actions
|
||||
!.gitignore
|
||||
!.github
|
||||
|
||||
|
||||
# Android & iOS folders
|
||||
/android/
|
||||
/ios/
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Firebase (Android) Google services
|
||||
google-services.json
|
||||
|
||||
# Performance results (Flashlight)
|
||||
.perf/
|
||||
|
||||
# ESLint
|
||||
.eslintcache
|
||||
|
||||
# i18n
|
||||
src/locale/locales/_build/
|
||||
src/locale/locales/**/*.js
|
||||
|
||||
# local builds
|
||||
*.apk
|
||||
*.aab
|
||||
*.ipa
|
||||
|
||||
# ogcard assets
|
||||
bskyogcard/src/assets/fonts/noto-*
|
||||
junit.xml
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
+4
-14
@@ -1,15 +1,5 @@
|
||||
# Ignore everything except JS-ey code.
|
||||
# Based on https://stackoverflow.com/a/70715829/458193
|
||||
*
|
||||
!**/*.js
|
||||
!**/*.jsx
|
||||
!**/*.ts
|
||||
!**/*.tsx
|
||||
!*/
|
||||
|
||||
# More specific ignores go below.
|
||||
.expo
|
||||
android
|
||||
ios
|
||||
src/locale/locales
|
||||
lib/react-compiler-runtime
|
||||
android
|
||||
src/third-party
|
||||
src/app.json
|
||||
public
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
FROM golang:1.22-bullseye AS build-env
|
||||
|
||||
WORKDIR /usr/src/social-app
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Node
|
||||
ENV NODE_VERSION=18
|
||||
ENV NVM_DIR=/usr/share/nvm
|
||||
|
||||
# Go
|
||||
ENV GODEBUG="netdns=go"
|
||||
ENV GOOS="linux"
|
||||
ENV GOARCH="amd64"
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOEXPERIMENT="loopvar"
|
||||
|
||||
# Expo
|
||||
ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
|
||||
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER ${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev}
|
||||
|
||||
COPY . .
|
||||
|
||||
#
|
||||
# Generate the JavaScript webpack.
|
||||
#
|
||||
RUN mkdir --parents $NVM_DIR && \
|
||||
wget \
|
||||
--output-document=/tmp/nvm-install.sh \
|
||||
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh && \
|
||||
bash /tmp/nvm-install.sh
|
||||
|
||||
RUN \. "$NVM_DIR/nvm.sh" && \
|
||||
nvm install $NODE_VERSION && \
|
||||
nvm use $NODE_VERSION && \
|
||||
echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env && \
|
||||
npm install --global yarn && \
|
||||
yarn && \
|
||||
yarn intl:build && \
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER EXPO_PUBLIC_BUNDLE_DATE=$() yarn build-web
|
||||
|
||||
# DEBUG
|
||||
RUN find ./bskyweb/static && find ./web-build/static
|
||||
|
||||
#
|
||||
# Generate the bskyweb Go binary.
|
||||
#
|
||||
RUN cd bskyweb/ && \
|
||||
go mod download && \
|
||||
go mod verify
|
||||
|
||||
RUN cd bskyweb/ && \
|
||||
go build \
|
||||
-v \
|
||||
-trimpath \
|
||||
-tags timetzdata \
|
||||
-o /bskyweb \
|
||||
./cmd/bskyweb
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
ENV GODEBUG=netdns=go
|
||||
ENV TZ=Etc/UTC
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install --yes \
|
||||
dumb-init \
|
||||
ca-certificates
|
||||
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
WORKDIR /bskyweb
|
||||
COPY --from=build-env /bskyweb /usr/bin/bskyweb
|
||||
|
||||
CMD ["/usr/bin/bskyweb"]
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
|
||||
LABEL org.opencontainers.image.description="bsky.app Web App"
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
|
||||
# NOOP
|
||||
@@ -1,41 +0,0 @@
|
||||
FROM node:20.11-alpine3.18 as build
|
||||
|
||||
# Move files into the image and install
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./bskylink/package.json ./
|
||||
COPY ./bskylink/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY ./bskylink ./
|
||||
|
||||
# build then prune dev deps
|
||||
RUN yarn build
|
||||
RUN yarn install --production --ignore-scripts --prefer-offline
|
||||
|
||||
# Uses assets from build stage to reduce build size
|
||||
FROM node:20.11-alpine3.18
|
||||
|
||||
RUN apk add --update dumb-init
|
||||
|
||||
# Avoid zombie processes, handle signal forwarding
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /app /app
|
||||
RUN mkdir /app/data && chown node /app/data
|
||||
|
||||
VOLUME /app/data
|
||||
EXPOSE 3000
|
||||
ENV LINK_PORT=3000
|
||||
ENV NODE_ENV=production
|
||||
# potential perf issues w/ io_uring on this version of node
|
||||
ENV UV_USE_IO_URING=0
|
||||
|
||||
# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user
|
||||
USER node
|
||||
CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "dist/bin.js"]
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
|
||||
LABEL org.opencontainers.image.description="Bsky Link Service"
|
||||
LABEL org.opencontainers.image.licenses=UNLICENSED
|
||||
@@ -1,41 +0,0 @@
|
||||
FROM node:20.11-alpine3.18 as build
|
||||
|
||||
# Move files into the image and install
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./bskyogcard/package.json ./
|
||||
COPY ./bskyogcard/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY ./bskyogcard ./
|
||||
|
||||
# build then prune dev deps
|
||||
RUN yarn install-fonts && yarn build
|
||||
RUN yarn install --production --ignore-scripts --prefer-offline
|
||||
|
||||
# Uses assets from build stage to reduce build size
|
||||
FROM node:20.11-alpine3.18
|
||||
|
||||
RUN apk add --update dumb-init
|
||||
|
||||
# Avoid zombie processes, handle signal forwarding
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /app /app
|
||||
RUN mkdir /app/data && chown node /app/data
|
||||
|
||||
VOLUME /app/data
|
||||
EXPOSE 3000
|
||||
ENV CARD_PORT=3000
|
||||
ENV NODE_ENV=production
|
||||
# potential perf issues w/ io_uring on this version of node
|
||||
ENV UV_USE_IO_URING=0
|
||||
|
||||
# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user
|
||||
USER node
|
||||
CMD ["node", "--heapsnapshot-signal=SIGUSR2", "--enable-source-maps", "dist/bin.js"]
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
|
||||
LABEL org.opencontainers.image.description="Bsky Card Service"
|
||||
LABEL org.opencontainers.image.licenses=UNLICENSED
|
||||
@@ -1,79 +0,0 @@
|
||||
FROM golang:1.22-bullseye AS build-env
|
||||
|
||||
WORKDIR /usr/src/social-app
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Node
|
||||
ENV NODE_VERSION=18
|
||||
ENV NVM_DIR=/usr/share/nvm
|
||||
|
||||
# Go
|
||||
ENV GODEBUG="netdns=go"
|
||||
ENV GOOS="linux"
|
||||
ENV GOARCH="amd64"
|
||||
ENV CGO_ENABLED=1
|
||||
ENV GOEXPERIMENT="loopvar"
|
||||
|
||||
COPY . .
|
||||
|
||||
#
|
||||
# Generate the JavaScript webpack. NOTE: this will change
|
||||
#
|
||||
RUN mkdir --parents $NVM_DIR && \
|
||||
wget \
|
||||
--output-document=/tmp/nvm-install.sh \
|
||||
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh && \
|
||||
bash /tmp/nvm-install.sh
|
||||
|
||||
RUN \. "$NVM_DIR/nvm.sh" && \
|
||||
nvm install $NODE_VERSION && \
|
||||
nvm use $NODE_VERSION && \
|
||||
npm install --global yarn && \
|
||||
yarn && \
|
||||
cd bskyembed && yarn install --frozen-lockfile && cd .. && \
|
||||
yarn intl:build && \
|
||||
yarn build-embed
|
||||
|
||||
# DEBUG
|
||||
RUN find ./bskyweb/embedr-static && find ./bskyweb/embedr-templates && find ./bskyembed/dist
|
||||
|
||||
# hack around issue with empty directory and go:embed
|
||||
RUN touch bskyweb/static/js/empty.txt
|
||||
RUN touch bskyweb/static/media/empty.txt
|
||||
|
||||
#
|
||||
# Generate the embedr Go binary.
|
||||
#
|
||||
RUN cd bskyweb/ && \
|
||||
go mod download && \
|
||||
go mod verify
|
||||
|
||||
RUN cd bskyweb/ && \
|
||||
go build \
|
||||
-v \
|
||||
-trimpath \
|
||||
-tags timetzdata \
|
||||
-o /embedr \
|
||||
./cmd/embedr
|
||||
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
ENV GODEBUG=netdns=go
|
||||
ENV TZ=Etc/UTC
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install --yes \
|
||||
dumb-init \
|
||||
ca-certificates
|
||||
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
WORKDIR /embedr
|
||||
COPY --from=build-env /embedr /usr/bin/embedr
|
||||
|
||||
CMD ["/usr/bin/embedr"]
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/bluesky-social/social-app
|
||||
LABEL org.opencontainers.image.description="embed.bsky.app Web App"
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
@@ -1,7 +0,0 @@
|
||||
Copyright 2023–2024 Bluesky PBC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,42 +0,0 @@
|
||||
|
||||
SHELL = /bin/bash
|
||||
.SHELLFLAGS = -o pipefail -c
|
||||
|
||||
.PHONY: help
|
||||
help: ## Print info about all commands
|
||||
@echo "Commands:"
|
||||
@echo
|
||||
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[01;32m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
.PHONY: build-web
|
||||
build-web: ## Compile web bundle, copy to bskyweb directory
|
||||
yarn intl:build
|
||||
yarn build-web
|
||||
|
||||
.PHONY: build-web-embed
|
||||
build-web-embed: ## Compile web embed bundle, copy to bskyweb/embedr* directories
|
||||
yarn intl:build
|
||||
yarn build-embed
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run all tests
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run style checks and verify syntax
|
||||
yarn run lint
|
||||
|
||||
#.PHONY: fmt
|
||||
#fmt: ## Run syntax re-formatting
|
||||
# yarn prettier
|
||||
|
||||
.PHONY: deps
|
||||
deps: ## Installs dependent libs using 'yarn install'
|
||||
yarn install --frozen-lockfile
|
||||
cd bskyembed && yarn install --frozen-lockfile
|
||||
|
||||
.PHONY: nvm-setup
|
||||
nvm-setup: ## Use NVM to install and activate node+yarn
|
||||
nvm install 18
|
||||
nvm use 18
|
||||
npm install --global yarn
|
||||
@@ -1,74 +1,41 @@
|
||||
# Bluesky Social App
|
||||
# Social App
|
||||
|
||||
Welcome friends! This is the codebase for the Bluesky Social app.
|
||||
In-progress social app.
|
||||
|
||||
Get the app itself:
|
||||
Uses:
|
||||
|
||||
- **Web: [bsky.app](https://bsky.app)**
|
||||
- **iOS: [App Store](https://apps.apple.com/us/app/bluesky-social/id6444370199)**
|
||||
- **Android: [Play Store](https://play.google.com/store/apps/details?id=xyz.blueskyweb.app)**
|
||||
- [React Native](https://reactnative.dev)
|
||||
- [React Native for Web](https://necolas.github.io/react-native-web/)
|
||||
- [React Navigation](https://reactnative.dev/docs/navigation#react-navigation)
|
||||
- [MobX](https://mobx.js.org/README.html)
|
||||
- [Async Storage](https://github.com/react-native-async-storage/async-storage)
|
||||
|
||||
## Development Resources
|
||||
## Build instructions
|
||||
|
||||
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).
|
||||
- Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
|
||||
- After initial setup:
|
||||
- `cd ios ; pod install`
|
||||
- Start the dev servers
|
||||
- `git clone git@github.com:bluesky-social/atproto.git`
|
||||
- `cd atproto`
|
||||
- `yarn`
|
||||
- `cd packages/dev-env && yarn start`
|
||||
- Run the dev app
|
||||
- iOS: `yarn ios`
|
||||
- Android: `yarn android`
|
||||
- Web: `yarn web`
|
||||
- Tips
|
||||
- `npx react-native info` Checks what has been installed.
|
||||
- On M1 macs, [you need to exclude "arm64" from the target architectures](https://stackoverflow.com/a/65399525)
|
||||
- Annoyingly this must be re-set via XCode after every pod install
|
||||
- The android simulator won't be able to access localhost services unless you run `adb reverse tcp:{PORT} tcp:{PORT}`
|
||||
- For instance, the localhosted dev-wallet will need `adb reverse tcp:3001 tcp:3001`
|
||||
- For some reason, the typescript compiler chokes on platform-specific files (e.g. `foo.native.ts`) but only when compiling for Web thus far. Therefore we always have one version of the file which doesn't use a platform specifier, and that should bee the Web version. ([More info](https://stackoverflow.com/questions/44001050/platform-specific-import-component-in-react-native-with-typescript).)
|
||||
|
||||
There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application.
|
||||
## Various notes
|
||||
|
||||
The [Build Instructions](./docs/build.md) are a good place to get started with the app itself.
|
||||
### Polyfills
|
||||
|
||||
The Authenticated Transfer Protocol ("AT Protocol" or "atproto") is a decentralized social media protocol. You don't *need* to understand AT Protocol to work with this application, but it can help. Learn more at:
|
||||
`./platform/polyfills.*.ts` adds polyfills to the environment. Currently this includes:
|
||||
|
||||
- [Overview and Guides](https://atproto.com/guides/overview)
|
||||
- [Github Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions
|
||||
- [Protocol Specifications](https://atproto.com/specs/atp)
|
||||
- [Blogpost on self-authenticating data structures](https://bsky.social/about/blog/3-6-2022-a-self-authenticating-social-protocol)
|
||||
|
||||
The Bluesky Social application encompasses a set of schemas and APIs built in the overall AT Protocol framework. The namespace for these "Lexicons" is `app.bsky.*`.
|
||||
|
||||
## Contributions
|
||||
|
||||
> While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review.
|
||||
|
||||
**Rules:**
|
||||
|
||||
- We may not respond to your issue or PR.
|
||||
- We may close an issue or PR without much feedback.
|
||||
- We may lock discussions or contributions if our attention is getting DDOSed.
|
||||
- We're not going to provide support for build issues.
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- Check for existing issues before filing a new one please.
|
||||
- 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, 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!
|
||||
|
||||
## Forking guidelines
|
||||
|
||||
You have our blessing 🪄✨ to fork this application! However, it's very important to be clear to users when you're giving them a fork.
|
||||
|
||||
Please be sure to:
|
||||
|
||||
- Change all branding in the repository and UI to clearly differentiate from Bluesky.
|
||||
- Change any support links (feedback, email, terms of service, etc) to your own systems.
|
||||
- Replace any analytics or error-collection systems with your own so we don't get super confused.
|
||||
|
||||
## Security disclosures
|
||||
|
||||
If you discover any security issues, please send an email to security@bsky.app. The email is automatically CCed to the entire team and we'll respond promptly.
|
||||
|
||||
## 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 integration can be as seamless as first-party through custom feeds, federated services, clients, and more.
|
||||
|
||||
## License (MIT)
|
||||
|
||||
See [./LICENSE](./LICENSE) for the full license.
|
||||
|
||||
## P.S.
|
||||
|
||||
We ❤️ you and all of the ways you support us. Thank you for making Bluesky a great place!
|
||||
- TextEncoder / TextDecoder
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
flows:
|
||||
- "flows/*"
|
||||
@@ -1,30 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
# Post an image with the porn label
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Post with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn:
|
||||
id: "labelsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on porn"
|
||||
point: 78%,67%
|
||||
- tapOn:
|
||||
label: "Tap on confirm"
|
||||
point: 51%,92%
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- tapOn:
|
||||
id: "e2eRefreshHome"
|
||||
- assertVisible: "Adult Content"
|
||||
@@ -1,87 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Post text only"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Post with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Post with a https://example.com link card"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "e2eRefreshHome"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply text only"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply with a https://example.com link card"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
- tapOn:
|
||||
id: "quoteBtn"
|
||||
- inputText: "QP text only"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
- tapOn:
|
||||
id: "quoteBtn"
|
||||
- inputText: "QP with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
- tapOn:
|
||||
id: "quoteBtn"
|
||||
- inputText: "QP with a https://example.com link card"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
@@ -1,37 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ""
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eOpenLoggedOutView"
|
||||
- tapOn:
|
||||
id: "createAccountButton"
|
||||
- tapOn:
|
||||
id: "selectServiceButton"
|
||||
- tapOn:
|
||||
id: "customSelectBtn"
|
||||
- tapOn:
|
||||
id: "customServerTextInput"
|
||||
- inputText: "http://localhost:3000"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
- tapOn:
|
||||
id: "emailInput"
|
||||
- inputText: "example@test.com"
|
||||
- tapOn:
|
||||
id: "passwordInput"
|
||||
- inputText: "hunter2"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn:
|
||||
id: "handleInput"
|
||||
- inputText: "e2e-test"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&follows&posts"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- tapOn:
|
||||
label: "Create a curate list"
|
||||
id: "e2eGotoLists"
|
||||
- tapOn:
|
||||
id: "newUserListBtn"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
- inputText: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn: "About"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
|
||||
- tapOn:
|
||||
label: "Edit display name and description via the edit curatelist modal"
|
||||
point: "90%,9%"
|
||||
- tapOn: "Edit list details"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
- eraseText
|
||||
- inputText: "Bad Ppl"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- eraseText
|
||||
- inputText: "They bad"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertVisible: Bad Ppl
|
||||
- assertVisible: They bad
|
||||
|
||||
- tapOn:
|
||||
label: "Remove description via the edit curatelist modal"
|
||||
point: "90%,9%"
|
||||
- tapOn: "Edit list details"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- eraseText
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- assertNotVisible:
|
||||
id: "listDescription"
|
||||
|
||||
- tapOn:
|
||||
label: "Delete the curatelist"
|
||||
point: "90%,9%"
|
||||
- tapOn: "Delete List"
|
||||
- tapOn:
|
||||
id: "confirmBtn"
|
||||
- assertVisible:
|
||||
id: "listsEmpty"
|
||||
|
||||
- tapOn:
|
||||
label: "Create a new curatelist"
|
||||
id: "e2eGotoLists"
|
||||
- tapOn:
|
||||
id: "newUserListBtn"
|
||||
- assertVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
- inputText: "Good Ppl"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- inputText: "They good"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
- assertNotVisible:
|
||||
id: "createOrEditListModal"
|
||||
- tapOn: "About"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
- tapOn: "About"
|
||||
|
||||
- tapOn:
|
||||
label: "Adds users on curatelists from the list"
|
||||
id: "addUserBtn"
|
||||
- assertVisible:
|
||||
id: "listAddUserModal"
|
||||
- tapOn:
|
||||
id: "searchInput"
|
||||
- inputText: "b"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
id: "user-bob.test-addBtn"
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
- assertNotVisible:
|
||||
id: "listAddUserModal"
|
||||
- assertVisible:
|
||||
id: "user-bob.test"
|
||||
|
||||
- tapOn: "Posts"
|
||||
- assertVisible:
|
||||
label: "Shows posts by the users in the list"
|
||||
id: "feedItem-by-bob.test"
|
||||
|
||||
- tapOn:
|
||||
label: "Pins the list"
|
||||
id: "pinBtn"
|
||||
- tapOn:
|
||||
id: "e2eGotoHome"
|
||||
- tapOn: "Good Ppl"
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "e2eGotoFeeds"
|
||||
- tapOn:
|
||||
id: "saved-feed-Good Ppl"
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "unpinBtn"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- assertNotVisible:
|
||||
id: "homeScreenFeedTabs-Good Ppl"
|
||||
- tapOn:
|
||||
id: "e2eGotoLists"
|
||||
- tapOn: "Good Ppl"
|
||||
|
||||
- tapOn: "About"
|
||||
- assertVisible:
|
||||
label: "Removes users on curatelists from the list"
|
||||
id: "user-bob.test"
|
||||
- tapOn:
|
||||
point: "90%,43%"
|
||||
- assertVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
- tapOn:
|
||||
id: "user-bob.test-addBtn"
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
- assertNotVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
|
||||
- tapOn:
|
||||
label: "Shows the curatelist on my profile"
|
||||
id: "bottomBarProfileBtn"
|
||||
- swipe:
|
||||
from:
|
||||
id: "profilePager-selector"
|
||||
direction: LEFT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-6"
|
||||
- tapOn: "Good Ppl"
|
||||
|
||||
- tapOn:
|
||||
label: "Adds and removes users on curatelists from the profile"
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn:
|
||||
id: "searchTextInput"
|
||||
- inputText: "bob"
|
||||
- tapOn:
|
||||
id: "searchAutoCompleteResult-bob.test"
|
||||
- assertVisible:
|
||||
id: "profileView"
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownBtn"
|
||||
- tapOn: "Add to Lists"
|
||||
- assertVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
- tapOn:
|
||||
id: "user-bob.test-addBtn"
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
- assertNotVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownBtn"
|
||||
- tapOn: "Add to Lists"
|
||||
- assertVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
- tapOn:
|
||||
id: "user-bob.test-addBtn"
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
- assertNotVisible:
|
||||
id: "userAddRemoveListsModal"
|
||||
@@ -1,92 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users&follows&posts&feeds
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
# Pin alice's feed
|
||||
- tapOn:
|
||||
id: "bottomBarProfileBtn"
|
||||
- swipe:
|
||||
from:
|
||||
id: "profilePager-selector"
|
||||
direction: LEFT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-4"
|
||||
- tapOn: "alice-favs"
|
||||
- tapOn: "Pin to Home"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- assertNotVisible: "Feeds ✨"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "Following"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "alice-favs"
|
||||
|
||||
# Set alice-favs first
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on down arrow"
|
||||
point: "79%,23%"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "alice-favs"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "Following"
|
||||
|
||||
# Set following first
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on down arrow"
|
||||
point: "79%,23%"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "Following"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "alice-favs"
|
||||
|
||||
# Remove following
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on unpin"
|
||||
point: "91%,23%"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "alice-favs"
|
||||
- assertNotVisible: "Following"
|
||||
@@ -1,64 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users&follows&posts&feeds
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- tapOn:
|
||||
label: "Can go to feeds page using feeds button in tab bar"
|
||||
text: "Feeds ✨"
|
||||
- assertVisible: "Discover New Feeds"
|
||||
|
||||
- tapOn:
|
||||
label: "Feeds button disappears after pinning a feed"
|
||||
id: "bottomBarProfileBtn"
|
||||
- swipe:
|
||||
from:
|
||||
id: "profilePager-selector"
|
||||
direction: LEFT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-4"
|
||||
- tapOn: "alice-favs"
|
||||
- tapOn: "Pin to Home"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- assertNotVisible: "Feeds ✨"
|
||||
|
||||
- tapOn:
|
||||
label: "Can like posts"
|
||||
id: "likeBtn"
|
||||
- assertVisible:
|
||||
id: "likeCount"
|
||||
text: "1"
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
- assertNotVisible:
|
||||
id: "likeCount"
|
||||
|
||||
- tapOn:
|
||||
label: "Can repost posts"
|
||||
id: "repostBtn"
|
||||
- tapOn: "Repost"
|
||||
- assertVisible:
|
||||
id: "repostCount"
|
||||
text: "1"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
- tapOn: "Remove repost"
|
||||
- assertNotVisible:
|
||||
id: "repostCount"
|
||||
|
||||
- tapOn:
|
||||
label: "Can delete posts"
|
||||
id: "postDropdownBtn"
|
||||
childOf:
|
||||
id: "feedItem-by-alice.test"
|
||||
- tapOn: "Delete post"
|
||||
- tapOn: "Delete"
|
||||
@@ -1,26 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eOpenLoggedOutView"
|
||||
- tapOn: "Sign in"
|
||||
- tapOn:
|
||||
id: "selectServiceButton"
|
||||
- tapOn: "Custom"
|
||||
- tapOn:
|
||||
id: "customServerTextInput"
|
||||
- inputText: "http://localhost:3000"
|
||||
- tapOn: "Done"
|
||||
- tapOn:
|
||||
id: "loginUsernameInput"
|
||||
- inputText: "Alice"
|
||||
- tapOn:
|
||||
id: "loginPasswordInput"
|
||||
- inputText: "hunter2"
|
||||
- pressKey: Enter
|
||||
- assertVisible: "Following"
|
||||
@@ -1,45 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&follows&labels"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
# create a modlist
|
||||
- tapOn:
|
||||
id: "e2eGotoModeration"
|
||||
- tapOn:
|
||||
id: "moderationlistsBtn"
|
||||
- tapOn: "New"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
- inputText: "Muted Users"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- inputText: "Shhh"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
|
||||
# view modlist
|
||||
- assertVisible: "Muted Users"
|
||||
- assertVisible: "Shhh"
|
||||
|
||||
# toggle mute subscription
|
||||
- tapOn:
|
||||
point: "70%,9%"
|
||||
- tapOn: "Mute accounts"
|
||||
- tapOn: "Mute list"
|
||||
- tapOn: "Unmute"
|
||||
|
||||
# toggle block subscription
|
||||
- tapOn:
|
||||
point: "70%,9%"
|
||||
- tapOn: "Block accounts"
|
||||
- tapOn: "Block list"
|
||||
- tapOn: "Unblock"
|
||||
|
||||
# the rest of the behaviors are tested in curate-lists.yml
|
||||
@@ -1,32 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "e2eStartOnboarding"
|
||||
- tapOn: "Open avatar creator"
|
||||
- tapOn: "Select the zap emoji as your avatar"
|
||||
- tapOn:
|
||||
label: "Tap on yellow"
|
||||
point: "23%,79%"
|
||||
- tapOn: "Done"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Select an avatar"
|
||||
- tapOn: "Select the atom emoji as your avatar"
|
||||
- tapOn: "Done"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Continue to next step"
|
||||
- assertVisible: "What are your interests?"
|
||||
- tapOn:
|
||||
label: "Tap on continue"
|
||||
point: "50%,92%"
|
||||
- assertVisible: "You're ready to go!"
|
||||
- tapOn:
|
||||
label: "Tap on Lets go"
|
||||
point: "50%,92%"
|
||||
@@ -1,28 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "e2eStartOnboarding"
|
||||
- tapOn: "Select an avatar"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn:
|
||||
point: "16%,22%"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Choose"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Continue to next step"
|
||||
- assertVisible: "What are your interests?"
|
||||
- tapOn:
|
||||
label: "Tap on continue"
|
||||
point: "50%,92%"
|
||||
- assertVisible: "You're ready to go!"
|
||||
- tapOn:
|
||||
label: "Tap on Lets go"
|
||||
point: "50%,92%"
|
||||
@@ -1,124 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&posts&feeds"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
|
||||
# Navigate to my profile
|
||||
- tapOn:
|
||||
id: "bottomBarProfileBtn"
|
||||
|
||||
# Can see feeds
|
||||
- swipe:
|
||||
from:
|
||||
id: "profilePager-selector"
|
||||
direction: LEFT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-4"
|
||||
- assertVisible: "alice-favs"
|
||||
- swipe:
|
||||
from:
|
||||
id: "profilePager-selector"
|
||||
direction: RIGHT
|
||||
- tapOn:
|
||||
id: "profilePager-selector-0"
|
||||
|
||||
# Open and close edit profile modal
|
||||
- tapOn:
|
||||
id: "profileHeaderEditProfileButton"
|
||||
- assertVisible:
|
||||
id: "editProfileModal"
|
||||
- tapOn:
|
||||
id: "editProfileCancelBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
|
||||
# Edit display name and description via the edit profile modal
|
||||
- tapOn:
|
||||
id: "profileHeaderEditProfileButton"
|
||||
- assertVisible:
|
||||
id: "editProfileModal"
|
||||
- tapOn:
|
||||
id: "editProfileDisplayNameInput"
|
||||
- eraseText
|
||||
- inputText: "Alicia"
|
||||
- tapOn:
|
||||
id: "editProfileDescriptionInput"
|
||||
- eraseText
|
||||
- inputText: "One cool hacker"
|
||||
- tapOn: "Description"
|
||||
- tapOn:
|
||||
id: "editProfileSaveBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
- assertVisible: "Alicia"
|
||||
- assertVisible: "One cool hacker"
|
||||
|
||||
# Remove display name and description via the edit profile modal
|
||||
- tapOn:
|
||||
id: "profileHeaderEditProfileButton"
|
||||
- assertVisible:
|
||||
id: "editProfileModal"
|
||||
- tapOn:
|
||||
id: "editProfileDisplayNameInput"
|
||||
- eraseText
|
||||
- tapOn:
|
||||
id: "editProfileDescriptionInput"
|
||||
- eraseText
|
||||
- tapOn: "Description"
|
||||
- tapOn:
|
||||
id: "editProfileSaveBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
- assertVisible: "alice.test"
|
||||
- assertNotVisible: "One cool hacker"
|
||||
|
||||
# Set avi and banner via the edit profile modal
|
||||
- assertVisible:
|
||||
id: "userBannerFallback"
|
||||
- tapOn:
|
||||
id: "profileHeaderEditProfileButton"
|
||||
- assertVisible:
|
||||
id: "editProfileModal"
|
||||
- tapOn:
|
||||
id: "changeBannerBtn"
|
||||
- tapOn: "Upload from Library"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Choose"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn:
|
||||
id: "changeAvatarBtn"
|
||||
- tapOn: "Upload from Library"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Choose"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn:
|
||||
id: "editProfileSaveBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
- assertVisible:
|
||||
id: "userBannerImage"
|
||||
|
||||
# # Remove avi and banner via the edit profile modal
|
||||
- tapOn:
|
||||
id: "profileHeaderEditProfileButton"
|
||||
- assertVisible:
|
||||
id: "editProfileModal"
|
||||
- tapOn:
|
||||
id: "changeBannerBtn"
|
||||
- tapOn: "Remove Banner"
|
||||
- tapOn:
|
||||
id: "changeAvatarBtn"
|
||||
- tapOn: "Remove Avatar"
|
||||
- tapOn:
|
||||
id: "editProfileSaveBtn"
|
||||
- assertNotVisible:
|
||||
id: "editProfileModal"
|
||||
- assertVisible:
|
||||
id: "userBannerFallback"
|
||||
@@ -1,37 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&posts&feeds"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
# Navigate to another user profile
|
||||
- tapOn:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn:
|
||||
id: "searchTextInput"
|
||||
- inputText: "b"
|
||||
- tapOn:
|
||||
id: "searchAutoCompleteResult-bob.test"
|
||||
- assertVisible:
|
||||
id: "profileView"
|
||||
|
||||
# Can follow/unfollow another user
|
||||
- tapOn:
|
||||
id: "followBtn"
|
||||
- tapOn:
|
||||
id: "unfollowBtn"
|
||||
|
||||
# Can mute/unmute another user
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownBtn"
|
||||
- tapOn: "Mute Account"
|
||||
- assertVisible: "Account Muted"
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownBtn"
|
||||
- tapOn: "Unmute Account"
|
||||
- assertNotVisible: "Account Muted"
|
||||
@@ -1,22 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
# Navigate to another user profile via autocomplete
|
||||
- tapOn:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn:
|
||||
id: "searchTextInput"
|
||||
- inputText: "b"
|
||||
- tapOn:
|
||||
id: "searchAutoCompleteResult-bob.test"
|
||||
- assertVisible:
|
||||
id: "profileView"
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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"
|
||||
@@ -1,82 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&follows"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
|
||||
|
||||
# Login, create a thread, and log out
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Test thread"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
|
||||
# Login, reply to the thread, and log out
|
||||
- tapOn:
|
||||
id: "e2eSignInBob"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 1"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
|
||||
# Login, confirm notification exists, mute thread, and log out
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "bottomBarNotificationsBtn"
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "postDropdownBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- tapOn: "Mute thread"
|
||||
|
||||
# Login, reply to the thread twice, and log out
|
||||
- tapOn:
|
||||
id: "e2eSignInBob"
|
||||
- tapOn:
|
||||
id: "bottomBarProfileBtn"
|
||||
- tapOn:
|
||||
id: "profilePager-selector-1"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 2"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- tapOn:
|
||||
id: "replyBtn"
|
||||
- inputText: "Reply 3"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
|
||||
|
||||
# Login, confirm notifications dont exist, unmute the thread, confirm notifications exist
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- tapOn:
|
||||
id: "bottomBarNotificationsBtn"
|
||||
- assertNotVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- tapOn:
|
||||
id: "postDropdownBtn"
|
||||
- tapOn: "Unmute thread"
|
||||
- tapOn:
|
||||
id: "bottomBarNotificationsBtn"
|
||||
- swipe:
|
||||
from:
|
||||
id: "notifsFeed"
|
||||
direction: DOWN
|
||||
- assertVisible:
|
||||
id: "feedItem-by-bob.test"
|
||||
@@ -1,84 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&follows&thread"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
|
||||
# Navigate to thread
|
||||
- tapOn: "Thread root"
|
||||
- assertVisible: "Thread reply"
|
||||
|
||||
# Can like the root post
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- assertVisible:
|
||||
id: "likeCount-expanded"
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- assertNotVisible:
|
||||
id: "likeCount-expanded"
|
||||
|
||||
# Can like a reply post
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- assertVisible:
|
||||
id: "likeCount"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- assertNotVisible:
|
||||
id: "likeCount"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
|
||||
# Can repost the root post
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- tapOn: "Repost"
|
||||
- assertVisible:
|
||||
id: "repostCount-expanded"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- tapOn: "Remove repost"
|
||||
- assertNotVisible:
|
||||
id: "repostCount-expanded"
|
||||
|
||||
|
||||
# Can repost a reply post
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- tapOn: "Repost"
|
||||
- assertVisible:
|
||||
id: "repostCount"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- tapOn: "Remove repost"
|
||||
- assertNotVisible:
|
||||
id: "repostCount"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
@@ -1,520 +0,0 @@
|
||||
import {createServer as createHTTPServer} from 'node:http'
|
||||
import {parse} from 'node:url'
|
||||
|
||||
import {createServer, TestPDS} from '../jest/test-pds'
|
||||
|
||||
async function main() {
|
||||
let server: TestPDS
|
||||
createHTTPServer(async (req, res) => {
|
||||
const url = parse(req.url || '/', true)
|
||||
if (req.method !== 'POST') {
|
||||
return res.writeHead(200).end()
|
||||
}
|
||||
try {
|
||||
console.log('Closing old server')
|
||||
await server?.close()
|
||||
console.log('Starting new server')
|
||||
const inviteRequired = url?.query && 'invite' in url.query
|
||||
server = await createServer({inviteRequired})
|
||||
console.log('Listening at', server.pdsUrl)
|
||||
if (url?.query) {
|
||||
if ('users' in url.query) {
|
||||
console.log('Generating mock users')
|
||||
await server.mocker.createUser('alice')
|
||||
await server.mocker.createUser('bob')
|
||||
await server.mocker.createUser('carla')
|
||||
await server.mocker.users.alice.agent.upsertProfile(() => ({
|
||||
displayName: 'Alice',
|
||||
description: 'Test user 1',
|
||||
}))
|
||||
await server.mocker.users.bob.agent.upsertProfile(() => ({
|
||||
displayName: 'Bob',
|
||||
description: 'Test user 2',
|
||||
}))
|
||||
await server.mocker.users.carla.agent.upsertProfile(() => ({
|
||||
displayName: 'Carla',
|
||||
description: 'Test user 3',
|
||||
}))
|
||||
if (inviteRequired) {
|
||||
await server.mocker.createInvite(server.mocker.users.alice.did)
|
||||
}
|
||||
}
|
||||
if ('follows' in url.query) {
|
||||
console.log('Generating mock follows')
|
||||
await server.mocker.follow('alice', 'bob')
|
||||
await server.mocker.follow('alice', 'carla')
|
||||
await server.mocker.follow('bob', 'alice')
|
||||
await server.mocker.follow('bob', 'carla')
|
||||
await server.mocker.follow('carla', 'alice')
|
||||
await server.mocker.follow('carla', 'bob')
|
||||
}
|
||||
if ('posts' in url.query) {
|
||||
console.log('Generating mock posts')
|
||||
for (let user in server.mocker.users) {
|
||||
await server.mocker.users[user].agent.post({text: 'Post'})
|
||||
}
|
||||
}
|
||||
if ('feeds' in url.query) {
|
||||
console.log('Generating mock feed')
|
||||
await server.mocker.createFeed('alice', 'alice-favs', [])
|
||||
}
|
||||
if ('thread' in url.query) {
|
||||
console.log('Generating mock posts')
|
||||
const res = await server.mocker.users.bob.agent.post({
|
||||
text: 'Thread root',
|
||||
})
|
||||
await server.mocker.users.carla.agent.post({
|
||||
text: 'Thread reply',
|
||||
reply: {
|
||||
parent: {cid: res.cid, uri: res.uri},
|
||||
root: {cid: res.cid, uri: res.uri},
|
||||
},
|
||||
})
|
||||
}
|
||||
if ('mergefeed' in url.query) {
|
||||
console.log('Generating mock users')
|
||||
await server.mocker.createUser('alice')
|
||||
await server.mocker.createUser('bob')
|
||||
await server.mocker.createUser('carla')
|
||||
await server.mocker.createUser('dan')
|
||||
await server.mocker.users.alice.agent.upsertProfile(() => ({
|
||||
displayName: 'Alice',
|
||||
description: 'Test user 1',
|
||||
}))
|
||||
await server.mocker.users.bob.agent.upsertProfile(() => ({
|
||||
displayName: 'Bob',
|
||||
description: 'Test user 2',
|
||||
}))
|
||||
await server.mocker.users.carla.agent.upsertProfile(() => ({
|
||||
displayName: 'Carla',
|
||||
description: 'Test user 3',
|
||||
}))
|
||||
await server.mocker.users.dan.agent.upsertProfile(() => ({
|
||||
displayName: 'Dan',
|
||||
description: 'Test user 4',
|
||||
}))
|
||||
console.log('Generating mock follows')
|
||||
await server.mocker.follow('alice', 'bob')
|
||||
await server.mocker.follow('alice', 'carla')
|
||||
console.log('Generating mock posts')
|
||||
let posts: Record<string, any[]> = {
|
||||
alice: [],
|
||||
bob: [],
|
||||
carla: [],
|
||||
dan: [],
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let user in server.mocker.users) {
|
||||
if (user === 'alice') continue
|
||||
posts[user].push(
|
||||
await server.mocker.createPost(user, `Post ${i}`),
|
||||
)
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let user in server.mocker.users) {
|
||||
if (user === 'alice') continue
|
||||
if (i % 5 === 0) {
|
||||
await server.mocker.createReply(user, 'Self reply', {
|
||||
cid: posts[user][i].cid,
|
||||
uri: posts[user][i].uri,
|
||||
})
|
||||
}
|
||||
if (i % 5 === 1) {
|
||||
await server.mocker.createReply(user, 'Reply to bob', {
|
||||
cid: posts.bob[i].cid,
|
||||
uri: posts.bob[i].uri,
|
||||
})
|
||||
}
|
||||
if (i % 5 === 2) {
|
||||
await server.mocker.createReply(user, 'Reply to dan', {
|
||||
cid: posts.dan[i].cid,
|
||||
uri: posts.dan[i].uri,
|
||||
})
|
||||
}
|
||||
await server.mocker.users[user].agent.post({text: `Post ${i}`})
|
||||
}
|
||||
}
|
||||
console.log('Generating mock feeds')
|
||||
await server.mocker.createFeed(
|
||||
'alice',
|
||||
'alice-favs',
|
||||
posts.dan.map(p => p.uri),
|
||||
)
|
||||
await server.mocker.createFeed(
|
||||
'alice',
|
||||
'alice-favs2',
|
||||
posts.dan.map(p => p.uri),
|
||||
)
|
||||
}
|
||||
if ('labels' in url.query) {
|
||||
console.log('Generating naughty users with labels')
|
||||
|
||||
const anchorPost = await server.mocker.createPost(
|
||||
'alice',
|
||||
'Anchor post',
|
||||
)
|
||||
|
||||
for (const user of [
|
||||
'dmca-account',
|
||||
'dmca-profile',
|
||||
'dmca-posts',
|
||||
'porn-account',
|
||||
'porn-profile',
|
||||
'porn-posts',
|
||||
'nudity-account',
|
||||
'nudity-profile',
|
||||
'nudity-posts',
|
||||
'scam-account',
|
||||
'scam-profile',
|
||||
'scam-posts',
|
||||
'unknown-account',
|
||||
'unknown-profile',
|
||||
'unknown-posts',
|
||||
'hide-account',
|
||||
'hide-profile',
|
||||
'hide-posts',
|
||||
'no-promote-account',
|
||||
'no-promote-profile',
|
||||
'no-promote-posts',
|
||||
'warn-account',
|
||||
'warn-profile',
|
||||
'warn-posts',
|
||||
'muted-account',
|
||||
'muted-by-list-account',
|
||||
'blocking-account',
|
||||
'blockedby-account',
|
||||
'mutual-block-account',
|
||||
]) {
|
||||
await server.mocker.createUser(user)
|
||||
await server.mocker.follow('alice', user)
|
||||
await server.mocker.follow(user, 'alice')
|
||||
await server.mocker.createPost(user, `Unlabeled post from ${user}`)
|
||||
await server.mocker.createReply(
|
||||
user,
|
||||
`Unlabeled reply from ${user}`,
|
||||
anchorPost,
|
||||
)
|
||||
await server.mocker.like(user, anchorPost)
|
||||
}
|
||||
|
||||
await server.mocker.labelAccount('dmca-violation', 'dmca-account')
|
||||
await server.mocker.labelProfile('dmca-violation', 'dmca-profile')
|
||||
await server.mocker.labelPost(
|
||||
'dmca-violation',
|
||||
await server.mocker.createPost('dmca-posts', 'dmca post'),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'dmca-violation',
|
||||
await server.mocker.createQuotePost(
|
||||
'dmca-posts',
|
||||
'dmca quote post',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'dmca-violation',
|
||||
await server.mocker.createReply(
|
||||
'dmca-posts',
|
||||
'dmca reply',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
|
||||
await server.mocker.labelAccount('porn', 'porn-account')
|
||||
await server.mocker.labelProfile('porn', 'porn-profile')
|
||||
await server.mocker.labelPost(
|
||||
'porn',
|
||||
await server.mocker.createImagePost('porn-posts', 'porn post'),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'porn',
|
||||
await server.mocker.createQuotePost(
|
||||
'porn-posts',
|
||||
'porn quote post',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'porn',
|
||||
await server.mocker.createReply(
|
||||
'porn-posts',
|
||||
'porn reply',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
|
||||
await server.mocker.labelAccount('nudity', 'nudity-account')
|
||||
await server.mocker.labelProfile('nudity', 'nudity-profile')
|
||||
await server.mocker.labelPost(
|
||||
'nudity',
|
||||
await server.mocker.createImagePost('nudity-posts', 'nudity post'),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'nudity',
|
||||
await server.mocker.createQuotePost(
|
||||
'nudity-posts',
|
||||
'nudity quote post',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'nudity',
|
||||
await server.mocker.createReply(
|
||||
'nudity-posts',
|
||||
'nudity reply',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
|
||||
await server.mocker.labelAccount('scam', 'scam-account')
|
||||
await server.mocker.labelProfile('scam', 'scam-profile')
|
||||
await server.mocker.labelPost(
|
||||
'scam',
|
||||
await server.mocker.createPost('scam-posts', 'scam post'),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'scam',
|
||||
await server.mocker.createQuotePost(
|
||||
'scam-posts',
|
||||
'scam quote post',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'scam',
|
||||
await server.mocker.createReply(
|
||||
'scam-posts',
|
||||
'scam reply',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
|
||||
await server.mocker.labelAccount(
|
||||
'not-a-real-label',
|
||||
'unknown-account',
|
||||
)
|
||||
await server.mocker.labelProfile(
|
||||
'not-a-real-label',
|
||||
'unknown-profile',
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'not-a-real-label',
|
||||
await server.mocker.createPost('unknown-posts', 'unknown post'),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'not-a-real-label',
|
||||
await server.mocker.createQuotePost(
|
||||
'unknown-posts',
|
||||
'unknown quote post',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'not-a-real-label',
|
||||
await server.mocker.createReply(
|
||||
'unknown-posts',
|
||||
'unknown reply',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
|
||||
await server.mocker.labelAccount('!hide', 'hide-account')
|
||||
await server.mocker.labelProfile('!hide', 'hide-profile')
|
||||
await server.mocker.labelPost(
|
||||
'!hide',
|
||||
await server.mocker.createPost('hide-posts', 'hide post'),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'!hide',
|
||||
await server.mocker.createQuotePost(
|
||||
'hide-posts',
|
||||
'hide quote post',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'!hide',
|
||||
await server.mocker.createReply(
|
||||
'hide-posts',
|
||||
'hide reply',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
|
||||
await server.mocker.labelAccount('!no-promote', 'no-promote-account')
|
||||
await server.mocker.labelProfile('!no-promote', 'no-promote-profile')
|
||||
await server.mocker.labelPost(
|
||||
'!no-promote',
|
||||
await server.mocker.createPost(
|
||||
'no-promote-posts',
|
||||
'no-promote post',
|
||||
),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'!no-promote',
|
||||
await server.mocker.createQuotePost(
|
||||
'no-promote-posts',
|
||||
'no-promote quote post',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'!no-promote',
|
||||
await server.mocker.createReply(
|
||||
'no-promote-posts',
|
||||
'no-promote reply',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
|
||||
await server.mocker.labelAccount('!warn', 'warn-account')
|
||||
await server.mocker.labelProfile('!warn', 'warn-profile')
|
||||
await server.mocker.labelPost(
|
||||
'!warn',
|
||||
await server.mocker.createPost('warn-posts', 'warn post'),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'!warn',
|
||||
await server.mocker.createQuotePost(
|
||||
'warn-posts',
|
||||
'warn quote post',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
await server.mocker.labelPost(
|
||||
'!warn',
|
||||
await server.mocker.createReply(
|
||||
'warn-posts',
|
||||
'warn reply',
|
||||
anchorPost,
|
||||
),
|
||||
)
|
||||
|
||||
await server.mocker.users.alice.agent.mute('muted-account.test')
|
||||
await server.mocker.createPost('muted-account', 'muted post')
|
||||
await server.mocker.createQuotePost(
|
||||
'muted-account',
|
||||
'muted quote post',
|
||||
anchorPost,
|
||||
)
|
||||
await server.mocker.createReply(
|
||||
'muted-account',
|
||||
'muted reply',
|
||||
anchorPost,
|
||||
)
|
||||
|
||||
const list = await server.mocker.createMuteList(
|
||||
'alice',
|
||||
'Muted Users',
|
||||
)
|
||||
await server.mocker.addToMuteList(
|
||||
'alice',
|
||||
list,
|
||||
server.mocker.users['muted-by-list-account'].did,
|
||||
)
|
||||
await server.mocker.createPost('muted-by-list-account', 'muted post')
|
||||
await server.mocker.createQuotePost(
|
||||
'muted-by-list-account',
|
||||
'account quote post',
|
||||
anchorPost,
|
||||
)
|
||||
await server.mocker.createReply(
|
||||
'muted-by-list-account',
|
||||
'account reply',
|
||||
anchorPost,
|
||||
)
|
||||
|
||||
await server.mocker.createPost('blocking-account', 'blocking post')
|
||||
await server.mocker.createQuotePost(
|
||||
'blocking-account',
|
||||
'blocking quote post',
|
||||
anchorPost,
|
||||
)
|
||||
await server.mocker.createReply(
|
||||
'blocking-account',
|
||||
'blocking reply',
|
||||
anchorPost,
|
||||
)
|
||||
await server.mocker.users.alice.agent.app.bsky.graph.block.create(
|
||||
{
|
||||
repo: server.mocker.users.alice.did,
|
||||
},
|
||||
{
|
||||
subject: server.mocker.users['blocking-account'].did,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
|
||||
await server.mocker.createPost('blockedby-account', 'blockedby post')
|
||||
await server.mocker.createQuotePost(
|
||||
'blockedby-account',
|
||||
'blockedby quote post',
|
||||
anchorPost,
|
||||
)
|
||||
await server.mocker.createReply(
|
||||
'blockedby-account',
|
||||
'blockedby reply',
|
||||
anchorPost,
|
||||
)
|
||||
await server.mocker.users[
|
||||
'blockedby-account'
|
||||
].agent.app.bsky.graph.block.create(
|
||||
{
|
||||
repo: server.mocker.users['blockedby-account'].did,
|
||||
},
|
||||
{
|
||||
subject: server.mocker.users.alice.did,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
|
||||
await server.mocker.createPost(
|
||||
'mutual-block-account',
|
||||
'mutual-block post',
|
||||
)
|
||||
await server.mocker.createQuotePost(
|
||||
'mutual-block-account',
|
||||
'mutual-block quote post',
|
||||
anchorPost,
|
||||
)
|
||||
await server.mocker.createReply(
|
||||
'mutual-block-account',
|
||||
'mutual-block reply',
|
||||
anchorPost,
|
||||
)
|
||||
await server.mocker.users.alice.agent.app.bsky.graph.block.create(
|
||||
{
|
||||
repo: server.mocker.users.alice.did,
|
||||
},
|
||||
{
|
||||
subject: server.mocker.users['mutual-block-account'].did,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
await server.mocker.users[
|
||||
'mutual-block-account'
|
||||
].agent.app.bsky.graph.block.create(
|
||||
{
|
||||
repo: server.mocker.users['mutual-block-account'].did,
|
||||
},
|
||||
{
|
||||
subject: server.mocker.users.alice.did,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
|
||||
// flush caches
|
||||
await server.mocker.testNet.processAll()
|
||||
}
|
||||
}
|
||||
console.log('Ready')
|
||||
return res.writeHead(200).end(server.pdsUrl)
|
||||
} catch (e) {
|
||||
console.error('Error!', e)
|
||||
return res.writeHead(500).end()
|
||||
}
|
||||
}).listen(1986)
|
||||
console.log('Mock server manager listening on 1986')
|
||||
}
|
||||
main()
|
||||
@@ -1,77 +0,0 @@
|
||||
|
||||
# flow.yaml
|
||||
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- launchApp
|
||||
# Login
|
||||
# - runFlow:
|
||||
# when:
|
||||
# - tapOn: "Sign In"
|
||||
# - tapOn: "Username or email address"
|
||||
# - inputText: "ansh.bsky.team"
|
||||
# - tapOn: "Password"
|
||||
# - inputText: "PASSWORd"
|
||||
# - tapOn: "Next"
|
||||
# Allow notifications if popup is visible
|
||||
# - runFlow:
|
||||
# when:
|
||||
# visible: "Notifications"
|
||||
# commands:
|
||||
# - tapOn: "Allow"
|
||||
# Scroll in main feed
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
# Swipe between feeds
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- swipe:
|
||||
direction: "LEFT"
|
||||
- swipe:
|
||||
direction: "RIGHT"
|
||||
- swipe:
|
||||
direction: "RIGHT"
|
||||
- swipe:
|
||||
direction: "RIGHT"
|
||||
# Go to Notifications
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn: "Notifications"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- swipe:
|
||||
direction: "DOWN" # Make header visible
|
||||
# Go to Feeds tab
|
||||
- tapOn:
|
||||
id: "viewHeaderDrawerBtn"
|
||||
- tapOn: "Feeds"
|
||||
- scrollUntilVisible:
|
||||
element: "Discover"
|
||||
direction: UP
|
||||
- tapOn: "Discover"
|
||||
- waitForAnimationToEnd
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
# Click on post
|
||||
- tapOn:
|
||||
id: "postText"
|
||||
index: 0
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
@@ -1,11 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- launchApp:
|
||||
appId: "xyz.blueskyweb.app"
|
||||
clearState: true
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "http://localhost:8081"
|
||||
- waitForAnimationToEnd
|
||||
- swipe:
|
||||
from: "Bluesky"
|
||||
direction: DOWN
|
||||
@@ -1,5 +0,0 @@
|
||||
// eslint-disable-next-line
|
||||
http.post('http://localhost:1986/' + SERVER_PATH, {
|
||||
headers: {'Content-Type': 'text/plain'},
|
||||
body: '',
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {ReactNode} from 'react'
|
||||
import React from 'react'
|
||||
import {View, ScrollView, Modal, FlatList, TextInput} from 'react-native'
|
||||
|
||||
const BottomSheetModalContext = React.createContext(null)
|
||||
@@ -6,10 +6,7 @@ const BottomSheetModalContext = React.createContext(null)
|
||||
const BottomSheetModalProvider = (props: any) => {
|
||||
return <BottomSheetModalContext.Provider {...props} value={{}} />
|
||||
}
|
||||
class BottomSheet extends React.Component<{
|
||||
onClose?: () => void
|
||||
children?: ReactNode
|
||||
}> {
|
||||
class BottomSheet extends React.Component {
|
||||
snapToIndex() {}
|
||||
snapToPosition() {}
|
||||
expand() {}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const Slider = {}
|
||||
@@ -1 +0,0 @@
|
||||
export const getLocales = jest.fn().mockResolvedValue([])
|
||||
Vendored
-67
@@ -1,67 +0,0 @@
|
||||
// @flow
|
||||
|
||||
// https://github.com/FormidableLabs/react-native-svg-mock
|
||||
import React from 'react'
|
||||
|
||||
const createComponent = function (name) {
|
||||
return class extends React.Component {
|
||||
// overwrite the displayName, since this is a class created dynamically
|
||||
static displayName = name
|
||||
|
||||
render() {
|
||||
return React.createElement(name, this.props, this.props.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock all react-native-svg exports
|
||||
// from https://github.com/magicismight/react-native-svg/blob/master/index.js
|
||||
const Svg = createComponent('Svg')
|
||||
const Circle = createComponent('Circle')
|
||||
const Ellipse = createComponent('Ellipse')
|
||||
const G = createComponent('G')
|
||||
const Text = createComponent('Text')
|
||||
const TextPath = createComponent('TextPath')
|
||||
const TSpan = createComponent('TSpan')
|
||||
const Path = createComponent('Path')
|
||||
const Polygon = createComponent('Polygon')
|
||||
const Polyline = createComponent('Polyline')
|
||||
const Line = createComponent('Line')
|
||||
const Rect = createComponent('Rect')
|
||||
const Use = createComponent('Use')
|
||||
const Image = createComponent('Image')
|
||||
const Symbol = createComponent('Symbol')
|
||||
const Defs = createComponent('Defs')
|
||||
const LinearGradient = createComponent('LinearGradient')
|
||||
const RadialGradient = createComponent('RadialGradient')
|
||||
const Stop = createComponent('Stop')
|
||||
const ClipPath = createComponent('ClipPath')
|
||||
const Pattern = createComponent('Pattern')
|
||||
const Mask = createComponent('Mask')
|
||||
|
||||
export {
|
||||
Svg,
|
||||
Circle,
|
||||
Ellipse,
|
||||
G,
|
||||
Text,
|
||||
TextPath,
|
||||
TSpan,
|
||||
Path,
|
||||
Polygon,
|
||||
Polyline,
|
||||
Line,
|
||||
Rect,
|
||||
Use,
|
||||
Image,
|
||||
Symbol,
|
||||
Defs,
|
||||
LinearGradient,
|
||||
RadialGradient,
|
||||
Stop,
|
||||
ClipPath,
|
||||
Pattern,
|
||||
Mask,
|
||||
}
|
||||
|
||||
export default Svg
|
||||
@@ -1,3 +0,0 @@
|
||||
jest.mock('sentry-expo', () => ({
|
||||
init: () => jest.fn(),
|
||||
}))
|
||||
@@ -0,0 +1,937 @@
|
||||
import {LogModel} from './../src/state/models/log'
|
||||
import {LRUMap} from 'lru_map'
|
||||
import {RootStoreModel} from './../src/state/models/root-store'
|
||||
import {NavigationTabModel} from './../src/state/models/navigation'
|
||||
import {SessionModel} from '../src/state/models/session'
|
||||
import {NavigationModel} from '../src/state/models/navigation'
|
||||
import {ShellUiModel} from '../src/state/models/shell-ui'
|
||||
import {MeModel} from '../src/state/models/me'
|
||||
import {OnboardModel} from '../src/state/models/onboard'
|
||||
import {ProfilesViewModel} from '../src/state/models/profiles-view'
|
||||
import {LinkMetasViewModel} from '../src/state/models/link-metas-view'
|
||||
import {MembershipsViewModel} from '../src/state/models/memberships-view'
|
||||
import {FeedModel} from '../src/state/models/feed-view'
|
||||
import {NotificationsViewModel} from '../src/state/models/notifications-view'
|
||||
import {ProfileViewModel} from '../src/state/models/profile-view'
|
||||
import {MembersViewModel} from '../src/state/models/members-view'
|
||||
import {ProfileUiModel, Sections} from '../src/state/models/profile-ui'
|
||||
import {SessionServiceClient} from '@atproto/api'
|
||||
import {UserAutocompleteViewModel} from '../src/state/models/user-autocomplete-view'
|
||||
import {UserLocalPhotosModel} from '../src/state/models/user-local-photos'
|
||||
import {SuggestedActorsViewModel} from '../src/state/models/suggested-actors-view'
|
||||
import {UserFollowersViewModel} from '../src/state/models/user-followers-view'
|
||||
import {UserFollowsViewModel} from '../src/state/models/user-follows-view'
|
||||
import {NotificationsViewItemModel} from './../src/state/models/notifications-view'
|
||||
import {
|
||||
PostThreadViewModel,
|
||||
PostThreadViewPostModel,
|
||||
} from '../src/state/models/post-thread-view'
|
||||
import {FeedItemModel} from '../src/state/models/feed-view'
|
||||
import {RepostedByViewModel} from '../src/state/models/reposted-by-view'
|
||||
import {VotesViewModel} from '../src/state/models/votes-view'
|
||||
|
||||
export const mockedProfileStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: true,
|
||||
error: '',
|
||||
params: {
|
||||
actor: '',
|
||||
},
|
||||
did: 'test did',
|
||||
handle: 'testhandle',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: '',
|
||||
},
|
||||
creator: 'test did',
|
||||
displayName: '',
|
||||
description: '',
|
||||
avatar: '',
|
||||
banner: '',
|
||||
followersCount: 0,
|
||||
followsCount: 0,
|
||||
membersCount: 0,
|
||||
postsCount: 0,
|
||||
myState: {
|
||||
follow: '',
|
||||
member: '',
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
isUser: true,
|
||||
isScene: false,
|
||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
toggleFollowing: jest.fn().mockResolvedValue({}),
|
||||
updateProfile: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _load, _replaceAll
|
||||
} as unknown as ProfileViewModel
|
||||
|
||||
export const mockedMembersStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: true,
|
||||
error: '',
|
||||
params: {
|
||||
actor: 'test actor',
|
||||
},
|
||||
subject: {
|
||||
did: 'test did',
|
||||
handle: '',
|
||||
displayName: '',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: '',
|
||||
},
|
||||
avatar: undefined,
|
||||
},
|
||||
members: [
|
||||
{
|
||||
did: 'test did2',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: '',
|
||||
},
|
||||
handle: 'testhandle',
|
||||
displayName: 'test name',
|
||||
indexedAt: '',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
isMember: jest.fn(),
|
||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn(),
|
||||
removeMember: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _replaceAll, _append
|
||||
} as unknown as MembersViewModel
|
||||
|
||||
export const mockedMembershipsStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: true,
|
||||
error: '',
|
||||
params: {
|
||||
actor: '',
|
||||
limit: 1,
|
||||
before: '',
|
||||
},
|
||||
subject: {
|
||||
did: 'test did',
|
||||
handle: '',
|
||||
displayName: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
avatar: undefined,
|
||||
},
|
||||
memberships: [
|
||||
{
|
||||
did: 'test did',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: 'app.bsky.system.actorUser',
|
||||
},
|
||||
handle: ',',
|
||||
displayName: '',
|
||||
createdAt: '',
|
||||
indexedAt: '',
|
||||
_reactKey: 'item-1',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
isMemberOf: jest.fn(),
|
||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _replaceAll, _append
|
||||
} as unknown as MembershipsViewModel
|
||||
|
||||
export const mockedFeedItemStore = {
|
||||
_reactKey: 'item-1',
|
||||
_isThreadParent: false,
|
||||
_isThreadChildElided: false,
|
||||
_isThreadChild: false,
|
||||
post: {
|
||||
uri: 'testuri',
|
||||
cid: 'test cid',
|
||||
author: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
displayName: 'test name',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
record: {
|
||||
$type: 'app.bsky.feed.post',
|
||||
createdAt: '2022-12-29T16:39:57.919Z',
|
||||
text: 'Sup',
|
||||
},
|
||||
replyCount: 0,
|
||||
repostCount: 0,
|
||||
upvoteCount: 0,
|
||||
downvoteCount: 0,
|
||||
indexedAt: '2022-12-29T16:39:57.919Z',
|
||||
viewer: {},
|
||||
},
|
||||
postRecord: {
|
||||
$type: 'app.bsky.feed.post',
|
||||
text: 'test text',
|
||||
createdAt: '1',
|
||||
reply: {
|
||||
root: {
|
||||
uri: 'testuri',
|
||||
cid: 'tes cid',
|
||||
},
|
||||
parent: {
|
||||
uri: 'testuri',
|
||||
cid: 'tes cid',
|
||||
},
|
||||
},
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
copy: jest.fn(),
|
||||
toggleUpvote: jest.fn().mockResolvedValue({}),
|
||||
toggleDownvote: jest.fn(),
|
||||
toggleRepost: jest.fn().mockResolvedValue({}),
|
||||
delete: jest.fn().mockResolvedValue({}),
|
||||
reasonRepost: {
|
||||
by: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
indexedAt: '',
|
||||
},
|
||||
reasonTrend: {
|
||||
by: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
indexedAt: '',
|
||||
},
|
||||
reply: {
|
||||
parent: {
|
||||
author: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
displayName: 'test name',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
cid: '',
|
||||
downvoteCount: 0,
|
||||
indexedAt: '2023-01-10T11:17:46.945Z',
|
||||
record: {},
|
||||
replyCount: 1,
|
||||
repostCount: 0,
|
||||
upvoteCount: 0,
|
||||
uri: 'testuri',
|
||||
viewer: {},
|
||||
},
|
||||
root: {
|
||||
author: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
displayName: 'test name',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
cid: '',
|
||||
downvoteCount: 0,
|
||||
indexedAt: '2023-01-10T11:17:46.739Z',
|
||||
record: {},
|
||||
replyCount: 1,
|
||||
repostCount: 0,
|
||||
upvoteCount: 1,
|
||||
uri: 'testuri',
|
||||
viewer: {},
|
||||
},
|
||||
},
|
||||
} as FeedItemModel
|
||||
|
||||
export const mockedFeedStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasNewLatest: false,
|
||||
hasLoaded: true,
|
||||
error: '',
|
||||
hasMore: true,
|
||||
params: {
|
||||
actor: '',
|
||||
limit: 1,
|
||||
before: '',
|
||||
},
|
||||
feed: [],
|
||||
rootStore: {} as RootStoreModel,
|
||||
feedType: 'home',
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
nonReplyFeed: [
|
||||
{
|
||||
_reactKey: 'item-1',
|
||||
post: {
|
||||
author: {
|
||||
handle: 'handle.test',
|
||||
displayName: 'test name',
|
||||
avatar: '',
|
||||
},
|
||||
cid: 'bafyreihkwjoy2vbfqld2lp3tv4ce6yfr354sqgp32qoplrudso4gyyjiwe',
|
||||
downvoteCount: 0,
|
||||
indexedAt: '2022-12-29T16:35:55.270Z',
|
||||
record: {
|
||||
$type: 'app.bsky.feed.post',
|
||||
createdAt: '2022-12-29T16:39:57.919Z',
|
||||
text: 'Sup',
|
||||
},
|
||||
replyCount: 0,
|
||||
repostCount: 0,
|
||||
upvoteCount: 0,
|
||||
uri: 'at://did:plc:wcizmlgv3rdslk64t6q4silu/app.bsky.feed.post/3jkzce5kfvn2h',
|
||||
viewer: {
|
||||
handle: 'handle.test',
|
||||
displayName: 'test name',
|
||||
avatar: '',
|
||||
},
|
||||
},
|
||||
reason: undefined,
|
||||
reply: undefined,
|
||||
},
|
||||
],
|
||||
setHasNewLatest: jest.fn(),
|
||||
setup: jest.fn().mockResolvedValue({}),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn().mockResolvedValue({}),
|
||||
loadLatest: jest.fn(),
|
||||
update: jest.fn(),
|
||||
checkForLatest: jest.fn().mockRejectedValue('Error checking for latest'),
|
||||
registerListeners: jest.fn().mockReturnValue(jest.fn()),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _pendingWork, _initialLoad, _loadLatest, _loadMore, _update, _replaceAll, _appendAll, _prependAll, _updateAll, _getFeed, loadMoreCursor, pollCursor, _loadPromise, _updatePromise, _loadLatestPromise, _loadMorePromise
|
||||
} as unknown as FeedModel
|
||||
|
||||
export const mockedPostThreadViewPostStore = {
|
||||
_reactKey: 'item-1',
|
||||
_depth: 0,
|
||||
_isHighlightedPost: false,
|
||||
_hasMore: false,
|
||||
postRecord: {
|
||||
text: 'test text',
|
||||
createdAt: '',
|
||||
reply: {
|
||||
root: {
|
||||
uri: 'testuri',
|
||||
cid: 'tes cid',
|
||||
},
|
||||
parent: {
|
||||
uri: 'testuri',
|
||||
cid: 'tes cid',
|
||||
},
|
||||
},
|
||||
},
|
||||
post: {
|
||||
uri: 'testuri',
|
||||
cid: 'testcid',
|
||||
record: {},
|
||||
author: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
viewer: {
|
||||
muted: true,
|
||||
},
|
||||
},
|
||||
replyCount: 0,
|
||||
repostCount: 0,
|
||||
upvoteCount: 0,
|
||||
downvoteCount: 0,
|
||||
indexedAt: '',
|
||||
viewer: {
|
||||
repost: '',
|
||||
upvote: '',
|
||||
downvote: '',
|
||||
},
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
assignTreeModels: jest.fn(),
|
||||
toggleRepost: jest.fn().mockRejectedValue({}),
|
||||
toggleUpvote: jest.fn().mockRejectedValue({}),
|
||||
toggleDownvote: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
} as PostThreadViewPostModel
|
||||
|
||||
export const mockedPostThreadViewStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
notFound: false,
|
||||
resolvedUri: 'testuri',
|
||||
params: {
|
||||
uri: 'testuri',
|
||||
},
|
||||
thread: mockedPostThreadViewPostStore,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
setup: jest.fn(),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
update: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _resolveUri, _load, _replaceAll
|
||||
} as unknown as PostThreadViewModel
|
||||
|
||||
export const mockedNotificationsViewItemStore = {
|
||||
_reactKey: 'item-1',
|
||||
uri: 'testuri',
|
||||
cid: '',
|
||||
author: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
copy: jest.fn(),
|
||||
reason: 'test reason',
|
||||
isRead: true,
|
||||
indexedAt: '',
|
||||
isUpvote: true,
|
||||
isRepost: false,
|
||||
isTrend: false,
|
||||
isMention: false,
|
||||
isReply: false,
|
||||
isFollow: false,
|
||||
isAssertion: false,
|
||||
needsAdditionalData: false,
|
||||
isInvite: false,
|
||||
subjectUri: 'testuri',
|
||||
toSupportedRecord: jest.fn().mockReturnValue({
|
||||
text: 'test text',
|
||||
createdAt: '',
|
||||
}),
|
||||
fetchAdditionalData: jest.fn(),
|
||||
toNotifeeOpts: jest.fn(),
|
||||
} as NotificationsViewItemModel
|
||||
|
||||
export const mockedNotificationsStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: true,
|
||||
error: '',
|
||||
params: {
|
||||
limit: 1,
|
||||
before: '',
|
||||
},
|
||||
hasMore: true,
|
||||
notifications: [mockedNotificationsViewItemStore],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn().mockResolvedValue({}),
|
||||
update: jest.fn().mockResolvedValue(null),
|
||||
updateReadState: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _pendingWork, _initialLoad, _loadMore, _update, _replaceAll, _appendAll, _updateAll, loadMoreCursor, _loadPromise, _updatePromise, _loadLatestPromise, _loadMorePromise
|
||||
} as unknown as NotificationsViewModel
|
||||
|
||||
export const mockedSessionStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
data: {
|
||||
service: '',
|
||||
refreshJwt: '',
|
||||
accessJwt: '',
|
||||
handle: '',
|
||||
did: 'test did',
|
||||
},
|
||||
online: false,
|
||||
attemptingConnect: false,
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasSession: true,
|
||||
clear: jest.fn(),
|
||||
setState: jest.fn(),
|
||||
setOnline: jest.fn(),
|
||||
updateAuthTokens: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
describeService: jest.fn().mockResolvedValue({
|
||||
availableUserDomains: ['test'],
|
||||
links: {
|
||||
termsOfService: 'https://testTermsOfService',
|
||||
privacyPolicy: 'https://testPrivacyPolicy',
|
||||
},
|
||||
}),
|
||||
login: jest.fn(),
|
||||
createAccount: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
|
||||
// unknown required because of the missing private methods: _connectPromise, configureApi & _connect
|
||||
} as unknown as SessionModel
|
||||
|
||||
export const mockedNavigationTabStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
id: 0,
|
||||
history: [
|
||||
{
|
||||
url: '',
|
||||
ts: 0,
|
||||
title: '',
|
||||
id: 0,
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
current: {
|
||||
url: '',
|
||||
ts: 0,
|
||||
title: '',
|
||||
id: 0,
|
||||
},
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
backTen: [
|
||||
{
|
||||
url: '',
|
||||
title: '',
|
||||
index: 0,
|
||||
id: 0,
|
||||
},
|
||||
],
|
||||
forwardTen: [
|
||||
{
|
||||
url: '',
|
||||
title: '',
|
||||
index: 0,
|
||||
id: 0,
|
||||
},
|
||||
],
|
||||
navigate: jest.fn(),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
goBack: jest.fn(),
|
||||
fixedTabReset: jest.fn(),
|
||||
goForward: jest.fn(),
|
||||
goToIndex: jest.fn(),
|
||||
setTitle: jest.fn(),
|
||||
setIsNewTab: jest.fn(),
|
||||
fixedTabPurpose: 0,
|
||||
getBackList: () => [
|
||||
{
|
||||
url: '/',
|
||||
title: '',
|
||||
index: 1,
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
getForwardList: jest.fn(),
|
||||
} as NavigationTabModel
|
||||
|
||||
export const mockedNavigationStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
tabs: [mockedNavigationTabStore],
|
||||
tabIndex: 0,
|
||||
clear: jest.fn(),
|
||||
tab: mockedNavigationTabStore,
|
||||
tabCount: 1,
|
||||
isCurrentScreen: jest.fn(),
|
||||
navigate: jest.fn(),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
setTitle: jest.fn(),
|
||||
handleLink: jest.fn(),
|
||||
switchTo: jest.fn(),
|
||||
setActiveTab: jest.fn(),
|
||||
closeTab: jest.fn(),
|
||||
newTab: jest.fn(),
|
||||
} as NavigationModel
|
||||
|
||||
export const mockedShellStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
minimalShellMode: false,
|
||||
isMainMenuOpen: false,
|
||||
isModalActive: false,
|
||||
activeModal: undefined,
|
||||
isLightboxActive: false,
|
||||
activeLightbox: undefined,
|
||||
isComposerActive: false,
|
||||
composerOpts: undefined,
|
||||
darkMode: false,
|
||||
setDarkMode: jest.fn(),
|
||||
setMainMenuOpen: jest.fn(),
|
||||
setMinimalShellMode: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
closeModal: jest.fn(),
|
||||
closeComposer: jest.fn(),
|
||||
closeLightbox: jest.fn(),
|
||||
openComposer: jest.fn(),
|
||||
openLightbox: jest.fn(),
|
||||
} as ShellUiModel
|
||||
|
||||
export const mockedMeStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
did: 'test did',
|
||||
handle: 'test',
|
||||
displayName: 'test',
|
||||
description: 'test',
|
||||
avatar: '',
|
||||
notificationCount: 0,
|
||||
rootStore: {} as RootStoreModel,
|
||||
memberships: mockedMembershipsStore,
|
||||
mainFeed: mockedFeedStore,
|
||||
notifications: mockedNotificationsStore,
|
||||
clear: jest.fn(),
|
||||
load: jest.fn(),
|
||||
clearNotificationCount: jest.fn(),
|
||||
fetchNotifications: jest.fn(),
|
||||
refreshMemberships: jest.fn(),
|
||||
} as MeModel
|
||||
|
||||
export const mockedOnboardStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
isOnboarding: false,
|
||||
stage: '',
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
next: jest.fn(),
|
||||
} as OnboardModel
|
||||
|
||||
export const mockedProfilesStore = {
|
||||
hydrate: jest.fn(),
|
||||
serialize: jest.fn(),
|
||||
cache: new LRUMap(100),
|
||||
rootStore: {} as RootStoreModel,
|
||||
getProfile: jest.fn().mockResolvedValue({data: {}}),
|
||||
overwrite: jest.fn(),
|
||||
} as ProfilesViewModel
|
||||
|
||||
export const mockedLinkMetasStore = {
|
||||
hydrate: jest.fn(),
|
||||
serialize: jest.fn(),
|
||||
cache: new LRUMap(100),
|
||||
rootStore: {} as RootStoreModel,
|
||||
getLinkMeta: jest.fn(),
|
||||
} as LinkMetasViewModel
|
||||
|
||||
export const mockedLogStore = {
|
||||
entries: [],
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
// unknown required because of the missing private methods: add
|
||||
} as unknown as LogModel
|
||||
|
||||
export const mockedRootStore = {
|
||||
api: {
|
||||
com: {},
|
||||
app: {
|
||||
bsky: {
|
||||
actor: {
|
||||
searchTypeahead: jest.fn().mockResolvedValue({data: {users: []}}),
|
||||
},
|
||||
graph: {
|
||||
getFollows: jest.fn().mockResolvedValue({data: {follows: []}}),
|
||||
getFollowers: jest.fn().mockResolvedValue({}),
|
||||
getMembers: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as SessionServiceClient,
|
||||
resolveName: jest.fn(),
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
fetchStateUpdate: jest.fn(),
|
||||
clearAll: jest.fn(),
|
||||
session: mockedSessionStore,
|
||||
nav: mockedNavigationStore,
|
||||
shell: mockedShellStore,
|
||||
me: mockedMeStore,
|
||||
onboard: mockedOnboardStore,
|
||||
profiles: mockedProfilesStore,
|
||||
linkMetas: mockedLinkMetasStore,
|
||||
log: mockedLogStore,
|
||||
} as RootStoreModel
|
||||
|
||||
export const mockedProfileUiStore = {
|
||||
profile: mockedProfileStore,
|
||||
feed: mockedFeedStore,
|
||||
memberships: mockedMembershipsStore,
|
||||
members: mockedMembersStore,
|
||||
selectedViewIndex: 0,
|
||||
rootStore: mockedRootStore,
|
||||
params: {
|
||||
user: 'test user',
|
||||
},
|
||||
currentView: mockedFeedStore,
|
||||
isInitialLoading: false,
|
||||
isRefreshing: false,
|
||||
isUser: true,
|
||||
isScene: false,
|
||||
selectorItems: [Sections.Posts, Sections.PostsWithReplies, Sections.Scenes],
|
||||
selectedView: Sections.Posts,
|
||||
setSelectedViewIndex: jest.fn(),
|
||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||
update: jest.fn(),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn(),
|
||||
} as ProfileUiModel
|
||||
|
||||
export const mockedAutocompleteViewStore = {
|
||||
isLoading: false,
|
||||
isActive: true,
|
||||
prefix: '',
|
||||
follows: [
|
||||
{
|
||||
did: 'test did',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: 'app.bsky.system.actorUser',
|
||||
},
|
||||
handle: '',
|
||||
displayName: '',
|
||||
createdAt: '',
|
||||
indexedAt: '',
|
||||
},
|
||||
],
|
||||
searchRes: [
|
||||
{
|
||||
did: 'test did',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: 'app.bsky.system.actorUser',
|
||||
},
|
||||
handle: '',
|
||||
displayName: '',
|
||||
},
|
||||
],
|
||||
knownHandles: new Set<string>(),
|
||||
suggestions: [
|
||||
{
|
||||
handle: 'handle.test',
|
||||
displayName: 'Test Display',
|
||||
},
|
||||
{
|
||||
handle: 'handle2.test',
|
||||
displayName: 'Test Display 2',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
setup: jest.fn(),
|
||||
setActive: jest.fn(),
|
||||
setPrefix: jest.fn(),
|
||||
// unknown required because of the missing private methods: _searchPromise, _getFollows , _search
|
||||
} as unknown as UserAutocompleteViewModel
|
||||
|
||||
export const mockedLocalPhotosStore = {
|
||||
photos: {
|
||||
node: {
|
||||
type: '',
|
||||
group_name: '',
|
||||
image: {
|
||||
filename: '',
|
||||
extension: '',
|
||||
uri: '',
|
||||
height: 1000,
|
||||
width: 1000,
|
||||
fileSize: null,
|
||||
playableDuration: 0,
|
||||
},
|
||||
timestamp: 1672847197,
|
||||
location: null,
|
||||
},
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
setup: jest.fn(),
|
||||
// unknown required because of the missing private methods: _getPhotos
|
||||
} as unknown as UserLocalPhotosModel
|
||||
|
||||
export const mockedSuggestedActorsStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
suggestions: [
|
||||
{
|
||||
did: '1',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: 'app.bsky.system.actorUser',
|
||||
},
|
||||
handle: 'handle1.test',
|
||||
displayName: 'test name 1',
|
||||
description: 'desc',
|
||||
indexedAt: '',
|
||||
_reactKey: '1',
|
||||
},
|
||||
{
|
||||
did: '2',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: 'app.bsky.system.actorUser',
|
||||
},
|
||||
handle: '',
|
||||
displayName: 'handle2.test',
|
||||
description: 'desc',
|
||||
indexedAt: '',
|
||||
_reactKey: '2',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn().mockResolvedValue(null),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _appendAll, _append
|
||||
} as unknown as SuggestedActorsViewModel
|
||||
|
||||
export const mockedUserFollowersStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
params: {
|
||||
user: 'test user',
|
||||
},
|
||||
subject: {
|
||||
did: 'test did',
|
||||
handle: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
followers: [
|
||||
{
|
||||
did: 'test did',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
handle: 'testhandle',
|
||||
displayName: 'test name',
|
||||
indexedAt: '',
|
||||
_reactKey: '1',
|
||||
},
|
||||
{
|
||||
did: 'test did2',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
handle: 'testhandle2',
|
||||
displayName: 'test name 2',
|
||||
indexedAt: '',
|
||||
_reactKey: '2',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn(),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xIdle, _xLoading, _fetch, _replaceAll, _append
|
||||
} as unknown as UserFollowersViewModel
|
||||
|
||||
export const mockedUserFollowsStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
params: {
|
||||
user: 'test user',
|
||||
},
|
||||
subject: {
|
||||
did: 'test did',
|
||||
handle: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
follows: [
|
||||
{
|
||||
did: 'test did',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
handle: 'testhandle',
|
||||
displayName: 'test name',
|
||||
indexedAt: '',
|
||||
_reactKey: '1',
|
||||
},
|
||||
{
|
||||
did: 'test did2',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
handle: 'testhandle2',
|
||||
displayName: 'test name 2',
|
||||
indexedAt: '',
|
||||
_reactKey: '2',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn(),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xIdle, _xLoading, _fetch, _replaceAll, _append
|
||||
} as unknown as UserFollowsViewModel
|
||||
|
||||
export const mockedRepostedByViewStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
resolvedUri: '',
|
||||
params: {
|
||||
uri: 'testuri',
|
||||
},
|
||||
uri: '',
|
||||
repostedBy: [
|
||||
{
|
||||
_reactKey: '',
|
||||
did: '',
|
||||
handle: '',
|
||||
displayName: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
indexedAt: '',
|
||||
},
|
||||
],
|
||||
hasContent: false,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn().mockResolvedValue({}),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn().mockResolvedValue({}),
|
||||
// unknown required because of the missing private methods: _xIdle, _xLoading, _resolveUri, _fetch, _refresh, _replaceAll, _append
|
||||
} as unknown as RepostedByViewModel
|
||||
|
||||
export const mockedVotesViewStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
resolvedUri: '',
|
||||
params: {
|
||||
uri: 'testuri',
|
||||
},
|
||||
uri: '',
|
||||
votes: [
|
||||
{
|
||||
_reactKey: '',
|
||||
direction: 'up',
|
||||
indexedAt: '',
|
||||
createdAt: '',
|
||||
actor: {
|
||||
did: '',
|
||||
handle: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
},
|
||||
],
|
||||
hasContent: false,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn(),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn().mockResolvedValue({}),
|
||||
// unknown required because of the missing private methods: _xIdle, _xLoading, _resolveUri, _fetch, _replaceAll, _append
|
||||
} as unknown as VotesViewModel
|
||||
@@ -1,2 +0,0 @@
|
||||
export const DropdownMenu = jest.fn().mockImplementation(() => {})
|
||||
export const create = jest.fn().mockImplementation(() => {})
|
||||
@@ -0,0 +1,257 @@
|
||||
import React from 'react'
|
||||
import {MobileShell} from '../src/view/shell/mobile'
|
||||
import {cleanup, fireEvent, render, waitFor} from '../jest/test-utils'
|
||||
import {createServer, TestPDS} from '../jest/test-pds'
|
||||
import {RootStoreModel, setupState} from '../src/state'
|
||||
|
||||
const WAIT_OPTS = {timeout: 5e3}
|
||||
|
||||
describe('Account flows', () => {
|
||||
let pds: TestPDS | undefined
|
||||
let rootStore: RootStoreModel | undefined
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers()
|
||||
pds = await createServer()
|
||||
rootStore = await setupState(pds.pdsUrl)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
await pds?.close()
|
||||
})
|
||||
|
||||
it('renders initial screen', () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
const signUpScreen = getByTestId('signinOrCreateAccount')
|
||||
|
||||
expect(signUpScreen).toBeTruthy()
|
||||
})
|
||||
|
||||
it('completes signin to the server', async () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
|
||||
// move to signin view
|
||||
fireEvent.press(getByTestId('signInButton'))
|
||||
expect(getByTestId('signIn')).toBeTruthy()
|
||||
expect(getByTestId('loginForm')).toBeTruthy()
|
||||
|
||||
// input the target server
|
||||
expect(getByTestId('loginSelectServiceButton')).toBeTruthy()
|
||||
fireEvent.press(getByTestId('loginSelectServiceButton'))
|
||||
expect(getByTestId('serverInputModal')).toBeTruthy()
|
||||
fireEvent.changeText(
|
||||
getByTestId('customServerTextInput'),
|
||||
pds?.pdsUrl || '',
|
||||
)
|
||||
fireEvent.press(getByTestId('customServerSelectBtn'))
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('loginUsernameInput')).toBeTruthy()
|
||||
}, WAIT_OPTS)
|
||||
|
||||
// enter username & pass
|
||||
fireEvent.changeText(getByTestId('loginUsernameInput'), 'alice')
|
||||
fireEvent.changeText(getByTestId('loginPasswordInput'), 'hunter2')
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('loginNextButton')).toBeTruthy()
|
||||
}, WAIT_OPTS)
|
||||
fireEvent.press(getByTestId('loginNextButton'))
|
||||
|
||||
// signed in
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('homeFeed')).toBeTruthy()
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(1)
|
||||
}, WAIT_OPTS)
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(1)
|
||||
})
|
||||
|
||||
it('opens the login screen when "add account" is pressed', async () => {
|
||||
const {getByTestId, getAllByTestId} = render(<MobileShell />, rootStore)
|
||||
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// open side menu
|
||||
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
|
||||
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// nav to settings
|
||||
fireEvent.press(getByTestId('menuItemButton-Settings'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// press '+ new account' in switcher
|
||||
fireEvent.press(getByTestId('switchToNewAccountBtn'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the "choose account" form when a previous session has been created', async () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
|
||||
// move to signin view
|
||||
fireEvent.press(getByTestId('signInButton'))
|
||||
expect(getByTestId('signIn')).toBeTruthy()
|
||||
expect(getByTestId('chooseAccountForm')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('logs directly into the account due to still possessing session tokens', async () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
|
||||
// move to signin view
|
||||
fireEvent.press(getByTestId('signInButton'))
|
||||
expect(getByTestId('signIn')).toBeTruthy()
|
||||
expect(getByTestId('chooseAccountForm')).toBeTruthy()
|
||||
|
||||
// select the previous account
|
||||
fireEvent.press(getByTestId('chooseAccountBtn-alice.test'))
|
||||
|
||||
// signs in immediately
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('homeFeed')).toBeTruthy()
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(1)
|
||||
}, WAIT_OPTS)
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(1)
|
||||
})
|
||||
|
||||
it('logs into a second account via the switcher', async () => {
|
||||
const {getByTestId, getAllByTestId} = render(<MobileShell />, rootStore)
|
||||
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// open side menu
|
||||
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
|
||||
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// nav to settings
|
||||
fireEvent.press(getByTestId('menuItemButton-Settings'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// press '+ new account' in switcher
|
||||
fireEvent.press(getByTestId('switchToNewAccountBtn'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// move to signin view
|
||||
fireEvent.press(getByTestId('signInButton'))
|
||||
expect(getByTestId('signIn')).toBeTruthy()
|
||||
expect(getByTestId('chooseAccountForm')).toBeTruthy()
|
||||
|
||||
// select a new account
|
||||
fireEvent.press(getByTestId('chooseNewAccountBtn'))
|
||||
expect(getByTestId('loginForm')).toBeTruthy()
|
||||
|
||||
// input the target server
|
||||
expect(getByTestId('loginSelectServiceButton')).toBeTruthy()
|
||||
fireEvent.press(getByTestId('loginSelectServiceButton'))
|
||||
expect(getByTestId('serverInputModal')).toBeTruthy()
|
||||
fireEvent.changeText(
|
||||
getByTestId('customServerTextInput'),
|
||||
pds?.pdsUrl || '',
|
||||
)
|
||||
fireEvent.press(getByTestId('customServerSelectBtn'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('loginUsernameInput')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// enter username & pass
|
||||
fireEvent.changeText(getByTestId('loginUsernameInput'), 'bob')
|
||||
fireEvent.changeText(getByTestId('loginPasswordInput'), 'hunter2')
|
||||
await waitFor(
|
||||
() => expect(getByTestId('loginNextButton')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
fireEvent.press(getByTestId('loginNextButton'))
|
||||
|
||||
// signed in
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('homeFeed')).toBeTruthy() // we go back to settings in this situation
|
||||
expect(rootStore?.me?.displayName).toBe('Bob')
|
||||
expect(rootStore?.me?.handle).toBe('bob.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(2)
|
||||
}, WAIT_OPTS)
|
||||
expect(rootStore?.me?.displayName).toBe('Bob')
|
||||
expect(rootStore?.me?.handle).toBe('bob.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(2)
|
||||
})
|
||||
|
||||
it('can instantly switch between accounts', async () => {
|
||||
const {getAllByTestId, getByTestId} = render(<MobileShell />, rootStore)
|
||||
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// open side menu
|
||||
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
|
||||
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// nav to settings
|
||||
fireEvent.press(getByTestId('menuItemButton-Settings'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// select the alice account
|
||||
fireEvent.press(getByTestId('switchToAccountBtn-alice.test'))
|
||||
|
||||
// swapped account
|
||||
await waitFor(() => {
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(2)
|
||||
}, WAIT_OPTS)
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(2)
|
||||
})
|
||||
|
||||
it('will prompt for a password if you sign out', async () => {
|
||||
const {getAllByTestId, getByTestId} = render(<MobileShell />, rootStore)
|
||||
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// open side menu
|
||||
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
|
||||
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// nav to settings
|
||||
fireEvent.press(getByTestId('menuItemButton-Settings'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// press the sign out button
|
||||
fireEvent.press(getByTestId('signOutBtn'))
|
||||
|
||||
// in the logged out state
|
||||
await waitFor(
|
||||
() => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// move to signin view
|
||||
fireEvent.press(getByTestId('signInButton'))
|
||||
expect(getByTestId('signIn')).toBeTruthy()
|
||||
expect(getByTestId('chooseAccountForm')).toBeTruthy()
|
||||
|
||||
// select an existing account
|
||||
fireEvent.press(getByTestId('chooseAccountBtn-alice.test'))
|
||||
|
||||
// goes to login screen instead of straight back to settings
|
||||
expect(getByTestId('loginForm')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
File diff suppressed because one or more lines are too long
@@ -1,47 +0,0 @@
|
||||
import {bundleAsync} from '../../../src/lib/async/bundle'
|
||||
|
||||
describe('bundle', () => {
|
||||
it('bundles multiple simultaneous calls into one execution', async () => {
|
||||
let calls = 0
|
||||
const fn = bundleAsync(async () => {
|
||||
calls++
|
||||
await new Promise(r => setTimeout(r, 1))
|
||||
return 'hello'
|
||||
})
|
||||
const [res1, res2, res3] = await Promise.all([fn(), fn(), fn()])
|
||||
expect(calls).toEqual(1)
|
||||
expect(res1).toEqual('hello')
|
||||
expect(res2).toEqual('hello')
|
||||
expect(res3).toEqual('hello')
|
||||
})
|
||||
it('does not bundle non-simultaneous calls', async () => {
|
||||
let calls = 0
|
||||
const fn = bundleAsync(async () => {
|
||||
calls++
|
||||
await new Promise(r => setTimeout(r, 1))
|
||||
return 'hello'
|
||||
})
|
||||
const res1 = await fn()
|
||||
const res2 = await fn()
|
||||
const res3 = await fn()
|
||||
expect(calls).toEqual(3)
|
||||
expect(res1).toEqual('hello')
|
||||
expect(res2).toEqual('hello')
|
||||
expect(res3).toEqual('hello')
|
||||
})
|
||||
it('is not affected by rejections', async () => {
|
||||
let calls = 0
|
||||
const fn = bundleAsync(async () => {
|
||||
calls++
|
||||
await new Promise(r => setTimeout(r, 1))
|
||||
throw new Error()
|
||||
})
|
||||
const res1 = await fn().catch(() => 'reject')
|
||||
const res2 = await fn().catch(() => 'reject')
|
||||
const res3 = await fn().catch(() => 'reject')
|
||||
expect(calls).toEqual(3)
|
||||
expect(res1).toEqual('reject')
|
||||
expect(res2).toEqual('reject')
|
||||
expect(res3).toEqual('reject')
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import {isNetworkError} from '../../src/lib/strings/errors'
|
||||
import {isNetworkError} from '../../src/lib/errors'
|
||||
|
||||
describe('isNetworkError', () => {
|
||||
const inputs = [
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import {extractHtmlMeta} from '../../src/lib/link-meta/html'
|
||||
import {extractHtmlMeta} from '../../src/lib/extractHtmlMeta'
|
||||
import {exampleComHtml} from './__mocks__/exampleComHtml'
|
||||
import {youtubeHTML} from './__mocks__/youtubeHtml'
|
||||
import {tiktokHtml} from './__mocks__/tiktokHtml'
|
||||
import {youtubeChannelHtml} from './__mocks__/youtubeChannelHtml'
|
||||
|
||||
describe('extractHtmlMeta', () => {
|
||||
const cases = [
|
||||
@@ -42,7 +41,6 @@ describe('extractHtmlMeta', () => {
|
||||
|
||||
it.each(cases)(
|
||||
'given the html tag %p, returns %p',
|
||||
// @ts-ignore not worth fixing -prf
|
||||
(input, expectedResult) => {
|
||||
const output = extractHtmlMeta({html: input as string, hostname: ''})
|
||||
expect(output).toEqual(expectedResult)
|
||||
@@ -83,25 +81,11 @@ describe('extractHtmlMeta', () => {
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('extracts avatar from a youtube channel', () => {
|
||||
const input = youtubeChannelHtml
|
||||
const expectedOutput = {
|
||||
title: 'penguinz0',
|
||||
description:
|
||||
'Clips channel: https://www.youtube.com/channel/UC4EQHfzIbkL_Skit_iKt1aA\n\nTwitter: https://twitter.com/MoistCr1TiKaL\n\nInstagram: https://www.instagram.com/bigmoistcr1tikal/?hl=en\n\nTwitch: https://www.twitch.tv/moistcr1tikal\n\nSnapchat: Hugecharles\n\nTik Tok: Hugecharles\n\nI don't have any other public accounts.',
|
||||
image:
|
||||
'https://yt3.googleusercontent.com/ytc/AL5GRJWOhJOuUC6C2b7gP-5D2q6ypXbcOOckyAE1En4RUQ=s176-c-k-c0x00ffffff-no-rj',
|
||||
}
|
||||
const output = extractHtmlMeta({html: input, hostname: 'youtube.com'})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('extracts username from the url a twitter profile page', () => {
|
||||
const expectedOutput = {
|
||||
title: '@bluesky on Twitter',
|
||||
}
|
||||
const output = extractHtmlMeta({
|
||||
html: '',
|
||||
hostname: 'twitter.com',
|
||||
pathname: '/bluesky',
|
||||
})
|
||||
@@ -113,7 +97,6 @@ describe('extractHtmlMeta', () => {
|
||||
title: 'Tweet by @bluesky',
|
||||
}
|
||||
const output = extractHtmlMeta({
|
||||
html: '',
|
||||
hostname: 'twitter.com',
|
||||
pathname: '/bluesky/status/1582437529969917953',
|
||||
})
|
||||
@@ -125,7 +108,6 @@ describe('extractHtmlMeta', () => {
|
||||
title: 'Twitter',
|
||||
}
|
||||
const output = extractHtmlMeta({
|
||||
html: '',
|
||||
hostname: 'twitter.com',
|
||||
pathname: '/i/articles/follows/-1675653703?time_window=24',
|
||||
})
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import {downloadAndResize, DownloadAndResizeOpts} from '../../src/lib/images'
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
import {
|
||||
downloadAndResize,
|
||||
DownloadAndResizeOpts,
|
||||
} from '../../src/lib/media/manip'
|
||||
|
||||
describe('downloadAndResize', () => {
|
||||
const errorSpy = jest.spyOn(global.console, 'error')
|
||||
|
||||
@@ -31,7 +27,6 @@ describe('downloadAndResize', () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
||||
info: jest.fn().mockReturnValue({status: 200}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
@@ -83,15 +78,8 @@ describe('downloadAndResize', () => {
|
||||
})
|
||||
|
||||
it('should return undefined for unsupported file type', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
||||
info: jest.fn().mockReturnValue({status: 200}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
uri: 'https://example.com/image',
|
||||
uri: 'https://example.com/image.bmp',
|
||||
width: 100,
|
||||
height: 100,
|
||||
maxSize: 500000,
|
||||
@@ -100,47 +88,6 @@ describe('downloadAndResize', () => {
|
||||
}
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(result).toEqual(mockResizedImage)
|
||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
||||
fileCache: true,
|
||||
appendExt: 'jpeg',
|
||||
})
|
||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'https://example.com/image',
|
||||
)
|
||||
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
|
||||
'file://downloaded-image',
|
||||
100,
|
||||
100,
|
||||
'JPEG',
|
||||
100,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{mode: 'cover'},
|
||||
)
|
||||
})
|
||||
|
||||
it('should return undefined for non-200 response', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
||||
info: jest.fn().mockReturnValue({status: 400}),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
uri: 'https://example.com/image',
|
||||
width: 100,
|
||||
height: 100,
|
||||
maxSize: 500000,
|
||||
mode: 'cover',
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(errorSpy).not.toHaveBeenCalled()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,98 @@
|
||||
import {LikelyType, getLikelyType} from '../../src/lib/link-meta/link-meta'
|
||||
import {LikelyType, getLinkMeta, getLikelyType} from '../../src/lib/link-meta'
|
||||
import {exampleComHtml} from './__mocks__/exampleComHtml'
|
||||
import {mockedRootStore} from '../../__mocks__/state-mock'
|
||||
|
||||
describe('getLinkMeta', () => {
|
||||
const inputs = [
|
||||
'',
|
||||
'httpbadurl',
|
||||
'https://example.com',
|
||||
'https://example.com/index.html',
|
||||
'https://example.com/image.png',
|
||||
'https://example.com/video.avi',
|
||||
'https://example.com/audio.ogg',
|
||||
'https://example.com/text.txt',
|
||||
'https://example.com/javascript.js',
|
||||
'https://bsky.app/',
|
||||
'https://bsky.app/index.html',
|
||||
]
|
||||
const outputs = [
|
||||
{
|
||||
error: 'Invalid URL',
|
||||
likelyType: LikelyType.Other,
|
||||
url: '',
|
||||
},
|
||||
{
|
||||
error: 'Invalid URL',
|
||||
likelyType: LikelyType.Other,
|
||||
url: 'httpbadurl',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.HTML,
|
||||
url: 'https://example.com',
|
||||
title: 'Example Domain',
|
||||
description: 'An example website',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.HTML,
|
||||
url: 'https://example.com/index.html',
|
||||
title: 'Example Domain',
|
||||
description: 'An example website',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.Image,
|
||||
url: 'https://example.com/image.png',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.Video,
|
||||
url: 'https://example.com/video.avi',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.Audio,
|
||||
url: 'https://example.com/audio.ogg',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.Text,
|
||||
url: 'https://example.com/text.txt',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.Other,
|
||||
url: 'https://example.com/javascript.js',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.AtpData,
|
||||
url: '/',
|
||||
title: 'Bluesky',
|
||||
description: 'A new kind of social network',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.AtpData,
|
||||
url: '/index.html',
|
||||
title: 'Not found',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.Other,
|
||||
url: '',
|
||||
title: '',
|
||||
},
|
||||
]
|
||||
it('correctly handles a set of text inputs', async () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
global.fetch = jest.fn().mockImplementationOnce(() => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: () => exampleComHtml,
|
||||
})
|
||||
})
|
||||
})
|
||||
const input = inputs[i]
|
||||
const output = await getLinkMeta(mockedRootStore, input)
|
||||
expect(output).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLikelyType', () => {
|
||||
it('correctly handles non-parsed url', async () => {
|
||||
|
||||
+229
-727
File diff suppressed because it is too large
Load Diff
@@ -1,84 +0,0 @@
|
||||
import {
|
||||
getMentionAt,
|
||||
insertMentionAt,
|
||||
} from '../../../src/lib/strings/mention-manip'
|
||||
|
||||
describe('getMentionAt', () => {
|
||||
type Case = [string, number, string | undefined]
|
||||
const cases: Case[] = [
|
||||
['hello @alice goodbye', 0, undefined],
|
||||
['hello @alice goodbye', 1, undefined],
|
||||
['hello @alice goodbye', 2, undefined],
|
||||
['hello @alice goodbye', 3, undefined],
|
||||
['hello @alice goodbye', 4, undefined],
|
||||
['hello @alice goodbye', 5, undefined],
|
||||
['hello @alice goodbye', 6, 'alice'],
|
||||
['hello @alice goodbye', 7, 'alice'],
|
||||
['hello @alice goodbye', 8, 'alice'],
|
||||
['hello @alice goodbye', 9, 'alice'],
|
||||
['hello @alice goodbye', 10, 'alice'],
|
||||
['hello @alice goodbye', 11, 'alice'],
|
||||
['hello @alice goodbye', 12, 'alice'],
|
||||
['hello @alice goodbye', 13, undefined],
|
||||
['hello @alice goodbye', 14, undefined],
|
||||
['@alice', 0, 'alice'],
|
||||
['@alice hello', 0, 'alice'],
|
||||
['@alice hello', 1, 'alice'],
|
||||
['@alice hello', 2, 'alice'],
|
||||
['@alice hello', 3, 'alice'],
|
||||
['@alice hello', 4, 'alice'],
|
||||
['@alice hello', 5, 'alice'],
|
||||
['@alice hello', 6, 'alice'],
|
||||
['@alice hello', 7, undefined],
|
||||
['alice@alice', 0, undefined],
|
||||
['alice@alice', 6, undefined],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
'given input string %p and cursor position %p, returns %p',
|
||||
(str, cursorPos, expected) => {
|
||||
const output = getMentionAt(str, cursorPos)
|
||||
expect(output?.value).toEqual(expected)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('insertMentionAt', () => {
|
||||
type Case = [string, number, string]
|
||||
const cases: Case[] = [
|
||||
['hello @alice goodbye', 0, 'hello @alice goodbye'],
|
||||
['hello @alice goodbye', 1, 'hello @alice goodbye'],
|
||||
['hello @alice goodbye', 2, 'hello @alice goodbye'],
|
||||
['hello @alice goodbye', 3, 'hello @alice goodbye'],
|
||||
['hello @alice goodbye', 4, 'hello @alice goodbye'],
|
||||
['hello @alice goodbye', 5, 'hello @alice goodbye'],
|
||||
['hello @alice goodbye', 6, 'hello @alice.com goodbye'],
|
||||
['hello @alice goodbye', 7, 'hello @alice.com goodbye'],
|
||||
['hello @alice goodbye', 8, 'hello @alice.com goodbye'],
|
||||
['hello @alice goodbye', 9, 'hello @alice.com goodbye'],
|
||||
['hello @alice goodbye', 10, 'hello @alice.com goodbye'],
|
||||
['hello @alice goodbye', 11, 'hello @alice.com goodbye'],
|
||||
['hello @alice goodbye', 12, 'hello @alice.com goodbye'],
|
||||
['hello @alice goodbye', 13, 'hello @alice goodbye'],
|
||||
['hello @alice goodbye', 14, 'hello @alice goodbye'],
|
||||
['@alice', 0, '@alice.com '],
|
||||
['@alice hello', 0, '@alice.com hello'],
|
||||
['@alice hello', 1, '@alice.com hello'],
|
||||
['@alice hello', 2, '@alice.com hello'],
|
||||
['@alice hello', 3, '@alice.com hello'],
|
||||
['@alice hello', 4, '@alice.com hello'],
|
||||
['@alice hello', 5, '@alice.com hello'],
|
||||
['@alice hello', 6, '@alice.com hello'],
|
||||
['@alice hello', 7, '@alice hello'],
|
||||
['alice@alice', 0, 'alice@alice'],
|
||||
['alice@alice', 6, 'alice@alice'],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
'given input string %p and cursor position %p, returns %p',
|
||||
(str, cursorPos, expected) => {
|
||||
const output = insertMentionAt(str, cursorPos, 'alice.com')
|
||||
expect(output).toEqual(expected)
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -1,180 +0,0 @@
|
||||
import {describe, expect, it} from '@jest/globals'
|
||||
|
||||
import {
|
||||
isPossiblyAUrl,
|
||||
isTrustedUrl,
|
||||
linkRequiresWarning,
|
||||
splitApexDomain,
|
||||
} from '../../../src/lib/strings/url-helpers'
|
||||
|
||||
describe('linkRequiresWarning', () => {
|
||||
type Case = [string, string, boolean]
|
||||
|
||||
const cases: Case[] = [
|
||||
['http://example.com', 'http://example.com', false],
|
||||
['http://example.com', 'example.com', false],
|
||||
['http://example.com', 'example.com/page', false],
|
||||
['http://example.com', '', true],
|
||||
['http://example.com', 'other.com', true],
|
||||
['http://example.com', 'http://other.com', true],
|
||||
['http://example.com', 'some label', true],
|
||||
['http://example.com', 'example.com more', true],
|
||||
['http://example.com', 'http://example.co', true],
|
||||
['http://example.co', 'http://example.com', true],
|
||||
['http://example.com', 'example.co', true],
|
||||
['http://example.co', 'example.com', true],
|
||||
['http://site.pages.dev', 'http://site.page', true],
|
||||
['http://site.page', 'http://site.pages.dev', true],
|
||||
['http://site.pages.dev', 'site.page', true],
|
||||
['http://site.page', 'site.pages.dev', true],
|
||||
['http://site.pages.dev', 'http://site.pages', true],
|
||||
['http://site.pages', 'http://site.pages.dev', true],
|
||||
['http://site.pages.dev', 'site.pages', true],
|
||||
['http://site.pages', 'site.pages.dev', true],
|
||||
['http://bsky.app/profile/bob.test/post/3kbeuduu7m22v', 'my post', false],
|
||||
['https://bsky.app/profile/bob.test/post/3kbeuduu7m22v', 'my post', false],
|
||||
['http://bsky.app/', 'bluesky', false],
|
||||
['https://bsky.app/', 'bluesky', false],
|
||||
[
|
||||
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
|
||||
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
|
||||
false,
|
||||
],
|
||||
[
|
||||
'https://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
|
||||
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
|
||||
false,
|
||||
],
|
||||
[
|
||||
'http://bsky.app/',
|
||||
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
|
||||
false,
|
||||
],
|
||||
[
|
||||
'https://bsky.app/',
|
||||
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
|
||||
false,
|
||||
],
|
||||
[
|
||||
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
|
||||
'https://google.com',
|
||||
true,
|
||||
],
|
||||
[
|
||||
'https://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
|
||||
'https://google.com',
|
||||
true,
|
||||
],
|
||||
['http://bsky.app/', 'https://google.com', true],
|
||||
['https://bsky.app/', 'https://google.com', true],
|
||||
|
||||
// case insensitive
|
||||
['https://Example.com', 'example.com', false],
|
||||
['https://example.com', 'Example.com', false],
|
||||
|
||||
// bad uri inputs, default to true
|
||||
['', '', true],
|
||||
['example.com', 'example.com', true],
|
||||
['/profile', 'Username', false],
|
||||
['#', 'Show More', false],
|
||||
['https://docs.bsky.app', 'https://docs.bsky.app', false],
|
||||
['https://bsky.app/compose/intent?text=test', 'Compose a post', false],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
'given input uri %p and text %p, returns %p',
|
||||
(uri, text, expected) => {
|
||||
const output = linkRequiresWarning(uri, text)
|
||||
expect(output).toEqual(expected)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('isPossiblyAUrl', () => {
|
||||
type Case = [string, boolean]
|
||||
const cases: Case[] = [
|
||||
['', false],
|
||||
['text', false],
|
||||
['some text', false],
|
||||
['some text', false],
|
||||
['some domain.com', false],
|
||||
['domain.com', true],
|
||||
[' domain.com', true],
|
||||
['domain.com ', true],
|
||||
[' domain.com ', true],
|
||||
['http://domain.com', true],
|
||||
[' http://domain.com', true],
|
||||
['http://domain.com ', true],
|
||||
[' http://domain.com ', true],
|
||||
['https://domain.com', true],
|
||||
[' https://domain.com', true],
|
||||
['https://domain.com ', true],
|
||||
[' https://domain.com ', true],
|
||||
['http://domain.com/foo', true],
|
||||
['http://domain.com stuff', true],
|
||||
]
|
||||
|
||||
it.each(cases)('given input uri %p, returns %p', (str, expected) => {
|
||||
const output = isPossiblyAUrl(str)
|
||||
expect(output).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('splitApexDomain', () => {
|
||||
type Case = [string, string, string]
|
||||
const cases: Case[] = [
|
||||
['', '', ''],
|
||||
['example.com', '', 'example.com'],
|
||||
['foo.example.com', 'foo.', 'example.com'],
|
||||
['foo.bar.example.com', 'foo.bar.', 'example.com'],
|
||||
['example.co.uk', '', 'example.co.uk'],
|
||||
['foo.example.co.uk', 'foo.', 'example.co.uk'],
|
||||
['example.nonsense', '', 'example.nonsense'],
|
||||
['foo.example.nonsense', '', 'foo.example.nonsense'],
|
||||
['foo.bar.example.nonsense', '', 'foo.bar.example.nonsense'],
|
||||
['example.com.example.com', 'example.com.', 'example.com'],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
'given input uri %p, returns %p,%p',
|
||||
(str, expected1, expected2) => {
|
||||
const output = splitApexDomain(str)
|
||||
expect(output[0]).toEqual(expected1)
|
||||
expect(output[1]).toEqual(expected2)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
describe('isTrustedUrl', () => {
|
||||
const cases = [
|
||||
['#', true],
|
||||
['#profile', true],
|
||||
['/', true],
|
||||
['/profile', true],
|
||||
['/profile/', true],
|
||||
['/profile/bob.test', true],
|
||||
['https://bsky.app', true],
|
||||
['https://bsky.app/', true],
|
||||
['https://bsky.app/profile/bob.test', true],
|
||||
['https://www.bsky.app', true],
|
||||
['https://www.bsky.app/', true],
|
||||
['https://docs.bsky.app', true],
|
||||
['https://bsky.social', true],
|
||||
['https://bsky.social/blog', true],
|
||||
['https://blueskyweb.xyz', true],
|
||||
['https://blueskyweb.zendesk.com', true],
|
||||
['http://bsky.app', true],
|
||||
['http://bsky.social', true],
|
||||
['http://blueskyweb.xyz', true],
|
||||
['http://blueskyweb.zendesk.com', true],
|
||||
['https://google.com', false],
|
||||
['https://docs.google.com', false],
|
||||
['https://google.com/#', false],
|
||||
['https://blueskywebxzendesk.com', false],
|
||||
]
|
||||
|
||||
it.each(cases)('given input uri %p, returns %p', (str, expected) => {
|
||||
const output = isTrustedUrl(str)
|
||||
expect(output).toEqual(expected)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import {RootStoreModel} from '../../../src/state/models/root-store'
|
||||
import {LinkMetasViewModel} from '../../../src/state/models/link-metas-view'
|
||||
import * as LinkMetaLib from '../../../src/lib/link-meta'
|
||||
import {LikelyType} from './../../../src/lib/link-meta'
|
||||
import {sessionClient, SessionServiceClient} from '@atproto/api'
|
||||
import {DEFAULT_SERVICE} from '../../../src/state'
|
||||
|
||||
describe('LinkMetasViewModel', () => {
|
||||
let viewModel: LinkMetasViewModel
|
||||
let rootStore: RootStoreModel
|
||||
|
||||
const getLinkMetaMockSpy = jest.spyOn(LinkMetaLib, 'getLinkMeta')
|
||||
const mockedMeta = {
|
||||
title: 'Test Title',
|
||||
url: 'testurl',
|
||||
likelyType: LikelyType.Other,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const api = sessionClient.service(DEFAULT_SERVICE) as SessionServiceClient
|
||||
rootStore = new RootStoreModel(api)
|
||||
viewModel = new LinkMetasViewModel(rootStore)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('getLinkMeta', () => {
|
||||
it('should return link meta if it is cached', async () => {
|
||||
const url = 'http://example.com'
|
||||
|
||||
viewModel.cache.set(url, mockedMeta)
|
||||
|
||||
const result = await viewModel.getLinkMeta(url)
|
||||
|
||||
expect(getLinkMetaMockSpy).not.toHaveBeenCalled()
|
||||
expect(result).toEqual(mockedMeta)
|
||||
})
|
||||
|
||||
it('should return link meta if it is not cached', async () => {
|
||||
getLinkMetaMockSpy.mockResolvedValueOnce(mockedMeta)
|
||||
|
||||
const result = await viewModel.getLinkMeta(mockedMeta.url)
|
||||
|
||||
expect(getLinkMetaMockSpy).toHaveBeenCalledWith(rootStore, mockedMeta.url)
|
||||
expect(result).toEqual(mockedMeta)
|
||||
})
|
||||
|
||||
it('should cache the link meta if it is successfully returned', async () => {
|
||||
getLinkMetaMockSpy.mockResolvedValueOnce(mockedMeta)
|
||||
|
||||
await viewModel.getLinkMeta(mockedMeta.url)
|
||||
|
||||
expect(viewModel.cache.get(mockedMeta.url)).toEqual(mockedMeta)
|
||||
})
|
||||
|
||||
it('should not cache the link meta if it fails to return', async () => {
|
||||
const url = 'http://example.com'
|
||||
const error = new Error('Failed to fetch link meta')
|
||||
getLinkMetaMockSpy.mockRejectedValueOnce(error)
|
||||
|
||||
try {
|
||||
await viewModel.getLinkMeta(url)
|
||||
fail('Error was not thrown')
|
||||
} catch (e) {
|
||||
expect(e).toEqual(error)
|
||||
expect(viewModel.cache.get(url)).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,153 @@
|
||||
import {LogModel} from '../../../src/state/models/log'
|
||||
|
||||
describe('LogModel', () => {
|
||||
let logModel: LogModel
|
||||
|
||||
beforeEach(() => {
|
||||
logModel = new LogModel()
|
||||
jest.spyOn(console, 'debug')
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call a log method and add a log entry to the entries array', () => {
|
||||
logModel.debug('Test log')
|
||||
expect(logModel.entries.length).toEqual(1)
|
||||
expect(logModel.entries[0]).toEqual({
|
||||
id: logModel.entries[0].id,
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: logModel.entries[0].ts,
|
||||
})
|
||||
|
||||
logModel.warn('Test log')
|
||||
expect(logModel.entries.length).toEqual(2)
|
||||
expect(logModel.entries[1]).toEqual({
|
||||
id: logModel.entries[1].id,
|
||||
type: 'warn',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: logModel.entries[1].ts,
|
||||
})
|
||||
|
||||
logModel.error('Test log')
|
||||
expect(logModel.entries.length).toEqual(3)
|
||||
expect(logModel.entries[2]).toEqual({
|
||||
id: logModel.entries[2].id,
|
||||
type: 'error',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: logModel.entries[2].ts,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the console.debug after calling the debug method', () => {
|
||||
logModel.debug('Test log')
|
||||
expect(console.debug).toHaveBeenCalledWith('Test log', '')
|
||||
})
|
||||
|
||||
it('should call the serialize method', () => {
|
||||
logModel.debug('Test log')
|
||||
expect(logModel.serialize()).toEqual({
|
||||
entries: [
|
||||
{
|
||||
id: logModel.entries[0].id,
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: logModel.entries[0].ts,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the hydrate method with valid properties', () => {
|
||||
logModel.hydrate({
|
||||
entries: [
|
||||
{
|
||||
id: '123',
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: 123,
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(logModel.entries).toEqual([
|
||||
{
|
||||
id: '123',
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: 123,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should call the hydrate method with invalid properties', () => {
|
||||
logModel.hydrate({
|
||||
entries: [
|
||||
{
|
||||
id: '123',
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: 123,
|
||||
},
|
||||
{
|
||||
summary: 'Invalid entry',
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(logModel.entries).toEqual([
|
||||
{
|
||||
id: '123',
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: 123,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should stringify the details if it is not a string', () => {
|
||||
logModel.debug('Test log', {details: 'test'})
|
||||
expect(logModel.entries[0].details).toEqual('{\n "details": "test"\n}')
|
||||
})
|
||||
|
||||
it('should stringify the details object if it is of a specific error', () => {
|
||||
class TestError extends Error {
|
||||
constructor() {
|
||||
super()
|
||||
this.name = 'TestError'
|
||||
}
|
||||
}
|
||||
const error = new TestError()
|
||||
logModel.error('Test error log', error)
|
||||
expect(logModel.entries[0].details).toEqual('TestError')
|
||||
|
||||
class XRPCInvalidResponseErrorMock {
|
||||
validationError = {toString: () => 'validationError'}
|
||||
lexiconNsid = 'test'
|
||||
}
|
||||
const xrpcInvalidResponseError = new XRPCInvalidResponseErrorMock()
|
||||
logModel.error('Test error log', xrpcInvalidResponseError)
|
||||
expect(logModel.entries[1].details).toEqual(
|
||||
'{\n "validationError": {},\n "lexiconNsid": "test"\n}',
|
||||
)
|
||||
|
||||
class XRPCErrorMock {
|
||||
status = 'status'
|
||||
error = 'error'
|
||||
message = 'message'
|
||||
}
|
||||
const xrpcError = new XRPCErrorMock()
|
||||
logModel.error('Test error log', xrpcError)
|
||||
expect(logModel.entries[2].details).toEqual(
|
||||
'{\n "status": "status",\n "error": "error",\n "message": "message"\n}',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,180 @@
|
||||
import {RootStoreModel} from '../../../src/state/models/root-store'
|
||||
import {MeModel} from '../../../src/state/models/me'
|
||||
import {NotificationsViewModel} from './../../../src/state/models/notifications-view'
|
||||
import {sessionClient, SessionServiceClient} from '@atproto/api'
|
||||
import {DEFAULT_SERVICE} from './../../../src/state/index'
|
||||
|
||||
describe('MeModel', () => {
|
||||
let rootStore: RootStoreModel
|
||||
let meModel: MeModel
|
||||
|
||||
beforeEach(() => {
|
||||
const api = sessionClient.service(DEFAULT_SERVICE) as SessionServiceClient
|
||||
rootStore = new RootStoreModel(api)
|
||||
meModel = new MeModel(rootStore)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should clear() correctly', () => {
|
||||
meModel.did = '123'
|
||||
meModel.handle = 'handle'
|
||||
meModel.displayName = 'John Doe'
|
||||
meModel.description = 'description'
|
||||
meModel.avatar = 'avatar'
|
||||
meModel.notificationCount = 1
|
||||
meModel.clear()
|
||||
expect(meModel.did).toEqual('')
|
||||
expect(meModel.handle).toEqual('')
|
||||
expect(meModel.displayName).toEqual('')
|
||||
expect(meModel.description).toEqual('')
|
||||
expect(meModel.avatar).toEqual('')
|
||||
expect(meModel.notificationCount).toEqual(0)
|
||||
})
|
||||
|
||||
it('should hydrate() successfully with valid properties', () => {
|
||||
meModel.hydrate({
|
||||
did: '123',
|
||||
handle: 'handle',
|
||||
displayName: 'John Doe',
|
||||
description: 'description',
|
||||
avatar: 'avatar',
|
||||
})
|
||||
expect(meModel.did).toEqual('123')
|
||||
expect(meModel.handle).toEqual('handle')
|
||||
expect(meModel.displayName).toEqual('John Doe')
|
||||
expect(meModel.description).toEqual('description')
|
||||
expect(meModel.avatar).toEqual('avatar')
|
||||
})
|
||||
|
||||
it('should not hydrate() with invalid properties', () => {
|
||||
meModel.hydrate({
|
||||
did: '',
|
||||
handle: 'handle',
|
||||
displayName: 'John Doe',
|
||||
description: 'description',
|
||||
avatar: 'avatar',
|
||||
})
|
||||
expect(meModel.did).toEqual('')
|
||||
expect(meModel.handle).toEqual('')
|
||||
expect(meModel.displayName).toEqual('')
|
||||
expect(meModel.description).toEqual('')
|
||||
expect(meModel.avatar).toEqual('')
|
||||
|
||||
meModel.hydrate({
|
||||
did: '123',
|
||||
displayName: 'John Doe',
|
||||
description: 'description',
|
||||
avatar: 'avatar',
|
||||
})
|
||||
expect(meModel.did).toEqual('')
|
||||
expect(meModel.handle).toEqual('')
|
||||
expect(meModel.displayName).toEqual('')
|
||||
expect(meModel.description).toEqual('')
|
||||
expect(meModel.avatar).toEqual('')
|
||||
})
|
||||
|
||||
it('should load() successfully', async () => {
|
||||
jest
|
||||
.spyOn(rootStore.api.app.bsky.actor, 'getProfile')
|
||||
.mockImplementationOnce((): Promise<any> => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
displayName: 'John Doe',
|
||||
description: 'description',
|
||||
avatar: 'avatar',
|
||||
},
|
||||
})
|
||||
})
|
||||
rootStore.session.data = {
|
||||
did: '123',
|
||||
handle: 'handle',
|
||||
service: 'test service',
|
||||
accessJwt: 'test token',
|
||||
refreshJwt: 'test token',
|
||||
}
|
||||
await meModel.load()
|
||||
expect(meModel.did).toEqual('123')
|
||||
expect(meModel.handle).toEqual('handle')
|
||||
expect(meModel.displayName).toEqual('John Doe')
|
||||
expect(meModel.description).toEqual('description')
|
||||
expect(meModel.avatar).toEqual('avatar')
|
||||
})
|
||||
|
||||
it('should load() successfully without profile data', async () => {
|
||||
jest
|
||||
.spyOn(rootStore.api.app.bsky.actor, 'getProfile')
|
||||
.mockImplementationOnce((): Promise<any> => {
|
||||
return Promise.resolve({
|
||||
data: null,
|
||||
})
|
||||
})
|
||||
rootStore.session.data = {
|
||||
did: '123',
|
||||
handle: 'handle',
|
||||
service: 'test service',
|
||||
accessJwt: 'test token',
|
||||
refreshJwt: 'test token',
|
||||
}
|
||||
await meModel.load()
|
||||
expect(meModel.did).toEqual('123')
|
||||
expect(meModel.handle).toEqual('handle')
|
||||
expect(meModel.displayName).toEqual('')
|
||||
expect(meModel.description).toEqual('')
|
||||
expect(meModel.avatar).toEqual('')
|
||||
})
|
||||
|
||||
it('should load() to nothing when no session', async () => {
|
||||
rootStore.session.data = null
|
||||
await meModel.load()
|
||||
expect(meModel.did).toEqual('')
|
||||
expect(meModel.handle).toEqual('')
|
||||
expect(meModel.displayName).toEqual('')
|
||||
expect(meModel.description).toEqual('')
|
||||
expect(meModel.avatar).toEqual('')
|
||||
expect(meModel.notificationCount).toEqual(0)
|
||||
})
|
||||
|
||||
it('should serialize() key information', () => {
|
||||
meModel.did = '123'
|
||||
meModel.handle = 'handle'
|
||||
meModel.displayName = 'John Doe'
|
||||
meModel.description = 'description'
|
||||
meModel.avatar = 'avatar'
|
||||
|
||||
expect(meModel.serialize()).toEqual({
|
||||
did: '123',
|
||||
handle: 'handle',
|
||||
displayName: 'John Doe',
|
||||
description: 'description',
|
||||
avatar: 'avatar',
|
||||
})
|
||||
})
|
||||
|
||||
it('should clearNotificationCount() successfully', () => {
|
||||
meModel.clearNotificationCount()
|
||||
expect(meModel.notificationCount).toBe(0)
|
||||
})
|
||||
|
||||
it('should update notifs count with fetchStateUpdate()', async () => {
|
||||
meModel.notifications = {
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
} as unknown as NotificationsViewModel
|
||||
|
||||
jest
|
||||
.spyOn(rootStore.api.app.bsky.notification, 'getCount')
|
||||
.mockImplementationOnce((): Promise<any> => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
count: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
await meModel.fetchNotifications()
|
||||
expect(meModel.notificationCount).toBe(1)
|
||||
expect(meModel.notifications.refresh).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,151 @@
|
||||
import {NavigationModel} from './../../../src/state/models/navigation'
|
||||
import * as flags from '../../../src/build-flags'
|
||||
|
||||
describe('NavigationModel', () => {
|
||||
let model: NavigationModel
|
||||
|
||||
beforeEach(() => {
|
||||
model = new NavigationModel()
|
||||
model.setTitle('0-0', 'title')
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should clear() to the correct base state', async () => {
|
||||
await model.clear()
|
||||
expect(model.tabCount).toBe(2)
|
||||
expect(model.tab).toEqual({
|
||||
fixedTabPurpose: 0,
|
||||
history: [
|
||||
{
|
||||
id: expect.anything(),
|
||||
ts: expect.anything(),
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
id: expect.anything(),
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the navigate method', async () => {
|
||||
const navigateSpy = jest.spyOn(model.tab, 'navigate')
|
||||
await model.navigate('testurl', 'teststring')
|
||||
expect(navigateSpy).toHaveBeenCalledWith('testurl', 'teststring')
|
||||
})
|
||||
|
||||
it('should call the refresh method', async () => {
|
||||
const refreshSpy = jest.spyOn(model.tab, 'refresh')
|
||||
await model.refresh()
|
||||
expect(refreshSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call the isCurrentScreen method', () => {
|
||||
expect(model.isCurrentScreen('11', 0)).toEqual(false)
|
||||
})
|
||||
|
||||
it('should call the tab getter', () => {
|
||||
expect(model.tab).toEqual({
|
||||
fixedTabPurpose: 0,
|
||||
history: [
|
||||
{
|
||||
id: expect.anything(),
|
||||
ts: expect.anything(),
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
id: expect.anything(),
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the tabCount getter', () => {
|
||||
expect(model.tabCount).toBe(2)
|
||||
})
|
||||
|
||||
describe('tabs not enabled', () => {
|
||||
jest.mock('../../../src/build-flags', () => ({
|
||||
TABS_ENABLED: false,
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should not create new tabs', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = false
|
||||
model.newTab('testurl')
|
||||
expect(model.tab.isNewTab).toBe(false)
|
||||
expect(model.tabIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('should not change the active tab', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = false
|
||||
model.setActiveTab(2)
|
||||
expect(model.tabIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('should note close tabs', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = false
|
||||
model.closeTab(0)
|
||||
expect(model.tabCount).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('tabs enabled', () => {
|
||||
jest.mock('../../../src/build-flags', () => ({
|
||||
TABS_ENABLED: true,
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should create new tabs', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = true
|
||||
|
||||
model.newTab('testurl', 'title')
|
||||
expect(model.tab.isNewTab).toBe(true)
|
||||
expect(model.tabIndex).toBe(2)
|
||||
})
|
||||
|
||||
it('should change the current tab', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = true
|
||||
|
||||
model.setActiveTab(0)
|
||||
expect(model.tabIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('should close tabs', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = true
|
||||
|
||||
model.closeTab(0)
|
||||
expect(model.tabs).toEqual([
|
||||
{
|
||||
fixedTabPurpose: 1,
|
||||
history: [
|
||||
{
|
||||
id: expect.anything(),
|
||||
ts: expect.anything(),
|
||||
url: '/notifications',
|
||||
},
|
||||
],
|
||||
id: expect.anything(),
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
},
|
||||
])
|
||||
expect(model.tabIndex).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
OnboardModel,
|
||||
OnboardStageOrder,
|
||||
} from '../../../src/state/models/onboard'
|
||||
|
||||
describe('OnboardModel', () => {
|
||||
let onboardModel: OnboardModel
|
||||
|
||||
beforeEach(() => {
|
||||
onboardModel = new OnboardModel()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should start/stop correctly', () => {
|
||||
onboardModel.start()
|
||||
expect(onboardModel.isOnboarding).toBe(true)
|
||||
onboardModel.stop()
|
||||
expect(onboardModel.isOnboarding).toBe(false)
|
||||
})
|
||||
|
||||
it('should call the next method until it has no more stages', () => {
|
||||
onboardModel.start()
|
||||
onboardModel.next()
|
||||
expect(onboardModel.stage).toBe(OnboardStageOrder[1])
|
||||
|
||||
onboardModel.next()
|
||||
expect(onboardModel.isOnboarding).toBe(false)
|
||||
expect(onboardModel.stage).toBe(OnboardStageOrder[0])
|
||||
})
|
||||
|
||||
it('serialize and hydrate', () => {
|
||||
const serialized = onboardModel.serialize()
|
||||
const newModel = new OnboardModel()
|
||||
newModel.hydrate(serialized)
|
||||
expect(newModel).toEqual(onboardModel)
|
||||
|
||||
onboardModel.start()
|
||||
onboardModel.next()
|
||||
const serialized2 = onboardModel.serialize()
|
||||
newModel.hydrate(serialized2)
|
||||
expect(newModel).toEqual(onboardModel)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import {RootStoreModel} from '../../../src/state/models/root-store'
|
||||
import {setupState} from '../../../src/state'
|
||||
|
||||
describe('rootStore', () => {
|
||||
let rootStore: RootStoreModel
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
rootStore = await setupState()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call the clearAll() resets state correctly', () => {
|
||||
rootStore.clearAll()
|
||||
|
||||
expect(rootStore.session.data).toEqual(null)
|
||||
expect(rootStore.nav.tabs).toEqual([
|
||||
{
|
||||
fixedTabPurpose: 0,
|
||||
history: [
|
||||
{
|
||||
id: expect.anything(),
|
||||
ts: expect.anything(),
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
id: expect.anything(),
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
},
|
||||
{
|
||||
fixedTabPurpose: 1,
|
||||
history: [
|
||||
{
|
||||
id: expect.anything(),
|
||||
ts: expect.anything(),
|
||||
url: '/notifications',
|
||||
},
|
||||
],
|
||||
id: expect.anything(),
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
},
|
||||
])
|
||||
expect(rootStore.nav.tabIndex).toEqual(0)
|
||||
expect(rootStore.me.did).toEqual('')
|
||||
expect(rootStore.me.handle).toEqual('')
|
||||
expect(rootStore.me.displayName).toEqual('')
|
||||
expect(rootStore.me.description).toEqual('')
|
||||
expect(rootStore.me.avatar).toEqual('')
|
||||
expect(rootStore.me.notificationCount).toEqual(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
ConfirmModal,
|
||||
ImagesLightbox,
|
||||
ShellUiModel,
|
||||
} from './../../../src/state/models/shell-ui'
|
||||
|
||||
describe('ShellUiModel', () => {
|
||||
let model: ShellUiModel
|
||||
|
||||
beforeEach(() => {
|
||||
model = new ShellUiModel()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call the openModal & closeModal method', () => {
|
||||
const m = new ConfirmModal('Test Modal', 'Look good?', () => {})
|
||||
model.openModal(m)
|
||||
expect(model.isModalActive).toEqual(true)
|
||||
expect(model.activeModal).toEqual(m)
|
||||
|
||||
model.closeModal()
|
||||
expect(model.isModalActive).toEqual(false)
|
||||
expect(model.activeModal).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should call the openLightbox & closeLightbox method', () => {
|
||||
const lt = new ImagesLightbox(['uri'], 0)
|
||||
model.openLightbox(lt)
|
||||
expect(model.isLightboxActive).toEqual(true)
|
||||
expect(model.activeLightbox).toEqual(lt)
|
||||
|
||||
model.closeLightbox()
|
||||
expect(model.isLightboxActive).toEqual(false)
|
||||
expect(model.activeLightbox).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should call the openComposer & closeComposer method', () => {
|
||||
const composer = {
|
||||
replyTo: {
|
||||
uri: 'uri',
|
||||
cid: 'cid',
|
||||
text: 'text',
|
||||
author: {
|
||||
handle: 'handle',
|
||||
displayName: 'name',
|
||||
},
|
||||
},
|
||||
onPost: jest.fn(),
|
||||
}
|
||||
model.openComposer(composer)
|
||||
expect(model.isComposerActive).toEqual(true)
|
||||
expect(model.composerOpts).toEqual(composer)
|
||||
|
||||
model.closeComposer()
|
||||
expect(model.isComposerActive).toEqual(false)
|
||||
expect(model.composerOpts).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import {Autocomplete} from '../../../../src/view/com/composer/Autocomplete'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
|
||||
describe('Autocomplete', () => {
|
||||
const onSelectMock = jest.fn()
|
||||
const mockedProps = {
|
||||
active: true,
|
||||
items: [
|
||||
{
|
||||
handle: 'handle.test',
|
||||
displayName: 'Test Display',
|
||||
},
|
||||
{
|
||||
handle: 'handle2.test',
|
||||
displayName: 'Test Display 2',
|
||||
},
|
||||
],
|
||||
onSelect: onSelectMock,
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders a button for each user', async () => {
|
||||
const {findAllByTestId} = render(<Autocomplete {...mockedProps} />)
|
||||
const autocompleteButton = await findAllByTestId('autocompleteButton')
|
||||
expect(autocompleteButton.length).toBe(2)
|
||||
})
|
||||
|
||||
it('triggers onSelect by pressing the button', async () => {
|
||||
const {findAllByTestId} = render(<Autocomplete {...mockedProps} />)
|
||||
const autocompleteButton = await findAllByTestId('autocompleteButton')
|
||||
|
||||
fireEvent.press(autocompleteButton[0])
|
||||
expect(onSelectMock).toHaveBeenCalledWith('handle.test')
|
||||
|
||||
fireEvent.press(autocompleteButton[1])
|
||||
expect(onSelectMock).toHaveBeenCalledWith('handle2.test')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import React from 'react'
|
||||
import {ComposePost} from '../../../../src/view/com/composer/ComposePost'
|
||||
import {cleanup, fireEvent, render, waitFor} from '../../../../jest/test-utils'
|
||||
import * as apilib from '../../../../src/state/lib/api'
|
||||
import {
|
||||
mockedAutocompleteViewStore,
|
||||
mockedRootStore,
|
||||
} from '../../../../__mocks__/state-mock'
|
||||
import Toast from 'react-native-root-toast'
|
||||
|
||||
describe('ComposePost', () => {
|
||||
const mockedProps = {
|
||||
replyTo: {
|
||||
uri: 'testUri',
|
||||
cid: 'testCid',
|
||||
text: 'testText',
|
||||
author: {
|
||||
handle: 'test.handle',
|
||||
displayName: 'test name',
|
||||
avatar: '',
|
||||
},
|
||||
},
|
||||
onPost: jest.fn(),
|
||||
onClose: jest.fn(),
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders post composer', async () => {
|
||||
const {findByTestId} = render(<ComposePost {...mockedProps} />)
|
||||
const composePostView = await findByTestId('composePostView')
|
||||
expect(composePostView).toBeTruthy()
|
||||
})
|
||||
|
||||
it('closes composer', async () => {
|
||||
const {findByTestId} = render(<ComposePost {...mockedProps} />)
|
||||
const composerCancelButton = await findByTestId('composerCancelButton')
|
||||
fireEvent.press(composerCancelButton)
|
||||
expect(mockedProps.onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('changes text and publishes post', async () => {
|
||||
const postSpy = jest.spyOn(apilib, 'post').mockResolvedValue({
|
||||
uri: '',
|
||||
cid: '',
|
||||
})
|
||||
const toastSpy = jest.spyOn(Toast, 'show')
|
||||
|
||||
const wrapper = render(<ComposePost {...mockedProps} />)
|
||||
|
||||
const composerTextInput = await wrapper.findByTestId('composerTextInput')
|
||||
fireEvent.changeText(composerTextInput, 'testing publish')
|
||||
|
||||
const composerPublishButton = await wrapper.findByTestId(
|
||||
'composerPublishButton',
|
||||
)
|
||||
fireEvent.press(composerPublishButton)
|
||||
|
||||
expect(postSpy).toHaveBeenCalledWith(
|
||||
mockedRootStore,
|
||||
'testing publish',
|
||||
'testUri',
|
||||
undefined,
|
||||
[],
|
||||
new Set<string>(),
|
||||
expect.anything(),
|
||||
)
|
||||
|
||||
// Waits for request to be resolved
|
||||
await waitFor(() => {
|
||||
expect(mockedProps.onPost).toHaveBeenCalled()
|
||||
expect(mockedProps.onClose).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Your reply has been published', {
|
||||
animation: true,
|
||||
duration: 3500,
|
||||
hideOnPress: true,
|
||||
position: 50,
|
||||
shadow: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('selects autocomplete item', async () => {
|
||||
jest
|
||||
.spyOn(React, 'useMemo')
|
||||
.mockReturnValueOnce(mockedAutocompleteViewStore)
|
||||
|
||||
const {findAllByTestId} = render(<ComposePost {...mockedProps} />)
|
||||
const autocompleteButton = await findAllByTestId('autocompleteButton')
|
||||
|
||||
fireEvent.press(autocompleteButton[0])
|
||||
expect(mockedAutocompleteViewStore.setActive).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('selects photos', async () => {
|
||||
const {findByTestId, queryByTestId} = render(
|
||||
<ComposePost {...mockedProps} />,
|
||||
)
|
||||
let photoCarouselPickerView = queryByTestId('photoCarouselPickerView')
|
||||
expect(photoCarouselPickerView).toBeFalsy()
|
||||
|
||||
const composerSelectPhotosButton = await findByTestId(
|
||||
'composerSelectPhotosButton',
|
||||
)
|
||||
fireEvent.press(composerSelectPhotosButton)
|
||||
|
||||
photoCarouselPickerView = await findByTestId('photoCarouselPickerView')
|
||||
expect(photoCarouselPickerView).toBeTruthy()
|
||||
|
||||
fireEvent.press(composerSelectPhotosButton)
|
||||
|
||||
photoCarouselPickerView = queryByTestId('photoCarouselPickerView')
|
||||
expect(photoCarouselPickerView).toBeFalsy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
import {SelectedPhoto} from '../../../../src/view/com/composer/SelectedPhoto'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
|
||||
describe('SelectedPhoto', () => {
|
||||
const mockedProps = {
|
||||
selectedPhotos: ['mock-uri', 'mock-uri-2'],
|
||||
onSelectPhotos: jest.fn(),
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('has no photos to render', () => {
|
||||
const {queryByTestId} = render(
|
||||
<SelectedPhoto selectedPhotos={[]} onSelectPhotos={jest.fn()} />,
|
||||
)
|
||||
const selectedPhotosView = queryByTestId('selectedPhotosView')
|
||||
expect(selectedPhotosView).toBeNull()
|
||||
|
||||
const selectedPhotoImage = queryByTestId('selectedPhotoImage')
|
||||
expect(selectedPhotoImage).toBeNull()
|
||||
})
|
||||
|
||||
it('has 1 photos to render', async () => {
|
||||
const {findByTestId} = render(
|
||||
<SelectedPhoto
|
||||
selectedPhotos={['mock-uri']}
|
||||
onSelectPhotos={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
const selectedPhotosView = await findByTestId('selectedPhotosView')
|
||||
expect(selectedPhotosView).toBeTruthy()
|
||||
|
||||
const selectedPhotoImage = await findByTestId('selectedPhotoImage')
|
||||
expect(selectedPhotoImage).toBeTruthy()
|
||||
// @ts-expect-error
|
||||
expect(selectedPhotoImage).toHaveStyle({width: 250})
|
||||
})
|
||||
|
||||
it('has 2 photos to render', async () => {
|
||||
const {findAllByTestId} = render(<SelectedPhoto {...mockedProps} />)
|
||||
const selectedPhotoImage = await findAllByTestId('selectedPhotoImage')
|
||||
expect(selectedPhotoImage[0]).toBeTruthy()
|
||||
// @ts-expect-error
|
||||
expect(selectedPhotoImage[0]).toHaveStyle({width: 175})
|
||||
})
|
||||
|
||||
it('has 3 photos to render', async () => {
|
||||
const {findAllByTestId} = render(
|
||||
<SelectedPhoto
|
||||
selectedPhotos={['mock-uri', 'mock-uri-2', 'mock-uri-3']}
|
||||
onSelectPhotos={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
const selectedPhotoImage = await findAllByTestId('selectedPhotoImage')
|
||||
expect(selectedPhotoImage[0]).toBeTruthy()
|
||||
// @ts-expect-error
|
||||
expect(selectedPhotoImage[0]).toHaveStyle({width: 85})
|
||||
})
|
||||
|
||||
it('removes a photo', async () => {
|
||||
const {findAllByTestId} = render(<SelectedPhoto {...mockedProps} />)
|
||||
const removePhotoButton = await findAllByTestId('removePhotoButton')
|
||||
fireEvent.press(removePhotoButton[0])
|
||||
expect(mockedProps.onSelectPhotos).toHaveBeenCalledWith(['mock-uri-2'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import {Keyboard} from 'react-native'
|
||||
import {CreateAccount} from '../../../../src/view/com/login/CreateAccount'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {
|
||||
mockedSessionStore,
|
||||
mockedShellStore,
|
||||
} from '../../../../__mocks__/state-mock'
|
||||
|
||||
describe('CreateAccount', () => {
|
||||
const mockedProps = {
|
||||
onPressBack: jest.fn(),
|
||||
}
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders form and creates new account', async () => {
|
||||
const {findByTestId} = render(<CreateAccount {...mockedProps} />)
|
||||
|
||||
const registerEmailInput = await findByTestId('registerEmailInput')
|
||||
expect(registerEmailInput).toBeTruthy()
|
||||
fireEvent.changeText(registerEmailInput, 'test@email.com')
|
||||
|
||||
const registerHandleInput = await findByTestId('registerHandleInput')
|
||||
expect(registerHandleInput).toBeTruthy()
|
||||
fireEvent.changeText(registerHandleInput, 'test.handle')
|
||||
|
||||
const registerPasswordInput = await findByTestId('registerPasswordInput')
|
||||
expect(registerPasswordInput).toBeTruthy()
|
||||
fireEvent.changeText(registerPasswordInput, 'testpass')
|
||||
|
||||
const registerIs13Input = await findByTestId('registerIs13Input')
|
||||
expect(registerIs13Input).toBeTruthy()
|
||||
fireEvent.press(registerIs13Input)
|
||||
|
||||
const createAccountButton = await findByTestId('createAccountButton')
|
||||
expect(createAccountButton).toBeTruthy()
|
||||
fireEvent.press(createAccountButton)
|
||||
|
||||
expect(mockedSessionStore.createAccount).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders and selects service', async () => {
|
||||
const keyboardSpy = jest.spyOn(Keyboard, 'dismiss')
|
||||
const {findByTestId} = render(<CreateAccount {...mockedProps} />)
|
||||
|
||||
const registerSelectServiceButton = await findByTestId(
|
||||
'registerSelectServiceButton',
|
||||
)
|
||||
expect(registerSelectServiceButton).toBeTruthy()
|
||||
fireEvent.press(registerSelectServiceButton)
|
||||
|
||||
expect(mockedShellStore.openModal).toHaveBeenCalled()
|
||||
expect(keyboardSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
import React from 'react'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {ProfileViewModel} from '../../../../src/state/models/profile-view'
|
||||
import {ProfileHeader} from '../../../../src/view/com/profile/ProfileHeader'
|
||||
import {
|
||||
mockedNavigationStore,
|
||||
mockedProfileStore,
|
||||
mockedShellStore,
|
||||
} from '../../../../__mocks__/state-mock'
|
||||
|
||||
describe('ProfileHeader', () => {
|
||||
const mockedProps = {
|
||||
view: mockedProfileStore,
|
||||
onRefreshAll: jest.fn(),
|
||||
}
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders ErrorMessage on error', async () => {
|
||||
const {findByTestId} = render(
|
||||
<ProfileHeader
|
||||
{...{
|
||||
view: {
|
||||
...mockedProfileStore,
|
||||
hasError: true,
|
||||
} as ProfileViewModel,
|
||||
onRefreshAll: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
const profileHeaderHasError = await findByTestId('profileHeaderHasError')
|
||||
expect(profileHeaderHasError).toBeTruthy()
|
||||
})
|
||||
|
||||
it('presses and opens edit profile', async () => {
|
||||
const {findByTestId} = render(<ProfileHeader {...mockedProps} />)
|
||||
|
||||
const profileHeaderEditProfileButton = await findByTestId(
|
||||
'profileHeaderEditProfileButton',
|
||||
)
|
||||
expect(profileHeaderEditProfileButton).toBeTruthy()
|
||||
fireEvent.press(profileHeaderEditProfileButton)
|
||||
|
||||
expect(mockedShellStore.openModal).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('presses and opens followers page', async () => {
|
||||
const {findByTestId} = render(<ProfileHeader {...mockedProps} />)
|
||||
|
||||
const profileHeaderFollowersButton = await findByTestId(
|
||||
'profileHeaderFollowersButton',
|
||||
)
|
||||
expect(profileHeaderFollowersButton).toBeTruthy()
|
||||
fireEvent.press(profileHeaderFollowersButton)
|
||||
|
||||
expect(mockedNavigationStore.navigate).toHaveBeenCalledWith(
|
||||
'/profile/testhandle/followers',
|
||||
)
|
||||
})
|
||||
|
||||
// TODO - this will only pass if the profile has an avatar image set
|
||||
// it('presses and opens avatar modal', async () => {
|
||||
// const {findByTestId} = render(<ProfileHeader {...mockedProps} />)
|
||||
|
||||
// const profileHeaderAviButton = await findByTestId('profileHeaderAviButton')
|
||||
// expect(profileHeaderAviButton).toBeTruthy()
|
||||
// fireEvent.press(profileHeaderAviButton)
|
||||
|
||||
// expect(mockedShellStore.openLightbox).toHaveBeenCalled()
|
||||
// })
|
||||
|
||||
it('presses and opens follows page', async () => {
|
||||
const {findByTestId} = render(<ProfileHeader {...mockedProps} />)
|
||||
|
||||
const profileHeaderFollowsButton = await findByTestId(
|
||||
'profileHeaderFollowsButton',
|
||||
)
|
||||
expect(profileHeaderFollowsButton).toBeTruthy()
|
||||
fireEvent.press(profileHeaderFollowsButton)
|
||||
|
||||
expect(mockedNavigationStore.navigate).toHaveBeenCalledWith(
|
||||
'/profile/testhandle/follows',
|
||||
)
|
||||
})
|
||||
|
||||
it('toggles following', async () => {
|
||||
const {findByTestId} = render(
|
||||
<ProfileHeader
|
||||
{...{
|
||||
view: {
|
||||
...mockedProfileStore,
|
||||
did: 'test did 2',
|
||||
} as ProfileViewModel,
|
||||
onRefreshAll: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
const profileHeaderToggleFollowButton = await findByTestId(
|
||||
'profileHeaderToggleFollowButton',
|
||||
)
|
||||
expect(profileHeaderToggleFollowButton).toBeTruthy()
|
||||
fireEvent.press(profileHeaderToggleFollowButton)
|
||||
|
||||
expect(mockedProps.view.toggleFollowing).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import {renderHook} from '../../../jest/test-utils'
|
||||
import {useAnimatedValue} from '../../../src/view/lib/hooks/useAnimatedValue'
|
||||
|
||||
describe('useAnimatedValue', () => {
|
||||
it('creates an Animated.Value with the initial value passed to the hook', () => {
|
||||
const {result} = renderHook(() => useAnimatedValue(10))
|
||||
// @ts-expect-error
|
||||
expect(result.current.__getValue()).toEqual(10)
|
||||
})
|
||||
|
||||
it('returns the same Animated.Value instance on subsequent renders', () => {
|
||||
const {result, rerender} = renderHook(() => useAnimatedValue(10))
|
||||
const firstValue = result.current
|
||||
rerender({})
|
||||
expect(result.current).toBe(firstValue)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import {fireEvent, render} from '../../../jest/test-utils'
|
||||
import {Home} from '../../../src/view/screens/Home'
|
||||
import {mockedRootStore, mockedShellStore} from '../../../__mocks__/state-mock'
|
||||
|
||||
describe('useOnMainScroll', () => {
|
||||
const mockedProps = {
|
||||
navIdx: [0, 0] as [number, number],
|
||||
params: {},
|
||||
visible: true,
|
||||
}
|
||||
|
||||
it('toggles minimalShellMode to true', () => {
|
||||
jest.useFakeTimers()
|
||||
const {getByTestId} = render(<Home {...mockedProps} />)
|
||||
|
||||
fireEvent.scroll(getByTestId('homeFeed'), {
|
||||
nativeEvent: {
|
||||
contentOffset: {y: 20},
|
||||
contentSize: {height: 100},
|
||||
layoutMeasurement: {height: 50},
|
||||
},
|
||||
})
|
||||
|
||||
expect(mockedRootStore.shell.setMinimalShellMode).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('toggles minimalShellMode to false', () => {
|
||||
jest.useFakeTimers()
|
||||
const {getByTestId} = render(<Home {...mockedProps} />, {
|
||||
...mockedRootStore,
|
||||
shell: {
|
||||
...mockedShellStore,
|
||||
minimalShellMode: true,
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.scroll(getByTestId('homeFeed'), {
|
||||
nativeEvent: {
|
||||
contentOffset: {y: 0},
|
||||
contentSize: {height: 100},
|
||||
layoutMeasurement: {height: 50},
|
||||
},
|
||||
})
|
||||
expect(mockedRootStore.shell.setMinimalShellMode).toHaveBeenCalledWith(
|
||||
false,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import {Login} from '../../../src/view/screens/Login'
|
||||
import {cleanup, fireEvent, render} from '../../../jest/test-utils'
|
||||
|
||||
describe('Login', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders initial screen', () => {
|
||||
const {getByTestId} = render(<Login />)
|
||||
const signUpScreen = getByTestId('signinOrCreateAccount')
|
||||
|
||||
expect(signUpScreen).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders Signin screen', () => {
|
||||
const {getByTestId} = render(<Login />)
|
||||
const signInButton = getByTestId('signInButton')
|
||||
|
||||
fireEvent.press(signInButton)
|
||||
|
||||
const signInScreen = getByTestId('signIn')
|
||||
expect(signInScreen).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders CreateAccount screen', () => {
|
||||
const {getByTestId} = render(<Login />)
|
||||
const createAccountButton = getByTestId('createAccountButton')
|
||||
|
||||
fireEvent.press(createAccountButton)
|
||||
|
||||
const createAccountScreen = getByTestId('createAccount')
|
||||
expect(createAccountScreen).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import {NotFound} from '../../../src/view/screens/NotFound'
|
||||
import {cleanup, fireEvent, render} from '../../../jest/test-utils'
|
||||
import {mockedNavigationStore} from '../../../__mocks__/state-mock'
|
||||
|
||||
describe('NotFound', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('navigates home', async () => {
|
||||
const navigationSpy = jest.spyOn(mockedNavigationStore, 'navigate')
|
||||
const {getByTestId} = render(<NotFound />)
|
||||
const navigateHomeButton = getByTestId('navigateHomeButton')
|
||||
|
||||
fireEvent.press(navigateHomeButton)
|
||||
|
||||
expect(navigationSpy).toHaveBeenCalledWith('/')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import {Search} from '../../../src/view/screens/Search'
|
||||
import {cleanup, fireEvent, render} from '../../../jest/test-utils'
|
||||
|
||||
describe('Search', () => {
|
||||
jest.useFakeTimers()
|
||||
const mockedProps = {
|
||||
navIdx: [0, 0] as [number, number],
|
||||
params: {
|
||||
name: 'test name',
|
||||
},
|
||||
visible: true,
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders with query', async () => {
|
||||
const {findByTestId} = render(<Search {...mockedProps} />)
|
||||
const searchTextInput = await findByTestId('searchTextInput')
|
||||
|
||||
expect(searchTextInput).toBeTruthy()
|
||||
fireEvent.changeText(searchTextInput, 'test')
|
||||
|
||||
const searchScrollView = await findByTestId('searchScrollView')
|
||||
expect(searchScrollView).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
import {Menu} from '../../../../src/view/shell/mobile/Menu'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {mockedNavigationStore} from '../../../../__mocks__/state-mock'
|
||||
|
||||
describe('Menu', () => {
|
||||
const onCloseMock = jest.fn()
|
||||
|
||||
const mockedProps = {
|
||||
visible: true,
|
||||
onClose: onCloseMock,
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders menu', () => {
|
||||
const {getByTestId} = render(<Menu {...mockedProps} />)
|
||||
|
||||
const menuView = getByTestId('menuView')
|
||||
|
||||
expect(menuView).toBeTruthy()
|
||||
})
|
||||
|
||||
it('presses profile card button', () => {
|
||||
const {getByTestId} = render(<Menu {...mockedProps} />)
|
||||
|
||||
const profileCardButton = getByTestId('profileCardButton')
|
||||
fireEvent.press(profileCardButton)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(0, true)
|
||||
})
|
||||
|
||||
it('presses search button', () => {
|
||||
const {getByTestId} = render(<Menu {...mockedProps} />)
|
||||
|
||||
const searchBtn = getByTestId('searchBtn')
|
||||
fireEvent.press(searchBtn)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(0, true)
|
||||
expect(mockedNavigationStore.navigate).toHaveBeenCalledWith('/search')
|
||||
})
|
||||
|
||||
it("presses notifications menu item' button", () => {
|
||||
const {getByTestId} = render(<Menu {...mockedProps} />)
|
||||
|
||||
const menuItemButton = getByTestId('menuItemButton-Notifications')
|
||||
fireEvent.press(menuItemButton)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(1, true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import React from 'react'
|
||||
import {Animated} from 'react-native'
|
||||
import {TabsSelector} from '../../../../src/view/shell/mobile/TabsSelector'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {mockedNavigationStore} from '../../../../__mocks__/state-mock'
|
||||
|
||||
describe('TabsSelector', () => {
|
||||
const onCloseMock = jest.fn()
|
||||
|
||||
const mockedProps = {
|
||||
active: true,
|
||||
tabMenuInterp: new Animated.Value(0),
|
||||
onClose: onCloseMock,
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders tabs selector', () => {
|
||||
const {getByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const tabsSelectorView = getByTestId('tabsSelectorView')
|
||||
|
||||
expect(tabsSelectorView).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders nothing if inactive', () => {
|
||||
const {getByTestId} = render(
|
||||
<TabsSelector {...{...mockedProps, active: false}} />,
|
||||
)
|
||||
|
||||
const emptyView = getByTestId('emptyView')
|
||||
|
||||
expect(emptyView).toBeTruthy()
|
||||
})
|
||||
|
||||
// TODO - this throws currently, but the tabs selector isnt being used atm so I just disabled -prf
|
||||
// it('presses share button', () => {
|
||||
// const shareSpy = jest.spyOn(Share, 'share')
|
||||
// const {getByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
// const shareButton = getByTestId('shareButton')
|
||||
// fireEvent.press(shareButton)
|
||||
|
||||
// expect(onCloseMock).toHaveBeenCalled()
|
||||
// expect(shareSpy).toHaveBeenCalledWith({url: 'https://bsky.app/'})
|
||||
// })
|
||||
|
||||
it('presses clone button', () => {
|
||||
const {getByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const cloneButton = getByTestId('cloneButton')
|
||||
fireEvent.press(cloneButton)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.newTab).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('presses new tab button', () => {
|
||||
const {getByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const newTabButton = getByTestId('newTabButton')
|
||||
fireEvent.press(newTabButton)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.newTab).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
it('presses change tab button', () => {
|
||||
const {getAllByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const changeTabButton = getAllByTestId('changeTabButton')
|
||||
fireEvent.press(changeTabButton[0])
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.newTab).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
it('presses close tab button', () => {
|
||||
const {getAllByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const closeTabButton = getAllByTestId('closeTabButton')
|
||||
fireEvent.press(closeTabButton[0])
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.setActiveTab).toHaveBeenCalledWith(0)
|
||||
})
|
||||
|
||||
it('presses swipes to close the tab', () => {
|
||||
const {getByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const tabsSwipable = getByTestId('tabsSwipable')
|
||||
fireEvent(tabsSwipable, 'swipeableRightOpen')
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.setActiveTab).toHaveBeenCalledWith(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,171 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "com.facebook.react"
|
||||
|
||||
import com.android.build.OutputFile
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
||||
*/
|
||||
|
||||
react {
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
||||
// root = file("../")
|
||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
||||
// reactNativeDir = file("../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
|
||||
// codegenDir = file("../node_modules/react-native-codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
||||
// cliFile = file("../node_modules/react-native/cli.js")
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
||||
// debuggableVariants = ["liteDebug", "prodDebug"]
|
||||
/* Bundling */
|
||||
// A list containing the node command and its flags. Default is just 'node'.
|
||||
// nodeExecutableAndArgs = ["node"]
|
||||
//
|
||||
// The command to run when bundling. By default is 'bundle'
|
||||
// bundleCommand = "ram-bundle"
|
||||
//
|
||||
// The path to the CLI configuration file. Default is empty.
|
||||
// bundleConfig = file(../rn-cli.config.js)
|
||||
//
|
||||
// The name of the generated asset file containing your JS bundle
|
||||
// bundleAssetName = "MyApplication.android.bundle"
|
||||
//
|
||||
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
||||
// entryFile = file("../js/MyApplication.android.js")
|
||||
//
|
||||
// A list of extra flags to pass to the 'bundle' commands.
|
||||
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
||||
// extraPackagerArgs = []
|
||||
/* Hermes Commands */
|
||||
// The hermes compiler command to run. By default it is 'hermesc'
|
||||
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to true to create four separate APKs instead of one,
|
||||
* one for each native architecture. This is useful if you don't
|
||||
* use App Bundles (https://developer.android.com/guide/app-bundle/)
|
||||
* and want to have separate APKs to upload to the Play Store.
|
||||
*/
|
||||
def enableSeparateBuildPerCPUArchitecture = false
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore (JSC)
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc:+'
|
||||
|
||||
/**
|
||||
* Private function to get the list of Native Architectures you want to build.
|
||||
* This reads the value from reactNativeArchitectures in your gradle.properties
|
||||
* file and works together with the --active-arch-only flag of react-native run-android.
|
||||
*/
|
||||
def reactNativeArchitectures() {
|
||||
def value = project.getProperties().get("reactNativeArchitectures")
|
||||
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
||||
}
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace "xyz.blueskyweb.app"
|
||||
defaultConfig {
|
||||
applicationId "xyz.blueskyweb.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk false // If true, also generate a universal APK
|
||||
include (*reactNativeArchitectures())
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('debug.keystore')
|
||||
storePassword 'android'
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// https://developer.android.com/studio/build/configure-apk-splits.html
|
||||
// Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
|
||||
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
defaultConfig.versionCode * 1000 + versionCodes.get(abi)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
|
||||
|
||||
implementation project(':rn-fetch-blob')
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
}
|
||||
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
Binary file not shown.
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.app;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.android.utils.FlipperUtils;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.react.ReactInstanceEventListener;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the debug
|
||||
* flavor of it. Here you can add your own plugins and customize the Flipper setup.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (FlipperUtils.shouldEnableFlipper(context)) {
|
||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
||||
|
||||
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
|
||||
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
|
||||
NetworkingModule.setCustomClientBuilder(
|
||||
new NetworkingModule.CustomClientBuilder() {
|
||||
@Override
|
||||
public void apply(OkHttpClient.Builder builder) {
|
||||
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
|
||||
}
|
||||
});
|
||||
client.addPlugin(networkFlipperPlugin);
|
||||
client.start();
|
||||
|
||||
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
|
||||
// Hence we run if after all native modules have been initialized
|
||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext == null) {
|
||||
reactInstanceManager.addReactInstanceEventListener(
|
||||
new ReactInstanceEventListener() {
|
||||
@Override
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="xyz.blueskyweb.app">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<!-- deep linking via web links -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- debug host: https://bsky.pfrazee.com -->
|
||||
<data android:scheme="https" />
|
||||
<data android:host="bsky.pfrazee.com" />
|
||||
</intent-filter>
|
||||
<!-- deep linking via custom links -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<!-- debug scheme: bsky://app -->
|
||||
<data android:scheme="bsky" />
|
||||
<data android:host="app" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,36 @@
|
||||
package xyz.blueskyweb.app;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "xyz.blueskyweb.app";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
|
||||
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
|
||||
* (aka React 18) with two boolean flags.
|
||||
*/
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new DefaultReactActivityDelegate(
|
||||
this,
|
||||
getMainComponentName(),
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
|
||||
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
|
||||
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package xyz.blueskyweb.app;
|
||||
|
||||
import android.app.Application;
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import xyz.blueskyweb.app.newarchitecture.MainApplicationReactNativeHost;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new DefaultReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isNewArchEnabled() {
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
}
|
||||
@Override
|
||||
protected Boolean isHermesEnabled() {
|
||||
return BuildConfig.IS_HERMES_ENABLED;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
// If you opted-in for the New Architecture, we enable the TurboModule system
|
||||
ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
DefaultNewArchitectureEntryPoint.load();
|
||||
}
|
||||
ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
THIS_DIR := $(call my-dir)
|
||||
|
||||
include $(REACT_ANDROID_DIR)/Android-prebuilt.mk
|
||||
|
||||
# If you wish to add a custom TurboModule or Fabric component in your app you
|
||||
# will have to include the following autogenerated makefile.
|
||||
# include $(GENERATED_SRC_DIR)/codegen/jni/Android.mk
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_PATH := $(THIS_DIR)
|
||||
|
||||
# You can customize the name of your application .so file here.
|
||||
LOCAL_MODULE := app_appmodules
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)
|
||||
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
|
||||
|
||||
# If you wish to add a custom TurboModule or Fabric component in your app you
|
||||
# will have to uncomment those lines to include the generated source
|
||||
# files from the codegen (placed in $(GENERATED_SRC_DIR)/codegen/jni)
|
||||
#
|
||||
# LOCAL_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni
|
||||
# LOCAL_SRC_FILES += $(wildcard $(GENERATED_SRC_DIR)/codegen/jni/*.cpp)
|
||||
# LOCAL_EXPORT_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni
|
||||
|
||||
# Here you should add any native library you wish to depend on.
|
||||
LOCAL_SHARED_LIBRARIES := \
|
||||
libfabricjni \
|
||||
libfbjni \
|
||||
libfolly_futures \
|
||||
libfolly_json \
|
||||
libglog \
|
||||
libjsi \
|
||||
libreact_codegen_rncore \
|
||||
libreact_debug \
|
||||
libreact_nativemodule_core \
|
||||
libreact_render_componentregistry \
|
||||
libreact_render_core \
|
||||
libreact_render_debug \
|
||||
libreact_render_graphics \
|
||||
librrc_view \
|
||||
libruntimeexecutor \
|
||||
libturbomodulejsijni \
|
||||
libyoga
|
||||
|
||||
LOCAL_CFLAGS := -DLOG_TAG=\"ReactNative\" -fexceptions -frtti -std=c++17 -Wall
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2014 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
|
||||
android:insetTop="@dimen/abc_edit_text_inset_top_material"
|
||||
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
|
||||
|
||||
<selector>
|
||||
<!--
|
||||
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
|
||||
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
|
||||
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
|
||||
|
||||
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
|
||||
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
|
||||
-->
|
||||
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
|
||||
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
|
||||
</selector>
|
||||
|
||||
</inset>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user