Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04dd8f9eef | |||
| a52b840c19 | |||
| 64b8d94bb6 | |||
| bf4fca4f8a |
+86
@@ -0,0 +1,86 @@
|
||||
/** @type {Detox.DetoxConfig} */
|
||||
module.exports = {
|
||||
testRunner: {
|
||||
args: {
|
||||
$0: 'jest',
|
||||
config: '__e2e__/jest.config.js',
|
||||
},
|
||||
jest: {
|
||||
setupTimeout: 120000,
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
'ios.debug': {
|
||||
type: 'ios.app',
|
||||
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/bluesky.app',
|
||||
build:
|
||||
'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
|
||||
},
|
||||
'ios.release': {
|
||||
type: 'ios.app',
|
||||
binaryPath:
|
||||
'ios/build/Build/Products/Release-iphonesimulator/bluesky.app',
|
||||
build:
|
||||
'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
|
||||
},
|
||||
'android.debug': {
|
||||
type: 'android.apk',
|
||||
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
|
||||
build:
|
||||
'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -',
|
||||
reversePorts: [8081],
|
||||
},
|
||||
'android.release': {
|
||||
type: 'android.apk',
|
||||
binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
|
||||
build:
|
||||
'cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -',
|
||||
},
|
||||
},
|
||||
devices: {
|
||||
simulator: {
|
||||
type: 'ios.simulator',
|
||||
device: {
|
||||
type: 'iPhone 15 Pro',
|
||||
},
|
||||
},
|
||||
attached: {
|
||||
type: 'android.attached',
|
||||
device: {
|
||||
adbName: '.*',
|
||||
},
|
||||
},
|
||||
emulator: {
|
||||
type: 'android.emulator',
|
||||
device: {
|
||||
avdName: 'Pixel_3a_API_30_x86',
|
||||
},
|
||||
},
|
||||
},
|
||||
configurations: {
|
||||
'ios.sim.debug': {
|
||||
device: 'simulator',
|
||||
app: 'ios.debug',
|
||||
},
|
||||
'ios.sim.release': {
|
||||
device: 'simulator',
|
||||
app: 'ios.release',
|
||||
},
|
||||
'android.att.debug': {
|
||||
device: 'attached',
|
||||
app: 'android.debug',
|
||||
},
|
||||
'android.att.release': {
|
||||
device: 'attached',
|
||||
app: 'android.release',
|
||||
},
|
||||
'android.emu.debug': {
|
||||
device: 'emulator',
|
||||
app: 'android.debug',
|
||||
},
|
||||
'android.emu.release': {
|
||||
device: 'emulator',
|
||||
app: 'android.release',
|
||||
},
|
||||
},
|
||||
}
|
||||
+2
-1
@@ -95,7 +95,8 @@ web-build/
|
||||
/ios/
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Firebase (Android) Google services
|
||||
# INCLUDED: google-services.json
|
||||
# INCLUDED: google-services.json
|
||||
@@ -1,2 +0,0 @@
|
||||
[*.{kt,kts}]
|
||||
indent_size=2
|
||||
+4
-30
@@ -1,32 +1,6 @@
|
||||
# The env the app is running in e.g. development, testflight, production
|
||||
# Copy this to `.env` and `.env.test` files
|
||||
|
||||
SENTRY_AUTH_TOKEN=
|
||||
EXPO_PUBLIC_ENV=development
|
||||
|
||||
# This is the semver release version of the app, pulled from package.json
|
||||
EXPO_PUBLIC_RELEASE_VERSION=
|
||||
|
||||
# This is the commit hash that the current bundle was made from.
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
|
||||
|
||||
# Should be formatted YYMMDDHH so that it increases for each build.
|
||||
EXPO_PUBLIC_BUNDLE_DATE=0
|
||||
|
||||
# The log level for the app's logger transports
|
||||
EXPO_PUBLIC_LOG_LEVEL=debug
|
||||
|
||||
# Enable debug logs for specific logger instances
|
||||
EXPO_PUBLIC_LOG_DEBUG=session
|
||||
|
||||
# Chat service DID
|
||||
EXPO_PUBLIC_CHAT_PROXY_DID=
|
||||
|
||||
#
|
||||
#
|
||||
# Bluesky specific values
|
||||
#
|
||||
#
|
||||
|
||||
# Sentry DSN for telemetry
|
||||
EXPO_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Bitdrift API key. If undefined, Bitdrift will be disabled.
|
||||
EXPO_PUBLIC_BITDRIFT_API_KEY=
|
||||
EXPO_PUBLIC_LOG_DEBUG=
|
||||
|
||||
+3
-90
@@ -1,99 +1,16 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'@react-native',
|
||||
'@react-native-community',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-native-a11y/ios',
|
||||
'prettier',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'react',
|
||||
'lingui',
|
||||
'simple-import-sort',
|
||||
'bsky-internal',
|
||||
'eslint-plugin-react-compiler',
|
||||
'import',
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'detox', 'react', 'lingui'],
|
||||
rules: {
|
||||
'react/no-unescaped-entities': 0,
|
||||
'react/prop-types': 0,
|
||||
'react-native/no-inline-styles': 0,
|
||||
'bsky-internal/avoid-unwrapped-text': [
|
||||
'error',
|
||||
{
|
||||
impliedTextComponents: [
|
||||
'H1',
|
||||
'H2',
|
||||
'H3',
|
||||
'H4',
|
||||
'H5',
|
||||
'H6',
|
||||
'P',
|
||||
'Admonition',
|
||||
'Admonition.Admonition',
|
||||
'AgeAssuranceAdmonition',
|
||||
'Span',
|
||||
],
|
||||
impliedTextProps: [],
|
||||
suggestedTextWrappers: {
|
||||
Button: 'ButtonText',
|
||||
'ToggleButton.Button': 'ToggleButton.ButtonText',
|
||||
},
|
||||
},
|
||||
],
|
||||
'bsky-internal/use-exact-imports': 'error',
|
||||
'bsky-internal/use-typed-gates': 'error',
|
||||
'bsky-internal/use-prefixed-imports': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
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': 'error',
|
||||
'react-compiler/react-compiler': 'warn',
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{argsIgnorePattern: '^_', varsIgnorePattern: '^_'},
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'warn',
|
||||
{prefer: 'type-imports', fixStyle: 'inline-type-imports'},
|
||||
],
|
||||
'import/consistent-type-specifier-style': ['warn', 'prefer-inline'],
|
||||
},
|
||||
ignorePatterns: [
|
||||
'**/__mocks__/*.ts',
|
||||
@@ -105,17 +22,13 @@ module.exports = {
|
||||
'*.lock',
|
||||
'.husky',
|
||||
'patches',
|
||||
'bskyweb',
|
||||
'*.html',
|
||||
'bskyweb',
|
||||
'bskyembed',
|
||||
'src/locale/locales/_build/',
|
||||
'src/locale/locales/**/*.js',
|
||||
],
|
||||
settings: {
|
||||
componentWrapperFunctions: ['observer'],
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
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,64 +0,0 @@
|
||||
name: "Bug Report"
|
||||
description: "Create a report for an issue you have experience in the app."
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report an issue you've found in the app! Before you submit this issue, please make sure you've searched for similar issues that are already open and being tracked. If you find an open issue that seems relevant to yours, it is best to leave a response there with your information instead of opening a new issue, since it helps to consolidate the info in one place.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
Please provide the steps that may reproduce the issue that you are experiencing. The more concise of a
|
||||
reproduction that you provide, the easier it will be for us to identify the issue and fix it in a timely manner.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: |
|
||||
If possible, please provide any images or videos that may help us understand the issue you are experiencing.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What platform(s) does this occur on?
|
||||
multiple: true
|
||||
options:
|
||||
- iOS
|
||||
- Android
|
||||
- Web (Desktop)
|
||||
- Web (Mobile)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Device Info
|
||||
description: |
|
||||
Please provide the device information that you are using when experiencing this issue. This can include the
|
||||
device model, operating system version, and any other relevant information.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: What version of the app are you using?
|
||||
description: |
|
||||
You can easily find this information by visting the Settings screen in the app, and tapping on the version info
|
||||
at the bottom of the screen. All the relevant information will be copied to your clipboard, and you will be
|
||||
able to share it here.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: |
|
||||
Please provide any additional information that you think may be helpful in identifying the issue you are
|
||||
experiencing. This can include any error messages, logs, or other information that you think may be relevant.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,69 +0,0 @@
|
||||
name: "Bug Report - Fabric/New Architecture"
|
||||
description: "Create a bug report for builds using the New Architecture. If you are not sure what that means, don't use this template, and instead use the default bug report template."
|
||||
labels: ["bug", "fabric"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: You're running a build that uses the React Native New Architecture, right?
|
||||
description: This template is specifically for issues related to builds that use the React Native New Architecture. If you are not sure what that means, this template is probably not relevant to you, and you should use the default Bug Report template.
|
||||
options:
|
||||
- label: I am running a build that uses the New Architecture
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report an issue you've found in the app! Before you submit this issue, please make sure you've searched for similar issues that are already open and being tracked. If you find an open issue that seems relevant to yours, it is best to leave a response there with your information instead of opening a new issue, since it helps to consolidate the info in one place.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
Please provide the steps that may reproduce the issue that you are experiencing. The more concise of a
|
||||
reproduction that you provide, the easier it will be for us to identify the issue and fix it in a timely manner.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: |
|
||||
If possible, please provide any images or videos that may help us understand the issue you are experiencing.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What platform(s) does this occur on?
|
||||
multiple: true
|
||||
options:
|
||||
- iOS
|
||||
- Android
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Device Info
|
||||
description: |
|
||||
Please provide the device information that you are using when experiencing this issue. This can include the
|
||||
device model, operating system version, and any other relevant information.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: What version of the app are you using?
|
||||
description: |
|
||||
You can easily find this information by visting the Settings screen in the app, and tapping on the version info
|
||||
at the bottom of the screen. All the relevant information will be copied to your clipboard, and you will be
|
||||
able to share it here.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: |
|
||||
Please provide any additional information that you think may be helpful in identifying the issue you are
|
||||
experiencing. This can include any error messages, logs, or other information that you think may be relevant.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
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,42 +0,0 @@
|
||||
name: "Feature Request"
|
||||
description: "Suggest an idea for the Bluesky app."
|
||||
labels: ["feature-request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for submitting a feature request! Before you submit this request, please make sure you've searched for similar requests that are already open and being tracked. If you find an open request that seems relevant to yours, it is best to leave a response there with your information instead of opening a new request, since it helps to consolidate the info in one place.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the Feature
|
||||
description: |
|
||||
Please provide a detailed description of the feature you would like to see implemented in the app. The more
|
||||
information you provide, the easier it will be for us to understand the feature you are requesting and to
|
||||
implement it in a timely manner.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Attachments
|
||||
description: |
|
||||
If helpful, provide some images or videos of similar features in other apps that you think would be helpful
|
||||
for us to understand the feature you are requesting, or places within the app that you think the feature belongs
|
||||
in or is missing from.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe Alternatives
|
||||
description: |
|
||||
If there are any alternative solutions or features that you think would be helpful to implement in place of the
|
||||
feature you are requesting, please provide a description of those as well.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional Context
|
||||
description: |
|
||||
If there is any additional context or information that you think would be helpful for us to know about the
|
||||
feature you are requesting, please provide that information here.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,10 +1,10 @@
|
||||
name: build-and-push-bskyweb-aws
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
- traffic-reduction
|
||||
- respect-optout-for-embeds
|
||||
env:
|
||||
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
|
||||
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
|
||||
@@ -43,12 +43,6 @@ jobs:
|
||||
tags: |
|
||||
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
|
||||
|
||||
- name: Env
|
||||
id: env
|
||||
run: |
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
@@ -60,9 +54,3 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
EXPO_PUBLIC_ENV=production
|
||||
EXPO_PUBLIC_RELEASE_VERSION=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
|
||||
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
|
||||
EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
name: build-and-push-embedr-aws
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- echoprom_fix
|
||||
|
||||
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,192 +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:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
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 2>&1 | tee i18n.log
|
||||
|
||||
- name: Check for i18n compilation errors
|
||||
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
|
||||
|
||||
# EXPO_PUBLIC_ENV is handled in eas.json
|
||||
- name: Env
|
||||
id: env
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: >
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
|
||||
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
|
||||
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: >
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
|
||||
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
|
||||
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,132 +0,0 @@
|
||||
---
|
||||
name: Build and Submit iOS
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
profile:
|
||||
type: choice
|
||||
description: Build profile to use
|
||||
options:
|
||||
- testflight
|
||||
- production
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-15
|
||||
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: '16.4'
|
||||
|
||||
- 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 2>&1 | tee i18n.log
|
||||
|
||||
- name: Check for i18n compilation errors
|
||||
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
|
||||
|
||||
# EXPO_PUBLIC_ENV is handled in eas.json
|
||||
- name: ✏️ Write environment variables
|
||||
id: env
|
||||
run: |
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: >
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
|
||||
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
|
||||
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,367 +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:
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
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: Lint lockfile
|
||||
run: yarn lockfile-lint
|
||||
|
||||
- name: Prettier check
|
||||
run: yarn prettier --check .
|
||||
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build 2>&1 | tee i18n.log
|
||||
|
||||
- name: Check for i18n compilation errors
|
||||
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
|
||||
|
||||
- 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
|
||||
|
||||
# eas.json not used here, set EXPO_PUBLIC_ENV
|
||||
- name: Env
|
||||
id: env
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_ENV=${{ inputs.channel || 'testflight' }}" >> .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ Create Bundle
|
||||
if: ${{ !steps.fingerprint.outputs.includes-changes }}
|
||||
run: >
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
|
||||
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
|
||||
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-15
|
||||
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 && github.repository == 'bluesky-social/social-app' }}
|
||||
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: '16.2'
|
||||
|
||||
- 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
|
||||
|
||||
# EXPO_PUBLIC_ENV is handled in eas.json
|
||||
- name: Env
|
||||
id: env
|
||||
run: |
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: >
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
|
||||
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
|
||||
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 && github.repository == 'bluesky-social/social-app'}}
|
||||
|
||||
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
|
||||
|
||||
# EXPO_PUBLIC_ENV is handled in eas.json
|
||||
- name: Env
|
||||
id: env
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ EAS Build
|
||||
run: >
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
|
||||
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
|
||||
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
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Deploy Nightly Testflight Release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Deploy Nightly Testflight Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
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
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: yarn
|
||||
|
||||
- name: Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Bump build number
|
||||
run: yarn bump:ios
|
||||
|
||||
- name: EAS build and submit
|
||||
run: eas build -p ios --profile production --auto-submit --non-interactive
|
||||
|
||||
- name: Commit
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: Nightly iOS Build Bump
|
||||
branch: main
|
||||
commit_user_name: github-actions[bot]
|
||||
commit_user_email: github-actions[bot]@users.noreply.github.com
|
||||
@@ -19,9 +19,9 @@ jobs:
|
||||
- name: Set up Go tooling
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- name: Dummy Static Files
|
||||
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
|
||||
go-version: '1.21'
|
||||
- name: Dummy JS File
|
||||
run: touch bskyweb/static/js/blah.js
|
||||
- name: Check
|
||||
run: cd bskyweb/ && make check
|
||||
- name: Build (binary)
|
||||
@@ -36,8 +36,8 @@ jobs:
|
||||
- name: Set up Go tooling
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- name: Dummy Static Files
|
||||
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
|
||||
go-version: '1.21'
|
||||
- name: Dummy JS File
|
||||
run: touch bskyweb/static/js/blah.js
|
||||
- name: Lint
|
||||
run: cd bskyweb/ && make lint
|
||||
|
||||
@@ -16,10 +16,6 @@ jobs:
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Yarn install
|
||||
uses: Wandalen/wretry.action@master
|
||||
with:
|
||||
@@ -28,10 +24,6 @@ jobs:
|
||||
attempt_delay: 2000
|
||||
- name: Lint check
|
||||
run: yarn lint
|
||||
- name: Lint lockfile
|
||||
run: yarn lockfile-lint
|
||||
- name: Prettier check
|
||||
run: yarn prettier --check .
|
||||
- name: Check & compile i18n
|
||||
run: yarn intl:build
|
||||
- name: Type check
|
||||
@@ -40,12 +32,12 @@ jobs:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Install node
|
||||
- name: Install node 18
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
node-version: 18
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Yarn install
|
||||
uses: Wandalen/wretry.action@master
|
||||
with:
|
||||
@@ -56,4 +48,4 @@ jobs:
|
||||
run: yarn intl:build
|
||||
- name: Run tests
|
||||
run: |
|
||||
NODE_ENV=test yarn test --forceExit
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test --forceExit
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Nightly Update Source Languages
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # run at 2 AM UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
extract-languages:
|
||||
name: Nightly Update Source Languages
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
# Give the default GITHUB_TOKEN write permission to commit and push the
|
||||
# added or changed files to the repository.
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ssh-key: ${{secrets.GH_ACTION_DEPLOY_KEY}}
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Yarn install
|
||||
uses: Wandalen/wretry.action@master
|
||||
with:
|
||||
command: yarn --frozen-lockfile
|
||||
attempt_limit: 3
|
||||
attempt_delay: 2000
|
||||
- name: Extract language strings
|
||||
run: yarn intl:extract
|
||||
- name: Create commit
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: Nightly source-language update
|
||||
file_pattern: ./src/locale/locales/en/messages.po
|
||||
- name: Push source lang to Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_sources_args: "-b main"
|
||||
upload_translations: false
|
||||
push_translations: false
|
||||
push_sources: false
|
||||
create_pull_request: false
|
||||
env:
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
@@ -1,207 +0,0 @@
|
||||
---
|
||||
name: PR Comment Trigger
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
# Permissiosn to make comments in the pull request
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
handle-comment:
|
||||
if: github.event.issue.pull_request
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should-deploy: ${{ steps.check-org.outputs.result }}
|
||||
|
||||
steps:
|
||||
- name: Check if bot is mentioned
|
||||
id: check-mention
|
||||
env:
|
||||
COMMENT: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
if [[ "$COMMENT" == *"@github-actions"* ]] || \
|
||||
[[ "$COMMENT" == *"github-actions[bot]"* ]]; then
|
||||
bot_mentioned=true
|
||||
else
|
||||
bot_mentioned=false
|
||||
fi
|
||||
|
||||
|
||||
if [[ "${{ github.event.comment.body }}" == *"ota"* ]]; then
|
||||
has_ota=true
|
||||
else
|
||||
has_ota=false
|
||||
fi
|
||||
|
||||
|
||||
if [[ "$bot_mentioned" == "true" ]] && [[ "$has_ota" == "true" ]]; then
|
||||
echo "mentioned=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "mentioned=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check organization membership
|
||||
if: steps.check-mention.outputs.mentioned == 'true'
|
||||
id: check-org
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: context.payload.comment.user.login
|
||||
});
|
||||
|
||||
const hasAccess = ['admin', 'write'].includes(perm.permission);
|
||||
console.log(`User has ${perm.permission} access`);
|
||||
|
||||
return hasAccess;
|
||||
} catch(error) {
|
||||
console.log('User has no repository access');
|
||||
return false;
|
||||
}
|
||||
|
||||
bundle-deploy:
|
||||
name: Bundle and Deploy EAS Update
|
||||
runs-on: ubuntu-latest
|
||||
needs: [handle-comment]
|
||||
if: needs.handle-comment.outputs.should-deploy == 'true'
|
||||
|
||||
steps:
|
||||
- name: Get PR HEAD SHA
|
||||
id: pr-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: ${{ github.event.issue.number }}
|
||||
});
|
||||
|
||||
console.log(`PR HEAD SHA: ${pr.data.head.sha}`);
|
||||
console.log(`PR HEAD REF: ${pr.data.head.ref}`);
|
||||
|
||||
core.setOutput('head-sha', pr.data.head.sha);
|
||||
core.setOutput('head-ref', pr.data.head.ref);
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
number: ${{ github.event.issue.number }}
|
||||
message: |
|
||||
An OTA deployment has been requested and is now running for `${{ steps.pr-info.outputs.head-sha }}`.
|
||||
|
||||
[Here is some music to listen to while you wait...](https://www.youtube.com/watch?v=VBlFHuCzPgY)
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
|
||||
- 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:
|
||||
ref: ${{ steps.pr-info.outputs.head-sha }}
|
||||
|
||||
- name: 🔧 Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Lint check
|
||||
run: yarn lint
|
||||
|
||||
- name: Lint lockfile
|
||||
run: yarn lockfile-lint
|
||||
|
||||
- name: 🔤 Compile translations
|
||||
run: yarn intl:build 2>&1 | tee i18n.log
|
||||
|
||||
- name: Check for i18n compilation errors
|
||||
run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
|
||||
|
||||
- name: Type check
|
||||
run: yarn typecheck
|
||||
|
||||
- name: 🔨 Setup EAS
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
|
||||
- name: ⛏️ Setup Expo
|
||||
run: yarn global add eas-cli-local-build-plugin
|
||||
|
||||
- name: 🪛 Setup jq
|
||||
uses: dcarbone/install-jq-action@v2
|
||||
|
||||
- name: Env
|
||||
id: env
|
||||
run: |
|
||||
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||
echo "EXPO_PUBLIC_ENV=testflight" >> .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
|
||||
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
|
||||
echo "EXPO_PUBLIC_GCP_PROJECT_ID=${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }}" >> .env
|
||||
echo "$json" > google-services.json
|
||||
|
||||
- name: 🏗️ Create Bundle
|
||||
run: >
|
||||
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
|
||||
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
|
||||
yarn export
|
||||
|
||||
- name: 📦 Package Bundle and 🚀 Deploy
|
||||
run: yarn use-build-number bash scripts/bundleUpdate.sh
|
||||
env:
|
||||
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
|
||||
CHANNEL_NAME: pull-request-${{ github.event.issue.number }}
|
||||
RUNTIME_VERSION:
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
number: ${{ github.event.issue.number }}
|
||||
message: |
|
||||
Your requested OTA deployment was successful! You may now apply it by opening the deep link below in your browser:
|
||||
|
||||
`bluesky://intent/apply-ota?channel=pull-request-${{ github.event.issue.number }}`
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
|
||||
|
||||
- name: 💬 Drop a comment
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
if: failure()
|
||||
with:
|
||||
header: pull-request-eas-build-${{ steps.pr-info.outputs.head-sha }}
|
||||
number: ${{ github.event.issue.number }}
|
||||
message: |
|
||||
Your requested OTA deployment was unsuccessful. See action logs for more details.
|
||||
---
|
||||
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
|
||||
@@ -1,147 +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
|
||||
yarn intl:build
|
||||
|
||||
- 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 intl:build
|
||||
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
|
||||
@@ -1,31 +0,0 @@
|
||||
name: Sync to internal repo
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'bluesky-social/social-app'
|
||||
steps:
|
||||
- name: Checkout public repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Generate GitHub App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ vars.SYNC_INTERNAL_APP_ID }}
|
||||
private-key: ${{ secrets.SYNC_INTERNAL_PK }}
|
||||
repositories: social-app-internal
|
||||
- name: Push to internal repo
|
||||
env:
|
||||
TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
git config user.name "github-actions"
|
||||
git config user.email "test@users.noreply.github.com"
|
||||
git config --unset-all http.https://github.com/.extraheader
|
||||
git remote add internal https://x-access-token:${TOKEN}@github.com/bluesky-social/social-app-internal.git
|
||||
git push internal main --force
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Lockfile
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify-yarn-lock:
|
||||
name: No manual yarn.lock edits
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out PR HEAD
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Fetch base branch
|
||||
run: git fetch origin ${{ github.base_ref }} --depth=1
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Reset yarn.lock to base
|
||||
run: git show "origin/${{ github.base_ref }}:yarn.lock" > yarn.lock
|
||||
|
||||
- name: Yarn install
|
||||
uses: Wandalen/wretry.action@master
|
||||
with:
|
||||
# Fine to skip scripts since we don't run any code
|
||||
command: yarn install --ignore-scripts
|
||||
attempt_limit: 3
|
||||
attempt_delay: 2000
|
||||
|
||||
- name: Verify yarn.lock
|
||||
run: |
|
||||
git diff --quiet --exit-code || {
|
||||
echo '::error::`yarn.lock` does not match what Yarn would generate given the base `yarn.lock` and the head `package.json`.'
|
||||
echo '::error:: - If this is intentional, you can ignore this check.'
|
||||
echo '::error:: - If this is unintentional, apply the following diff:'
|
||||
git --no-pager diff
|
||||
exit 1
|
||||
}
|
||||
+1
-12
@@ -18,6 +18,7 @@ xcuserdata
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
|
||||
# Android/IntelliJ
|
||||
@@ -77,7 +78,6 @@ dist/
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
stats.json
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
@@ -104,17 +104,6 @@ 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-*
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged --concurrent false
|
||||
yarn lint-staged
|
||||
|
||||
+11
-19
@@ -1,20 +1,12 @@
|
||||
# Ignore everything except JS-ey or CSS code.
|
||||
# Based on https://stackoverflow.com/a/70715829/458193
|
||||
*
|
||||
!**/*.js
|
||||
!**/*.jsx
|
||||
!**/*.ts
|
||||
!**/*.tsx
|
||||
!*/
|
||||
|
||||
!**/*.css
|
||||
|
||||
# More specific ignores go below.
|
||||
.expo
|
||||
android
|
||||
ios
|
||||
src/locale/locales
|
||||
lib/react-compiler-runtime
|
||||
bskyweb/static
|
||||
coverage
|
||||
web-build
|
||||
android
|
||||
src/third-party
|
||||
src/app.json
|
||||
public
|
||||
/bskyweb/templates
|
||||
/dist/
|
||||
/.watchmanconfig
|
||||
/app.json
|
||||
|
||||
web/index.html
|
||||
web-build/*
|
||||
|
||||
+5
-44
@@ -1,50 +1,20 @@
|
||||
FROM golang:1.24.5-bullseye AS build-env
|
||||
FROM golang:1.21-bullseye AS build-env
|
||||
|
||||
WORKDIR /usr/src/social-app
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
#
|
||||
# Node
|
||||
#
|
||||
ENV NODE_VERSION=20
|
||||
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"
|
||||
|
||||
# The latest git hash of the preview branch on render.com
|
||||
# https://render.com/docs/docker-secrets#environment-variables-in-docker-builds
|
||||
ARG RENDER_GIT_COMMIT
|
||||
|
||||
#
|
||||
# Expo
|
||||
#
|
||||
ARG EXPO_PUBLIC_ENV
|
||||
ENV EXPO_PUBLIC_ENV=${EXPO_PUBLIC_ENV:-development}
|
||||
ARG EXPO_PUBLIC_RELEASE_VERSION
|
||||
ENV EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION
|
||||
ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
|
||||
# If not set by GitHub workflows, we're probably in Render
|
||||
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-$RENDER_GIT_COMMIT}
|
||||
|
||||
#
|
||||
# Sentry
|
||||
#
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown}
|
||||
ARG EXPO_PUBLIC_SENTRY_DSN
|
||||
ENV EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN
|
||||
|
||||
#
|
||||
# Copy everything into the container
|
||||
#
|
||||
COPY . .
|
||||
|
||||
#
|
||||
@@ -53,23 +23,16 @@ COPY . .
|
||||
RUN mkdir --parents $NVM_DIR && \
|
||||
wget \
|
||||
--output-document=/tmp/nvm-install.sh \
|
||||
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh && \
|
||||
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/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_ENV=$EXPO_PUBLIC_ENV" >> .env && \
|
||||
echo "EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION" >> .env && \
|
||||
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \
|
||||
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env && \
|
||||
echo "EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN" >> .env && \
|
||||
npm install --global yarn && \
|
||||
yarn && \
|
||||
yarn intl:build 2>&1 | tee i18n.log && \
|
||||
if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compile errors!\n\n"; fi && \
|
||||
SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$EXPO_PUBLIC_RELEASE_VERSION SENTRY_DIST=$EXPO_PUBLIC_BUNDLE_IDENTIFIER yarn build-web
|
||||
yarn intl:compile && \
|
||||
yarn build-web
|
||||
|
||||
# DEBUG
|
||||
RUN find ./bskyweb/static && find ./web-build/static
|
||||
@@ -109,5 +72,3 @@ 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,80 +0,0 @@
|
||||
FROM golang:1.24.5-bullseye AS build-env
|
||||
|
||||
WORKDIR /usr/src/social-app
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Node
|
||||
ENV NODE_VERSION=20
|
||||
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/css/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,4 +1,4 @@
|
||||
Copyright 2023–2025 Bluesky Social PBC
|
||||
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:
|
||||
|
||||
|
||||
@@ -13,14 +13,9 @@ 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 yarn test
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run style checks and verify syntax
|
||||
@@ -33,10 +28,9 @@ lint: ## Run style checks and verify syntax
|
||||
.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 20
|
||||
nvm use 20
|
||||
nvm install 18
|
||||
nvm use 18
|
||||
npm install --global yarn
|
||||
|
||||
@@ -10,7 +10,7 @@ Get the app itself:
|
||||
|
||||
## Development Resources
|
||||
|
||||
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), which are also open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
|
||||
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also on open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
|
||||
|
||||
There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application.
|
||||
|
||||
@@ -19,7 +19,7 @@ The [Build Instructions](./docs/build.md) are a good place to get started with t
|
||||
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:
|
||||
|
||||
- [Overview and Guides](https://atproto.com/guides/overview)
|
||||
- [GitHub Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions
|
||||
- [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)
|
||||
|
||||
@@ -27,7 +27,6 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th
|
||||
|
||||
## Contributions
|
||||
|
||||
> [!NOTE]
|
||||
> 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:**
|
||||
@@ -43,10 +42,10 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th
|
||||
- Open an issue and give some time for discussion before submitting a PR.
|
||||
- Stay away from PRs like...
|
||||
- Changing "Post" to "Skeet."
|
||||
- Refactoring the codebase, e.g., to replace React Query with Redux Toolkit or something.
|
||||
- Refactoring the codebase, eg to replace mobx with redux or something.
|
||||
- Adding entirely new features without prior discussion.
|
||||
|
||||
Remember, we serve a wide community of users. Our day-to-day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for!
|
||||
Remember, we serve a wide community of users. Our day to day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for!
|
||||
|
||||
## Forking guidelines
|
||||
|
||||
@@ -60,11 +59,13 @@ Please be sure to:
|
||||
|
||||
## Security disclosures
|
||||
|
||||
If you discover any security issues, please send an email to security@bsky.app. The email is automatically CC'd to the entire team and we'll respond promptly.
|
||||
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.
|
||||
Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party can be as seamless as first-party through custom feeds, federated services, clients, and more.
|
||||
|
||||
If you're a developer interested in building on atproto, we'd love to email you a Bluesky invite code. Simply share your GitHub (or similar) profile with us via [this form](https://forms.gle/BF21oxVNZiDjDhXF9).
|
||||
|
||||
## License (MIT)
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
flows:
|
||||
- "flows/**"
|
||||
@@ -1,29 +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
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- tapOn:
|
||||
id: "composeFAB"
|
||||
- inputText: "Post with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn: "Content warnings"
|
||||
- tapOn: "Porn"
|
||||
- tapOn:
|
||||
label: "Tap on confirm"
|
||||
id: "confirmBtn"
|
||||
- tapOn:
|
||||
id: "composerPublishBtn"
|
||||
- tapOn:
|
||||
id: "e2eRefreshHome"
|
||||
- assertVisible: "Adult Content"
|
||||
@@ -1,89 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- 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: "Bluesky Social"
|
||||
- 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: "hunter22"
|
||||
- hideKeyboard
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn:
|
||||
text: "Not Now"
|
||||
optional: true
|
||||
- inputText: "e2e-test"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
|
||||
@@ -1,176 +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: "People"
|
||||
- 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"
|
||||
|
||||
- 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: "People"
|
||||
- assertVisible: "Good Ppl"
|
||||
- assertVisible: "They good"
|
||||
- tapOn: "People"
|
||||
|
||||
- tapOn: "Start adding people"
|
||||
- tapOn: "Search"
|
||||
- inputText: "b"
|
||||
- pressKey: Enter
|
||||
- tapOn: "Add user to list"
|
||||
- swipe:
|
||||
direction: DOWN
|
||||
- assertVisible: "View Bob's profile"
|
||||
|
||||
- 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: "People"
|
||||
- assertVisible: "View Bob's profile"
|
||||
- tapOn:
|
||||
point: "90%,43%"
|
||||
- tapOn:
|
||||
id: "user-bob.test-addBtn"
|
||||
- tapOn:
|
||||
id: "doneBtn"
|
||||
|
||||
- tapOn:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn: "Search for posts, users, or feeds"
|
||||
- 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,88 +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
|
||||
- extendedWaitUntil:
|
||||
visible: "Open drawer menu"
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "profileCardButton"
|
||||
- tapOn:
|
||||
id: "profilePager-selector-5"
|
||||
- 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: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on down arrow"
|
||||
id: "feed-timeline-moveDown"
|
||||
- tapOn:
|
||||
label: "Save button"
|
||||
id: "saveChangesBtn"
|
||||
- tapOn: "Go back"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "alice-favs"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "Following"
|
||||
|
||||
# Set following first
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on down arrow"
|
||||
id: "feed-feed-moveDown"
|
||||
- tapOn:
|
||||
label: "Save button"
|
||||
id: "saveChangesBtn"
|
||||
- tapOn: "Go back"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "Following"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-1"
|
||||
text: "alice-favs"
|
||||
|
||||
# Remove following
|
||||
- tapOn: "Open drawer menu"
|
||||
- tapOn:
|
||||
id: "menuItemButton-Feeds"
|
||||
- tapOn:
|
||||
id: "editFeedsBtn"
|
||||
- tapOn:
|
||||
label: "Tap on unpin"
|
||||
id: "feed-timeline-togglePin"
|
||||
- tapOn:
|
||||
label: "Save button"
|
||||
id: "saveChangesBtn"
|
||||
- tapOn: "Go back"
|
||||
- assertVisible:
|
||||
id: "homeScreenFeedTabs-selector-0"
|
||||
text: "alice-favs"
|
||||
- assertNotVisible: "Following"
|
||||
@@ -1,55 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users&follows&posts&feeds
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
text: "Feeds ✨"
|
||||
- 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"
|
||||
- tapOn:
|
||||
id: "profilePager-selector-5"
|
||||
- tapOn: "alice-favs"
|
||||
- tapOn: "Pin to Home"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- tapOn:
|
||||
id: "bottomBarHomeBtn"
|
||||
- assertNotVisible: "Feeds ✨"
|
||||
|
||||
- tapOn:
|
||||
label: "Can like posts"
|
||||
id: "likeBtn"
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
|
||||
- tapOn:
|
||||
label: "Can repost posts"
|
||||
id: "repostBtn"
|
||||
- tapOn: "Repost"
|
||||
- tapOn:
|
||||
id: "repostBtn"
|
||||
- tapOn: "Remove repost"
|
||||
|
||||
- tapOn:
|
||||
label: "Can delete posts"
|
||||
id: "postDropdownBtn"
|
||||
childOf:
|
||||
id: "feedItem-by-alice.test"
|
||||
- tapOn: "Delete post"
|
||||
- tapOn: "Delete"
|
||||
@@ -1,29 +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
|
||||
- tapOn:
|
||||
text: "Not Now"
|
||||
optional: true
|
||||
- assertVisible: "Following"
|
||||
@@ -1,47 +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 list"
|
||||
- tapOn:
|
||||
id: "editNameInput"
|
||||
- inputText: "Muted Users"
|
||||
- tapOn:
|
||||
id: "editDescriptionInput"
|
||||
- inputText: "Shhh"
|
||||
- tapOn: "Save"
|
||||
- tapOn: "Save"
|
||||
|
||||
# view modlist
|
||||
- assertVisible: "Muted Users"
|
||||
- assertVisible: "Shhh"
|
||||
|
||||
- tapOn:
|
||||
label: "Dropdown"
|
||||
point: "71%,9%"
|
||||
|
||||
- tapOn: "Mute accounts"
|
||||
- tapOn: "Mute list"
|
||||
- tapOn: "Unmute"
|
||||
|
||||
- tapOn:
|
||||
label: "Dropdown"
|
||||
point: "71%,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,29 +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
|
||||
- assertVisible: "Search your library…"
|
||||
- tapOn:
|
||||
point: "50%,22%"
|
||||
- waitForAnimationToEnd
|
||||
- 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,20 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&follows&posts"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- tapOn:
|
||||
id: "postDropdownBtn"
|
||||
index: 0
|
||||
- tapOn:
|
||||
id: "postDropdownReportBtn"
|
||||
- tapOn: "Create report for Misleading Post"
|
||||
- tapOn: "Send report to Dev-env Moderation"
|
||||
- tapOn:
|
||||
point: "50%,90%"
|
||||
@@ -1,112 +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
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn:
|
||||
id: "bottomBarProfileBtn"
|
||||
|
||||
# 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: "Done"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn:
|
||||
id: "changeAvatarBtn"
|
||||
- tapOn: "Upload from Library"
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "Done"
|
||||
- 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,41 +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
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn:
|
||||
id: "bottomBarSearchBtn"
|
||||
- tapOn: "Search for posts, users, or feeds"
|
||||
- inputText: "b"
|
||||
- tapOn:
|
||||
id: "searchAutoCompleteResult-bob.test"
|
||||
- assertVisible:
|
||||
id: "profileView"
|
||||
|
||||
# Can follow/unfollow another user
|
||||
- tapOn:
|
||||
id: "followBtn"
|
||||
- tapOn:
|
||||
point: 65%,35% # dismiss the subscribe prompt
|
||||
- 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,24 +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: "Carla's avatar"
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownBtn"
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownReportBtn"
|
||||
- tapOn:
|
||||
id: "report:option:com.atproto.moderation.defs#reasonSpam"
|
||||
- assertVisible:
|
||||
id: "report:labeler:mod-authority.test"
|
||||
- tapOn:
|
||||
id: "report:submit"
|
||||
- assertNotVisible:
|
||||
id: "report:dialog"
|
||||
@@ -1,24 +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:
|
||||
id: "postDropdownBtn"
|
||||
index: 0
|
||||
- tapOn:
|
||||
id: "postDropdownReportBtn"
|
||||
- tapOn:
|
||||
id: "report:option:com.atproto.moderation.defs#reasonSpam"
|
||||
- assertVisible:
|
||||
id: "report:labeler:mod-authority.test"
|
||||
- tapOn:
|
||||
id: "report:submit"
|
||||
- assertNotVisible:
|
||||
id: "report:dialog"
|
||||
@@ -1,35 +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:
|
||||
id: "postDropdownBtn"
|
||||
index: 0
|
||||
- tapOn:
|
||||
id: "postDropdownReportBtn"
|
||||
- tapOn:
|
||||
id: "report:option:com.atproto.moderation.defs#reasonOther"
|
||||
- assertVisible:
|
||||
id: "report:labeler:mod-authority.test"
|
||||
# reason "other" defaults with details open
|
||||
- assertVisible:
|
||||
id: "report:details"
|
||||
- tapOn:
|
||||
id: "report:clearOption"
|
||||
- assertNotVisible:
|
||||
id: "report:details"
|
||||
- tapOn:
|
||||
id: "report:option:com.atproto.moderation.defs#reasonSpam"
|
||||
- assertVisible:
|
||||
id: "report:labeler:mod-authority.test"
|
||||
- tapOn:
|
||||
id: "report:submit"
|
||||
- assertNotVisible:
|
||||
id: "report:dialog"
|
||||
@@ -1,30 +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:
|
||||
id: "postDropdownBtn"
|
||||
index: 0
|
||||
- tapOn:
|
||||
id: "postDropdownReportBtn"
|
||||
- tapOn:
|
||||
id: "report:option:com.atproto.moderation.defs#reasonOther"
|
||||
- assertVisible:
|
||||
id: "report:labeler:mod-authority.test"
|
||||
# reason "other" defaults with details open
|
||||
- assertVisible:
|
||||
id: "report:details"
|
||||
- tapOn:
|
||||
id: "report:details"
|
||||
- inputText: "This is a test report"
|
||||
- tapOn:
|
||||
id: "report:submit"
|
||||
- assertNotVisible:
|
||||
id: "report:dialog"
|
||||
@@ -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"
|
||||
- assertVisible: "Search for posts, users, or feeds"
|
||||
- tapOn: "Search for posts, users, or feeds"
|
||||
- inputText: "b"
|
||||
- tapOn:
|
||||
id: "searchAutoCompleteResult-bob.test"
|
||||
- assertVisible:
|
||||
id: "profileView"
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&posts&feeds"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
- assertVisible:
|
||||
id: "storybookBtn"
|
||||
- tapOn:
|
||||
id: "storybookBtn"
|
||||
- tapOn:
|
||||
id: "sharedPrefsTestOpenBtn"
|
||||
- tapOn:
|
||||
id: "setStringBtn"
|
||||
- assertVisible: "Hello"
|
||||
- tapOn:
|
||||
id: "removeStringBtn"
|
||||
- assertVisible: "undefined"
|
||||
- tapOn:
|
||||
id: "setBoolBtn"
|
||||
- assertVisible: "true"
|
||||
- tapOn:
|
||||
id: "setNumberBtn"
|
||||
- assertVisible: "123"
|
||||
- tapOn:
|
||||
id: "addToSetBtn"
|
||||
- assertVisible: "true"
|
||||
- tapOn:
|
||||
id: "removeFromSetBtn"
|
||||
- assertVisible: "false"
|
||||
@@ -1,83 +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"
|
||||
- assertVisible:
|
||||
id: "composeFAB"
|
||||
- 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,76 +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
|
||||
- extendedWaitUntil:
|
||||
visible: "Thread root"
|
||||
- tapOn: "Thread root"
|
||||
- assertVisible: "Thread reply"
|
||||
|
||||
# Can like the root post
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- assertVisible: "1 like"
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-bob.test"
|
||||
- assertNotVisible: "1 like"
|
||||
|
||||
# Can like a reply post
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
childOf:
|
||||
id: "postThreadItem-by-carla.test"
|
||||
- tapOn:
|
||||
id: "likeBtn"
|
||||
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"
|
||||
@@ -0,0 +1,12 @@
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
module.exports = {
|
||||
rootDir: '..',
|
||||
testMatch: ['<rootDir>/__e2e__/**/*.test.ts'],
|
||||
testTimeout: 120000,
|
||||
maxWorkers: 1,
|
||||
globalSetup: 'detox/runners/jest/globalSetup',
|
||||
globalTeardown: 'detox/runners/jest/globalTeardown',
|
||||
reporters: ['detox/runners/jest/reporter'],
|
||||
testEnvironment: 'detox/runners/jest/testEnvironment',
|
||||
verbose: true,
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# flow.yaml
|
||||
|
||||
appId: xyz.blueskyweb.app
|
||||
@@ -75,3 +74,4 @@ appId: xyz.blueskyweb.app
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
- "scroll"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {createServer as createHTTPServer} from 'node:http'
|
||||
import {parse} from 'node:url'
|
||||
|
||||
import {createServer, TestPDS} from '../jest/test-pds'
|
||||
|
||||
async function main() {
|
||||
@@ -15,7 +14,8 @@ async function main() {
|
||||
await server?.close()
|
||||
console.log('Starting new server')
|
||||
const inviteRequired = url?.query && 'invite' in url.query
|
||||
server = await createServer({inviteRequired})
|
||||
const phoneRequired = url?.query && 'phone' in url.query
|
||||
server = await createServer({inviteRequired, phoneRequired})
|
||||
console.log('Listening at', server.pdsUrl)
|
||||
if (url?.query) {
|
||||
if ('users' in url.query) {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- launchApp:
|
||||
appId: "xyz.blueskyweb.app"
|
||||
clearState: true
|
||||
- waitForAnimationToEnd
|
||||
- tapOn: "http://localhost:8081"
|
||||
- waitForAnimationToEnd
|
||||
- extendedWaitUntil:
|
||||
visible: "Continue"
|
||||
- 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: '',
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
||||
|
||||
describe('Composer', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
})
|
||||
|
||||
it('Post text only', async () => {
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await device.takeScreenshot('1- opened composer')
|
||||
await element(by.id('composerTextInput')).typeText('Post text only')
|
||||
await device.takeScreenshot('2- entered text')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await device.takeScreenshot('3- opened general section')
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Post with an image', async () => {
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Post with an image')
|
||||
await element(by.id('openGalleryBtn')).tap()
|
||||
await sleep(1e3)
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Post with a link card', async () => {
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await element(by.id('composerTextInput')).typeText(
|
||||
'Post with a https://example.com link card',
|
||||
)
|
||||
await element(by.id('addLinkCardBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Reply text only', async () => {
|
||||
await element(by.id('e2eRefreshHome')).tap()
|
||||
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Reply text only')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Reply with an image', async () => {
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Reply with an image')
|
||||
await element(by.id('openGalleryBtn')).tap()
|
||||
await sleep(1e3)
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Reply with a link card', async () => {
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText(
|
||||
'Reply with a https://example.com link card',
|
||||
)
|
||||
await element(by.id('addLinkCardBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('QP text only', async () => {
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await element(by.id('composerTextInput')).typeText('QP text only')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('QP with an image', async () => {
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await element(by.id('composerTextInput')).typeText('QP with an image')
|
||||
await element(by.id('openGalleryBtn')).tap()
|
||||
await sleep(1e3)
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('QP with a link card', async () => {
|
||||
const post = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await element(by.id('composerTextInput')).typeText(
|
||||
'QP with a https://example.com link card',
|
||||
)
|
||||
await element(by.id('addLinkCardBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, createServer} from '../util'
|
||||
|
||||
describe('Create account', () => {
|
||||
let service: string
|
||||
beforeAll(async () => {
|
||||
service = await createServer('')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('I can create a new account', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
|
||||
await element(by.id('createAccountButton')).tap()
|
||||
await device.takeScreenshot('1- opened create account screen')
|
||||
await element(by.id('selectServiceButton')).tap()
|
||||
await device.takeScreenshot('2- selected other server')
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await device.takeScreenshot('3- input test server URL')
|
||||
await element(by.id('emailInput')).typeText('example@test.com')
|
||||
await element(by.id('passwordInput')).typeText('hunter2')
|
||||
await device.takeScreenshot('4- entered account details')
|
||||
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await element(by.id('handleInput')).typeText('e2e-test')
|
||||
await device.takeScreenshot('5- entered handle')
|
||||
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await expect(element(by.id('onboardingInterests'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,211 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util'
|
||||
|
||||
describe('Curate lists', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&follows&posts')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login and create a curatelists', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('e2eGotoLists')).tap()
|
||||
await element(by.id('newUserListBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editNameInput')).typeText('Good Ppl')
|
||||
await element(by.id('editDescriptionInput')).typeText('They good')
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await element(by.text('About')).tap()
|
||||
await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl')
|
||||
await expect(element(by.id('listDescription'))).toHaveText('They good')
|
||||
})
|
||||
|
||||
it('Edit display name and description via the edit curatelist modal', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editNameInput')).clearText()
|
||||
await element(by.id('editNameInput')).typeText('Bad Ppl')
|
||||
await element(by.id('editDescriptionInput')).clearText()
|
||||
await element(by.id('editDescriptionInput')).typeText('They bad')
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
|
||||
await expect(element(by.id('listDescription'))).toHaveText('They bad')
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Remove description via the edit curatelist modal', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editDescriptionInput')).clearText()
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('listDescription'))).not.toBeVisible()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Set avi via the edit curatelist modal', async () => {
|
||||
await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('changeAvatarBtn')).tap()
|
||||
await element(by.text('Library')).tap()
|
||||
await sleep(3e3)
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Remove avi via the edit curatelist modal', async () => {
|
||||
await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('changeAvatarBtn')).tap()
|
||||
await element(by.text('Remove')).tap()
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Delete the curatelist', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Delete List')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await expect(element(by.id('listsEmpty'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Create a new curatelist', async () => {
|
||||
await element(by.id('e2eGotoLists')).tap()
|
||||
await element(by.id('newUserListBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editNameInput')).typeText('Good Ppl')
|
||||
await element(by.id('editDescriptionInput')).typeText('They good')
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await element(by.text('About')).tap()
|
||||
await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl')
|
||||
await expect(element(by.id('listDescription'))).toHaveText('They good')
|
||||
})
|
||||
|
||||
it('Adds users on curatelists from the list', async () => {
|
||||
await element(by.text('About')).tap()
|
||||
await element(by.id('addUserBtn')).tap()
|
||||
await expect(element(by.id('listAddUserModal'))).toBeVisible()
|
||||
await waitFor(element(by.id('user-bob.test-addBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('listAddUserModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('user-bob.test'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Shows posts by the users in the list', async () => {
|
||||
await element(by.text('Posts')).tap()
|
||||
await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Pins the list', async () => {
|
||||
await expect(element(by.id('pinBtn'))).toBeVisible()
|
||||
await element(by.id('pinBtn')).tap()
|
||||
await element(by.id('e2eGotoHome')).tap()
|
||||
await element(by.id('homeScreenFeedTabs-Good Ppl')).tap()
|
||||
await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
|
||||
|
||||
await element(by.id('bottomBarFeedsBtn')).tap()
|
||||
await element(by.id('saved-feed-Good Ppl')).tap()
|
||||
await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
|
||||
|
||||
await element(by.id('unpinBtn')).tap()
|
||||
await element(by.id('bottomBarHomeBtn')).tap()
|
||||
await expect(
|
||||
element(by.id('homeScreenFeedTabs-Good Ppl')),
|
||||
).not.toBeVisible()
|
||||
|
||||
await element(by.id('e2eGotoLists')).tap()
|
||||
await element(by.id('list-Good Ppl')).tap()
|
||||
})
|
||||
|
||||
it('Removes users on curatelists from the list', async () => {
|
||||
await element(by.text('About')).tap()
|
||||
await expect(element(by.id('user-bob.test'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-editBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Shows the curatelist on my profile', async () => {
|
||||
await element(by.id('bottomBarProfileBtn')).tap()
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-5')).tap()
|
||||
await element(by.id('list-Good Ppl')).tap()
|
||||
})
|
||||
|
||||
it('Adds and removes users on curatelists from the profile', async () => {
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('bob')
|
||||
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Add to Lists')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Add to Lists')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can report a user list', async () => {
|
||||
await element(by.id('e2eGotoSettings')).tap()
|
||||
await element(by.id('signOutBtn')).tap()
|
||||
await loginAsBob()
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('alice')
|
||||
await element(by.id('searchAutoCompleteResult-alice.test')).tap()
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-3')).tap()
|
||||
await element(by.id('list-Good Ppl')).tap()
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Report List')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await expect(element(by.text('Report List'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('Home screen', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&follows&posts&feeds')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
})
|
||||
|
||||
it('Can go to feeds page using feeds button in tab bar', async () => {
|
||||
await element(by.id('homeScreenFeedTabs-Feeds ✨')).tap()
|
||||
await expect(element(by.text('Discover new feeds'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Feeds button disappears after pinning a feed', async () => {
|
||||
await element(by.id('bottomBarProfileBtn')).tap()
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-4')).tap()
|
||||
await element(by.id('feed-alice-favs')).tap()
|
||||
await element(by.id('pinBtn')).tap()
|
||||
await element(by.id('bottomBarHomeBtn')).tap()
|
||||
await expect(
|
||||
element(by.id('homeScreenFeedTabs-Feeds ✨')),
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can like posts', async () => {
|
||||
const carlaPosts = by.id('feedItem-by-carla.test')
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can repost posts', async () => {
|
||||
const carlaPosts = by.id('feedItem-by-carla.test')
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can report posts', async () => {
|
||||
const carlaPosts = by.id('feedItem-by-carla.test')
|
||||
await element(by.id('postDropdownBtn').withAncestor(carlaPosts))
|
||||
.atIndex(0)
|
||||
.tap()
|
||||
await element(by.text('Report post')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can swipe between feeds', async () => {
|
||||
await element(by.id('homeScreen')).swipe('left', 'fast', 0.75)
|
||||
await expect(element(by.id('customFeedPage'))).toBeVisible()
|
||||
await element(by.id('homeScreen')).swipe('right', 'fast', 0.75)
|
||||
await expect(element(by.id('followingFeedPage'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can tap between feeds', async () => {
|
||||
await element(by.id('homeScreenFeedTabs-alice-favs')).tap()
|
||||
await expect(element(by.id('customFeedPage'))).toBeVisible()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
await expect(element(by.id('followingFeedPage'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can delete posts', async () => {
|
||||
const alicePosts = by.id('feedItem-by-alice.test')
|
||||
await expect(element(alicePosts.withDescendant(by.text('Post')))).toExist()
|
||||
await element(by.id('postDropdownBtn').withAncestor(alicePosts))
|
||||
.atIndex(0)
|
||||
.tap()
|
||||
await element(by.text('Delete post')).tap()
|
||||
await expect(element(by.id('confirmModal'))).toBeVisible()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await expect(
|
||||
element(alicePosts.withDescendant(by.text('Post'))),
|
||||
).not.toExist()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('invite-codes', () => {
|
||||
let service: string
|
||||
let inviteCode = ''
|
||||
beforeAll(async () => {
|
||||
service = await createServer('?users&invite')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('I can fetch invite codes', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('e2eOpenInviteCodesModal')).tap()
|
||||
await expect(element(by.id('inviteCodesModal'))).toBeVisible()
|
||||
const attrs = await element(by.id('inviteCode-0-code')).getAttributes()
|
||||
inviteCode = attrs.text
|
||||
await element(by.id('closeBtn')).tap()
|
||||
await element(by.id('e2eSignOut')).tap()
|
||||
})
|
||||
|
||||
it('I can create a new account with the invite code', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
await element(by.id('createAccountButton')).tap()
|
||||
await device.takeScreenshot('1- opened create account screen')
|
||||
await element(by.id('selectServiceButton')).tap()
|
||||
await device.takeScreenshot('2- selected other server')
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await device.takeScreenshot('3- input test server URL')
|
||||
await element(by.id('inviteCodeInput')).typeText(inviteCode)
|
||||
await element(by.id('emailInput')).typeText('example@test.com')
|
||||
await element(by.id('passwordInput')).typeText('hunter2')
|
||||
await device.takeScreenshot('4- entered account details')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
await element(by.id('handleInput')).typeText('e2e-test')
|
||||
await device.takeScreenshot('4- entered handle')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
await expect(element(by.id('onboardingInterests'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('invite-codes', () => {
|
||||
let service: string
|
||||
let inviteCode = ''
|
||||
beforeAll(async () => {
|
||||
service = await createServer('?users&invite&phone')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('I can fetch invite codes', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('e2eOpenInviteCodesModal')).tap()
|
||||
await expect(element(by.id('inviteCodesModal'))).toBeVisible()
|
||||
const attrs = await element(by.id('inviteCode-0-code')).getAttributes()
|
||||
inviteCode = attrs.text
|
||||
await element(by.id('closeBtn')).tap()
|
||||
await element(by.id('e2eSignOut')).tap()
|
||||
})
|
||||
|
||||
it('I can create a new account with the invite code', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
await element(by.id('createAccountButton')).tap()
|
||||
await device.takeScreenshot('1- opened create account screen')
|
||||
await element(by.id('selectServiceButton')).tap()
|
||||
await device.takeScreenshot('2- selected other server')
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await device.takeScreenshot('3- input test server URL')
|
||||
await element(by.id('inviteCodeInput')).typeText(inviteCode)
|
||||
await element(by.id('emailInput')).typeText('example@test.com')
|
||||
await element(by.id('passwordInput')).typeText('hunter2')
|
||||
await device.takeScreenshot('4- entered account details')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
await element(by.id('phoneInput')).typeText('2345551234')
|
||||
await element(by.id('requestCodeBtn')).tap()
|
||||
await device.takeScreenshot('5- requested code')
|
||||
await element(by.id('codeInput')).typeText('000000')
|
||||
await device.takeScreenshot('6- entered code')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
await element(by.id('handleInput')).typeText('e2e-test')
|
||||
await device.takeScreenshot('7- entered handle')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
await expect(element(by.id('onboardingInterests'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, login, createServer} from '../util'
|
||||
|
||||
describe('Login', () => {
|
||||
let service: string
|
||||
beforeAll(async () => {
|
||||
service = await createServer('?users')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('As Alice, I can login', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
|
||||
await expect(element(by.id('signInButton'))).toBeVisible()
|
||||
await login(service, 'alice', 'hunter2', {
|
||||
takeScreenshots: true,
|
||||
})
|
||||
await device.takeScreenshot('5- opened home screen')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,163 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('Mergefeed', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?mergefeed')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
await loginAsAlice()
|
||||
await element(by.id('e2eToggleMergefeed')).tap()
|
||||
await element(by.id('bottomBarFeedsBtn')).tap()
|
||||
await element(by.id('feed-alice-favs-toggleSave')).tap()
|
||||
await element(by.id('e2eGotoHome')).tap()
|
||||
})
|
||||
|
||||
it('Sees the expected mix of posts with default filters', async () => {
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
// followed users
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-carla.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 9')
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-bob.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 9')
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'up',
|
||||
'fast',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
// feed users
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 0')
|
||||
})
|
||||
|
||||
it('Sees the expected mix of posts with replies disabled', async () => {
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'fast',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'fast',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
await element(by.id('viewHeaderHomeFeedPrefsBtn')).tap()
|
||||
await element(by.id('toggleRepliesBtn')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
|
||||
// followed users
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-carla.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 9')
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-bob.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 9')
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'up',
|
||||
'fast',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
|
||||
// feed users
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 0')
|
||||
})
|
||||
|
||||
it('Sees the expected mix of posts with no follows', async () => {
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'fast',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('bob')
|
||||
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
await element(by.id('unfollowBtn')).tap()
|
||||
await element(by.id('profileHeaderBackBtn')).tap()
|
||||
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('searchTextInputClearBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
await element(by.id('searchTextInputClearBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('carla')
|
||||
await element(by.id('searchAutoCompleteResult-carla.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
await element(by.id('unfollowBtn')).tap()
|
||||
await element(by.id('profileHeaderBackBtn')).tap()
|
||||
|
||||
await element(by.id('bottomBarHomeBtn')).tap()
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
|
||||
// followed users NOT present
|
||||
await expect(element(by.id('feedItem-by-carla.test'))).not.toExist()
|
||||
await expect(element(by.id('feedItem-by-bob.test'))).not.toExist()
|
||||
|
||||
// feed users
|
||||
await expect(
|
||||
element(
|
||||
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
|
||||
).atIndex(0),
|
||||
).toHaveText('Post 0')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,189 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, loginAsBob, createServer} from '../util'
|
||||
|
||||
describe('Mod lists', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&follows&labels')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login and view my modlists', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('e2eGotoModeration')).tap()
|
||||
await element(by.id('moderationlistsBtn')).tap()
|
||||
await expect(element(by.id('list-Muted Users'))).toBeVisible()
|
||||
await element(by.id('list-Muted Users')).tap()
|
||||
await expect(
|
||||
element(by.id('user-muted-by-list-account.test')),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
it('Toggle mute subscription', async () => {
|
||||
await element(by.id('unmuteBtn')).tap()
|
||||
await element(by.id('subscribeBtn')).tap()
|
||||
await element(by.text('Mute accounts')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
})
|
||||
|
||||
it('Edit display name and description via the edit modlist modal', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editNameInput')).clearText()
|
||||
await element(by.id('editNameInput')).typeText('Bad Ppl')
|
||||
await element(by.id('editDescriptionInput')).clearText()
|
||||
await element(by.id('editDescriptionInput')).typeText('They bad')
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
|
||||
await expect(element(by.id('listDescription'))).toHaveText('They bad')
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Remove description via the edit modlist modal', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Edit list details')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editDescriptionInput')).clearText()
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('listDescription'))).not.toBeVisible()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('headerDropdownBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
// DISABLED e2e environment is real finicky about avatar uploads -prf
|
||||
// it('Set avi via the edit modlist modal', async () => {
|
||||
// await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
// await element(by.id('headerDropdownBtn')).tap()
|
||||
// await element(by.text('Edit list details')).tap()
|
||||
// await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
// await element(by.id('changeAvatarBtn')).tap()
|
||||
// await element(by.text('Library')).tap()
|
||||
// await sleep(3e3)
|
||||
// await element(by.id('saveBtn')).tap()
|
||||
// await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
// await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
// // have to wait for the toast to clear
|
||||
// await waitFor(element(by.id('headerDropdownBtn')))
|
||||
// .toBeVisible()
|
||||
// .withTimeout(5000)
|
||||
// })
|
||||
|
||||
// it('Remove avi via the edit modlist modal', async () => {
|
||||
// await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
// await element(by.id('headerDropdownBtn')).tap()
|
||||
// await element(by.text('Edit list details')).tap()
|
||||
// await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
// await element(by.id('changeAvatarBtn')).tap()
|
||||
// await element(by.text('Remove')).tap()
|
||||
// await element(by.id('saveBtn')).tap()
|
||||
// await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
// await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
// // have to wait for the toast to clear
|
||||
// await waitFor(element(by.id('headerDropdownBtn')))
|
||||
// .toBeVisible()
|
||||
// .withTimeout(5000)
|
||||
// })
|
||||
|
||||
it('Delete the modlist', async () => {
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Delete List')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await expect(element(by.id('listsEmpty'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Create a new modlist', async () => {
|
||||
await element(by.id('newModListBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
|
||||
await element(by.id('editNameInput')).typeText('Bad Ppl')
|
||||
await element(by.id('editDescriptionInput')).typeText('They bad')
|
||||
await element(by.id('saveBtn')).tap()
|
||||
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
|
||||
await expect(element(by.id('listDescription'))).toHaveText('They bad')
|
||||
})
|
||||
|
||||
it('Adds and removes users on modlists from the list', async () => {
|
||||
await element(by.id('addUserBtn')).tap()
|
||||
await expect(element(by.id('listAddUserModal'))).toBeVisible()
|
||||
await waitFor(element(by.id('user-warn-posts.test-addBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
await element(by.id('user-warn-posts.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('listAddUserModal'))).not.toBeVisible()
|
||||
await element(by.id('listItems-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
await expect(element(by.id('user-warn-posts.test'))).toBeVisible()
|
||||
await element(by.id('user-warn-posts.test-editBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-warn-posts.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Shows the modlist on my profile', async () => {
|
||||
await element(by.id('bottomBarProfileBtn')).tap()
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-5')).tap()
|
||||
await element(by.id('list-Bad Ppl')).tap()
|
||||
})
|
||||
|
||||
it('Adds and removes users on modlists from the profile', async () => {
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('bob')
|
||||
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Add to Lists')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Add to Lists')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
|
||||
await element(by.id('user-bob.test-addBtn')).tap()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can report a mute list', async () => {
|
||||
await element(by.id('e2eGotoSettings')).tap()
|
||||
await element(by.id('signOutBtn')).tap()
|
||||
await loginAsBob()
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('alice')
|
||||
await element(by.id('searchAutoCompleteResult-alice.test')).tap()
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-3')).tap()
|
||||
await element(by.id('list-Bad Ppl')).tap()
|
||||
await element(by.id('headerDropdownBtn')).tap()
|
||||
await element(by.text('Report List')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await expect(element(by.text('Report List'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,193 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
||||
|
||||
describe('Profile screen', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&posts&feeds')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login and navigate to my profile', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('bottomBarProfileBtn')).tap()
|
||||
})
|
||||
|
||||
it('Can see feeds', async () => {
|
||||
await element(by.id('profilePager-selector')).swipe('left')
|
||||
await element(by.id('profilePager-selector-4')).tap()
|
||||
await expect(element(by.id('feed-alice-favs'))).toBeVisible()
|
||||
await element(by.id('profilePager-selector')).swipe('right')
|
||||
await element(by.id('profilePager-selector-0')).tap()
|
||||
})
|
||||
|
||||
it('Open and close edit profile modal', async () => {
|
||||
await element(by.id('profileHeaderEditProfileButton')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).toBeVisible()
|
||||
await element(by.id('editProfileCancelBtn')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Edit display name and description via the edit profile modal', async () => {
|
||||
await element(by.id('profileHeaderEditProfileButton')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).toBeVisible()
|
||||
await element(by.id('editProfileDisplayNameInput')).clearText()
|
||||
await element(by.id('editProfileDisplayNameInput')).typeText('Alicia')
|
||||
await element(by.id('editProfileDescriptionInput')).clearText()
|
||||
await element(by.id('editProfileDescriptionInput')).typeText(
|
||||
'One cool hacker',
|
||||
)
|
||||
await element(by.id('editProfileSaveBtn')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('profileHeaderDisplayName'))).toHaveText(
|
||||
'Alicia',
|
||||
)
|
||||
await expect(element(by.id('profileHeaderDescription'))).toHaveText(
|
||||
'One cool hacker',
|
||||
)
|
||||
})
|
||||
|
||||
it('Remove display name and description via the edit profile modal', async () => {
|
||||
await element(by.id('profileHeaderEditProfileButton')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).toBeVisible()
|
||||
await element(by.id('editProfileDisplayNameInput')).clearText()
|
||||
await element(by.id('editProfileDescriptionInput')).clearText()
|
||||
await element(by.id('editProfileSaveBtn')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('profileHeaderDisplayName'))).toHaveText(
|
||||
'alice.test',
|
||||
)
|
||||
await expect(element(by.id('profileHeaderDescription'))).not.toExist()
|
||||
})
|
||||
|
||||
it('Set avi and banner via the edit profile modal', async () => {
|
||||
await expect(element(by.id('userBannerFallback'))).toExist()
|
||||
await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
await element(by.id('profileHeaderEditProfileButton')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).toBeVisible()
|
||||
await element(by.id('changeBannerBtn')).tap()
|
||||
await element(by.text('Library')).tap()
|
||||
await sleep(3e3)
|
||||
await element(by.id('changeAvatarBtn')).tap()
|
||||
await element(by.text('Library')).tap()
|
||||
await sleep(3e3)
|
||||
await element(by.id('editProfileSaveBtn')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('userBannerImage'))).toExist()
|
||||
await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
})
|
||||
|
||||
it('Remove avi and banner via the edit profile modal', async () => {
|
||||
await expect(element(by.id('userBannerImage'))).toExist()
|
||||
await expect(element(by.id('userAvatarImage'))).toExist()
|
||||
await element(by.id('profileHeaderEditProfileButton')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).toBeVisible()
|
||||
await element(by.id('changeBannerBtn')).tap()
|
||||
await element(by.text('Remove')).tap()
|
||||
await element(by.id('changeAvatarBtn')).tap()
|
||||
await element(by.text('Remove')).tap()
|
||||
await element(by.id('editProfileSaveBtn')).tap()
|
||||
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
|
||||
await expect(element(by.id('userBannerFallback'))).toExist()
|
||||
await expect(element(by.id('userAvatarFallback'))).toExist()
|
||||
})
|
||||
|
||||
it('Navigate to another user profile', async () => {
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('searchTextInput')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
await element(by.id('searchTextInput')).typeText('bob')
|
||||
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can follow/unfollow another user', async () => {
|
||||
await element(by.id('followBtn')).tap()
|
||||
await expect(element(by.id('unfollowBtn'))).toBeVisible()
|
||||
await element(by.id('unfollowBtn')).tap()
|
||||
await expect(element(by.id('followBtn'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can mute/unmute another user', async () => {
|
||||
await expect(element(by.id('profileHeaderAlert'))).not.toExist()
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Mute Account')).tap()
|
||||
await expect(element(by.id('profileHeaderAlert'))).toBeVisible()
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Unmute Account')).tap()
|
||||
await expect(element(by.id('profileHeaderAlert'))).not.toExist()
|
||||
})
|
||||
|
||||
it('Can report another user', async () => {
|
||||
await element(by.id('profileHeaderDropdownBtn')).tap()
|
||||
await element(by.text('Report Account')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can like posts', async () => {
|
||||
await element(by.id('postsFeed-flatlist')).swipe(
|
||||
'down',
|
||||
'slow',
|
||||
1,
|
||||
0.5,
|
||||
0.5,
|
||||
)
|
||||
|
||||
const posts = by.id('feedItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(posts)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(posts)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(posts)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can repost posts', async () => {
|
||||
const posts = by.id('feedItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(posts)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(posts)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(posts)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can report posts', async () => {
|
||||
const posts = by.id('feedItem-by-bob.test')
|
||||
await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
|
||||
await element(by.text('Report post')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('Search screen', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await loginAsAlice()
|
||||
})
|
||||
|
||||
it('Navigate to another user profile via autocomplete', async () => {
|
||||
await element(by.id('bottomBarSearchBtn')).tap()
|
||||
await element(by.id('searchTextInput')).typeText('bob')
|
||||
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
|
||||
await expect(element(by.id('profileView'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
||||
|
||||
describe('Self-labeling', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users')
|
||||
await openApp({
|
||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||
})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
})
|
||||
|
||||
it('Post an image with the porn label', async () => {
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Post with an image')
|
||||
await element(by.id('openGalleryBtn')).tap()
|
||||
await sleep(3e3)
|
||||
await element(by.id('labelsBtn')).tap()
|
||||
await element(by.id('pornLabelBtn')).tap()
|
||||
await element(by.id('confirmBtn')).tap()
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
const posts = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('e2eRefreshHome')).tap()
|
||||
await expect(
|
||||
element(by.id('contentHider-embed').withAncestor(posts)).atIndex(0),
|
||||
).toExist()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('Shell', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('Login', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
})
|
||||
|
||||
it('Can swipe the shelf open', async () => {
|
||||
await element(by.id('homeScreen')).swipe('right', 'fast', 0.75)
|
||||
await expect(element(by.id('drawer'))).toBeVisible()
|
||||
await element(by.id('drawer')).swipe('left', 'fast', 0.75)
|
||||
await expect(element(by.id('drawer'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can open the shelf by pressing the header avi', async () => {
|
||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
||||
await expect(element(by.id('drawer'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can navigate using the shelf', async () => {
|
||||
await element(by.id('menuItemButton-Notifications')).tap()
|
||||
await expect(element(by.id('drawer'))).not.toBeVisible()
|
||||
await expect(element(by.id('notificationsScreen'))).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, createServer} from '../util'
|
||||
|
||||
describe('Create account', () => {
|
||||
let service: string
|
||||
beforeAll(async () => {
|
||||
service = await createServer('?phone')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('I can create a new account with text verification', async () => {
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
|
||||
await element(by.id('createAccountButton')).tap()
|
||||
await device.takeScreenshot('1- opened create account screen')
|
||||
await element(by.id('selectServiceButton')).tap()
|
||||
await device.takeScreenshot('2- selected other server')
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await device.takeScreenshot('3- input test server URL')
|
||||
await element(by.id('emailInput')).typeText('text-verification@test.com')
|
||||
await element(by.id('passwordInput')).typeText('hunter2')
|
||||
await device.takeScreenshot('4- entered account details')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await element(by.id('phoneInput')).typeText('8042221111')
|
||||
await element(by.id('requestCodeBtn')).tap()
|
||||
await device.takeScreenshot('5- requested code')
|
||||
|
||||
await element(by.id('codeInput')).typeText('000000')
|
||||
await device.takeScreenshot('6- entered code')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await element(by.id('handleInput')).typeText('text-verification-test')
|
||||
await device.takeScreenshot('7- entered handle')
|
||||
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await expect(element(by.id('onboardingInterests'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('failed text verification correctly goes back to the code input screen', async () => {
|
||||
await element(by.id('e2eSignOut')).tap()
|
||||
await element(by.id('e2eOpenLoggedOutView')).tap()
|
||||
|
||||
await element(by.id('createAccountButton')).tap()
|
||||
await device.takeScreenshot('1- opened create account screen')
|
||||
await element(by.id('selectServiceButton')).tap()
|
||||
await device.takeScreenshot('2- selected other server')
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
await device.takeScreenshot('3- input test server URL')
|
||||
await element(by.id('emailInput')).typeText('text-verification2@test.com')
|
||||
await element(by.id('passwordInput')).typeText('hunter2')
|
||||
await device.takeScreenshot('4- entered account details')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await element(by.id('phoneInput')).typeText('8042221111')
|
||||
await element(by.id('requestCodeBtn')).tap()
|
||||
await device.takeScreenshot('5- requested code')
|
||||
|
||||
await element(by.id('codeInput')).typeText('111111')
|
||||
await device.takeScreenshot('6- entered code')
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await element(by.id('handleInput')).typeText('text-verification-test2')
|
||||
await device.takeScreenshot('7- entered handle')
|
||||
|
||||
await element(by.id('nextBtn')).tap()
|
||||
|
||||
await expect(element(by.id('codeInput'))).toBeVisible()
|
||||
await device.takeScreenshot('8- got error')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,103 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, loginAsBob, createServer} from '../util'
|
||||
|
||||
describe('Thread muting', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&follows')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('Login, create a thread, and log out', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
await element(by.id('composeFAB')).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Test thread')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Login, reply to the thread, and log out', async () => {
|
||||
await loginAsBob()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
const alicePosts = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Reply 1')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
})
|
||||
|
||||
it('Login, confirm notification exists, mute thread, and log out', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('bottomBarNotificationsBtn')).tap()
|
||||
const bobNotifs = by.id('feedItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
|
||||
).toHaveText('Reply 1')
|
||||
await element(by.id('postDropdownBtn').withAncestor(bobNotifs))
|
||||
.atIndex(0)
|
||||
.tap()
|
||||
await element(by.text('Mute thread')).tap()
|
||||
// have to wait for the toast to clear
|
||||
await waitFor(element(by.id('viewHeaderDrawerBtn')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000)
|
||||
})
|
||||
|
||||
it('Login, reply to the thread twice, and log out', async () => {
|
||||
await loginAsBob()
|
||||
|
||||
await element(by.id('bottomBarProfileBtn')).tap()
|
||||
await element(by.id('profilePager-selector-1')).tap()
|
||||
const bobPosts = by.id('feedItem-by-bob.test')
|
||||
await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Reply 2')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
|
||||
const alicePosts = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
|
||||
await element(by.id('composerTextInput')).typeText('Reply 3')
|
||||
await element(by.id('composerPublishBtn')).tap()
|
||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||
|
||||
await element(by.id('bottomBarHomeBtn')).tap()
|
||||
})
|
||||
|
||||
it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => {
|
||||
await loginAsAlice()
|
||||
|
||||
await element(by.id('bottomBarNotificationsBtn')).tap()
|
||||
const bobNotifs = by.id('feedItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
|
||||
).not.toExist()
|
||||
|
||||
await element(by.id('bottomBarHomeBtn')).tap()
|
||||
const alicePosts = by.id('feedItem-by-alice.test')
|
||||
await element(by.id('postDropdownBtn').withAncestor(alicePosts))
|
||||
.atIndex(0)
|
||||
.tap()
|
||||
await element(by.text('Unmute thread')).tap()
|
||||
|
||||
// TODO
|
||||
// the swipe down to trigger PTR isnt working and I dont want to block on this
|
||||
// -prf
|
||||
// await element(by.id('bottomBarNotificationsBtn')).tap()
|
||||
// await element(by.id('notifsFeed')).swipe('down', 'fast')
|
||||
// await waitFor(element(by.id('postText').withAncestor(bobNotifs)))
|
||||
// .toBeVisible()
|
||||
// .withTimeout(5000)
|
||||
// await expect(
|
||||
// element(by.id('postText').withAncestor(bobNotifs)).atIndex(0),
|
||||
// ).toHaveText('Reply 2')
|
||||
// await expect(
|
||||
// element(by.id('postText').withAncestor(bobNotifs)).atIndex(1),
|
||||
// ).toHaveText('Reply 3')
|
||||
// await expect(
|
||||
// element(by.id('postText').withAncestor(bobNotifs)).atIndex(2),
|
||||
// ).toHaveText('Reply 1')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,128 @@
|
||||
/* eslint-env detox/detox */
|
||||
|
||||
import {describe, beforeAll, it} from '@jest/globals'
|
||||
import {expect} from 'detox'
|
||||
import {openApp, loginAsAlice, createServer} from '../util'
|
||||
|
||||
describe('Thread screen', () => {
|
||||
beforeAll(async () => {
|
||||
await createServer('?users&follows&thread')
|
||||
await openApp({permissions: {notifications: 'YES'}})
|
||||
})
|
||||
|
||||
it('Login & navigate to thread', async () => {
|
||||
await loginAsAlice()
|
||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||
await element(by.id('feedItem-by-bob.test')).atIndex(0).tap()
|
||||
await expect(
|
||||
element(
|
||||
by
|
||||
.id('postThreadItem-by-bob.test')
|
||||
.withDescendant(by.text('Thread root')),
|
||||
),
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
element(
|
||||
by
|
||||
.id('postThreadItem-by-carla.test')
|
||||
.withDescendant(by.text('Thread reply')),
|
||||
),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
it('Can like the root post', async () => {
|
||||
const post = by.id('postThreadItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).toHaveText('1 like')
|
||||
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can like a reply post', async () => {
|
||||
const post = by.id('postThreadItem-by-carla.test')
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(post)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(
|
||||
element(by.id('likeCount').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can repost the root post', async () => {
|
||||
const post = by.id('postThreadItem-by-bob.test')
|
||||
await expect(
|
||||
element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).toHaveText('1 repost')
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can repost a reply post', async () => {
|
||||
const post = by.id('postThreadItem-by-carla.test')
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(post)).atIndex(0),
|
||||
).toHaveText('1')
|
||||
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await expect(element(by.id('repostModal'))).toBeVisible()
|
||||
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
|
||||
await expect(element(by.id('repostModal'))).not.toBeVisible()
|
||||
await expect(
|
||||
element(by.id('repostCount').withAncestor(post)).atIndex(0),
|
||||
).not.toExist()
|
||||
})
|
||||
|
||||
it('Can report the root post', async () => {
|
||||
const post = by.id('postThreadItem-by-bob.test')
|
||||
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.text('Report post')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('Can report a reply post', async () => {
|
||||
const post = by.id('postThreadItem-by-carla.test')
|
||||
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
|
||||
await element(by.text('Report post')).tap()
|
||||
await expect(element(by.id('reportModal'))).toBeVisible()
|
||||
await element(
|
||||
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
|
||||
).tap()
|
||||
await element(by.id('sendReportBtn')).tap()
|
||||
await expect(element(by.id('reportModal'))).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import {resolveConfig} from 'detox/internals'
|
||||
import {execSync} from 'child_process'
|
||||
import http from 'http'
|
||||
|
||||
const platform = device.getPlatform()
|
||||
|
||||
export async function openApp(opts: any) {
|
||||
opts = opts || {}
|
||||
const config = await resolveConfig()
|
||||
|
||||
if (device.getPlatform() === 'ios') {
|
||||
// disable password autofill
|
||||
execSync(
|
||||
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/UserSettings.plist`,
|
||||
)
|
||||
execSync(
|
||||
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/EffectiveUserSettings.plist`,
|
||||
)
|
||||
execSync(
|
||||
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist`,
|
||||
)
|
||||
}
|
||||
if (config.configurationName.split('.').includes('debug')) {
|
||||
return await openAppForDebugBuild(platform, opts)
|
||||
} else {
|
||||
return await device.launchApp({
|
||||
...opts,
|
||||
newInstance: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function isVisible(id: string) {
|
||||
try {
|
||||
await expect(element(by.id(id))).toBeVisible()
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function login(
|
||||
service: string,
|
||||
username: string,
|
||||
password: string,
|
||||
{takeScreenshots} = {takeScreenshots: false},
|
||||
) {
|
||||
await element(by.id('signInButton')).tap()
|
||||
if (takeScreenshots) {
|
||||
await device.takeScreenshot('1- opened sign-in screen')
|
||||
}
|
||||
if (await isVisible('chooseAccountForm')) {
|
||||
await element(by.id('chooseNewAccountBtn')).tap()
|
||||
}
|
||||
await element(by.id('loginSelectServiceButton')).tap()
|
||||
if (takeScreenshots) {
|
||||
await device.takeScreenshot('2- opened service selector')
|
||||
}
|
||||
await element(by.id('customSelectBtn')).tap()
|
||||
await element(by.id('customServerTextInput')).typeText(service)
|
||||
await element(by.id('customServerTextInput')).tapReturnKey()
|
||||
await element(by.id('doneBtn')).tap()
|
||||
if (takeScreenshots) {
|
||||
await device.takeScreenshot('3- input custom service')
|
||||
}
|
||||
await element(by.id('loginUsernameInput')).typeText(username)
|
||||
await element(by.id('loginPasswordInput')).typeText(password)
|
||||
if (takeScreenshots) {
|
||||
await device.takeScreenshot('4- entered username and password')
|
||||
}
|
||||
await element(by.id('loginNextButton')).tap()
|
||||
}
|
||||
|
||||
export async function loginAsAlice() {
|
||||
await element(by.id('e2eSignInAlice')).tap()
|
||||
}
|
||||
|
||||
export async function loginAsBob() {
|
||||
await element(by.id('e2eSignInBob')).tap()
|
||||
}
|
||||
|
||||
async function openAppForDebugBuild(platform: string, opts: any) {
|
||||
const deepLinkUrl = // Local testing with packager
|
||||
/*process.env.EXPO_USE_UPDATES
|
||||
? // Testing latest published EAS update for the test_debug channel
|
||||
getDeepLinkUrl(getLatestUpdateUrl())
|
||||
: */ getDeepLinkUrl(getDevLauncherPackagerUrl(platform))
|
||||
|
||||
if (platform === 'ios') {
|
||||
await device.launchApp({
|
||||
...opts,
|
||||
newInstance: true,
|
||||
})
|
||||
sleep(3000)
|
||||
await device.openURL({
|
||||
url: deepLinkUrl,
|
||||
})
|
||||
} else {
|
||||
await device.launchApp({
|
||||
...opts,
|
||||
newInstance: true,
|
||||
url: deepLinkUrl,
|
||||
})
|
||||
}
|
||||
|
||||
await sleep(3000)
|
||||
}
|
||||
|
||||
export async function createServer(path = ''): Promise<string> {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var req = http.request(
|
||||
{
|
||||
method: 'POST',
|
||||
host: 'localhost',
|
||||
port: 1986,
|
||||
path: `/${path}`,
|
||||
},
|
||||
function (res) {
|
||||
const body: Buffer[] = []
|
||||
res.on('data', chunk => body.push(chunk))
|
||||
res.on('end', function () {
|
||||
try {
|
||||
resolve(Buffer.concat(body).toString())
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
req.on('error', reject)
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
const getDeepLinkUrl = (url: string) =>
|
||||
`expo+bluesky://expo-development-client/?url=${encodeURIComponent(url)}`
|
||||
|
||||
const getDevLauncherPackagerUrl = (platform: string) =>
|
||||
`http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1`
|
||||
|
||||
export const sleep = (t: number) => new Promise(res => setTimeout(res, t))
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, {type ReactNode} from 'react'
|
||||
import {FlatList, Modal, ScrollView, TextInput, View} from 'react-native'
|
||||
import React, {ReactNode} from 'react'
|
||||
import {View, ScrollView, Modal, FlatList, TextInput} from 'react-native'
|
||||
|
||||
const BottomSheetModalContext = React.createContext(null)
|
||||
BottomSheetModalContext.displayName = 'BottomSheetModalContext'
|
||||
|
||||
const BottomSheetModalProvider = (props: any) => {
|
||||
return <BottomSheetModalContext.Provider {...props} value={{}} />
|
||||
@@ -48,13 +47,13 @@ export {useBottomSheetInternal}
|
||||
export {useBottomSheetDynamicSnapPoints}
|
||||
|
||||
export {
|
||||
BottomSheetModalProvider,
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetFlatList,
|
||||
BottomSheetFooter,
|
||||
BottomSheetHandle,
|
||||
BottomSheetModal,
|
||||
BottomSheetModalProvider,
|
||||
BottomSheetFooter,
|
||||
BottomSheetScrollView,
|
||||
BottomSheetFlatList,
|
||||
BottomSheetTextInput,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export const CID = jest.fn().mockImplementation(() => {
|
||||
return {}
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
export const from = jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
digest: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve('')
|
||||
}),
|
||||
}
|
||||
})
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export const openPicker = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({uri: ''}))
|
||||
export const openCamera = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({uri: ''}))
|
||||
export const openCropper = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({uri: ''}))
|
||||
@@ -0,0 +1,47 @@
|
||||
export const exampleComHtml = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Example Domain</title>
|
||||
<meta name="description" content="An example website">
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #f0f0f2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
|
||||
}
|
||||
div {
|
||||
width: 600px;
|
||||
margin: 5em auto;
|
||||
padding: 2em;
|
||||
background-color: #fdfdff;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
|
||||
}
|
||||
a:link, a:visited {
|
||||
color: #38488f;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
div {
|
||||
margin: 0 auto;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h1>Example Domain</h1>
|
||||
<p>This domain is for use in illustrative examples in documents. You may use this
|
||||
domain in literature without prior coordination or asking for permission.</p>
|
||||
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,82 +0,0 @@
|
||||
import {describe, expect, it} from '@jest/globals'
|
||||
import tldts from 'tldts'
|
||||
|
||||
import {isEmailMaybeInvalid} from '#/lib/strings/email'
|
||||
|
||||
describe('emailTypoChecker', () => {
|
||||
const invalidCases = [
|
||||
'gnail.com',
|
||||
'gnail.co',
|
||||
'gmaill.com',
|
||||
'gmaill.co',
|
||||
'gmai.com',
|
||||
'gmai.co',
|
||||
'gmal.com',
|
||||
'gmal.co',
|
||||
'gmail.co',
|
||||
'iclod.com',
|
||||
'iclod.co',
|
||||
'outllok.com',
|
||||
'outllok.co',
|
||||
'outlook.co',
|
||||
'yaoo.com',
|
||||
'yaoo.co',
|
||||
'yaho.com',
|
||||
'yaho.co',
|
||||
'yahooo.com',
|
||||
'yahooo.co',
|
||||
'yahoo.co',
|
||||
'hithere.jul',
|
||||
'agpowj.notshop',
|
||||
'thisisnot.avalid.tld.nope',
|
||||
// old tld for czechoslovakia
|
||||
'czechoslovakia.cs',
|
||||
// tlds that cbs was registering in 2024 but cancelled
|
||||
'liveon.cbs',
|
||||
'its.showtime',
|
||||
]
|
||||
const validCases = [
|
||||
'gmail.com',
|
||||
// subdomains (tests end of string)
|
||||
'gnail.com.test.com',
|
||||
'outlook.com',
|
||||
'yahoo.com',
|
||||
'icloud.com',
|
||||
'firefox.com',
|
||||
'firefox.co',
|
||||
'hello.world.com',
|
||||
'buy.me.a.coffee.shop',
|
||||
'mayotte.yt',
|
||||
'aland.ax',
|
||||
'bouvet.bv',
|
||||
'uk.gb',
|
||||
'chad.td',
|
||||
'somalia.so',
|
||||
'plane.aero',
|
||||
'cute.cat',
|
||||
'together.coop',
|
||||
'findme.jobs',
|
||||
'nightatthe.museum',
|
||||
'industrial.mil',
|
||||
'czechrepublic.cz',
|
||||
'lovakia.sk',
|
||||
// new gtlds in 2024
|
||||
'whatsinyour.locker',
|
||||
'letsmakea.deal',
|
||||
'skeet.now',
|
||||
'everyone.みんな',
|
||||
'bourgeois.lifestyle',
|
||||
'california.living',
|
||||
'skeet.ing',
|
||||
'listeningto.music',
|
||||
'createa.meme',
|
||||
]
|
||||
|
||||
it.each(invalidCases)(`should be invalid: abcde@%s`, domain => {
|
||||
expect(isEmailMaybeInvalid(`abcde@${domain}`, tldts)).toEqual(true)
|
||||
})
|
||||
|
||||
it.each(validCases)(`should be valid: abcde@%s`, domain => {
|
||||
expect(isEmailMaybeInvalid(`abcde@${domain}`, tldts)).toEqual(false)
|
||||
})
|
||||
})
|
||||
@@ -9,11 +9,11 @@ describe('isNetworkError', () => {
|
||||
]
|
||||
const outputs = [true, false, false, true]
|
||||
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const output = outputs[i]
|
||||
it(`correctly distinguishes network errors for ${input}`, () => {
|
||||
expect(isNetworkError(input)).toEqual(output)
|
||||
})
|
||||
}
|
||||
it('correctly distinguishes network errors', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const result = isNetworkError(input)
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import {extractHtmlMeta} from '../../src/lib/link-meta/html'
|
||||
import {exampleComHtml} from './__mocks__/exampleComHtml'
|
||||
import {youtubeHTML} from './__mocks__/youtubeHtml'
|
||||
import {tiktokHtml} from './__mocks__/tiktokHtml'
|
||||
import {youtubeChannelHtml} from './__mocks__/youtubeChannelHtml'
|
||||
|
||||
describe('extractHtmlMeta', () => {
|
||||
const cases = [
|
||||
['', {}],
|
||||
['nothing', {}],
|
||||
['<title>title</title>', {title: 'title'}],
|
||||
['<title> aSd!@#AC </title>', {title: 'aSd!@#AC'}],
|
||||
['<title>\n title\n </title>', {title: 'title'}],
|
||||
['<meta name="title" content="meta title">', {title: 'meta title'}],
|
||||
[
|
||||
'<meta name="description" content="meta description">',
|
||||
{description: 'meta description'},
|
||||
],
|
||||
['<meta property="og:title" content="og title">', {title: 'og title'}],
|
||||
[
|
||||
'<meta property="og:description" content="og description">',
|
||||
{description: 'og description'},
|
||||
],
|
||||
[
|
||||
'<meta property="og:image" content="https://ogimage.com/foo.png">',
|
||||
{image: 'https://ogimage.com/foo.png'},
|
||||
],
|
||||
[
|
||||
'<meta property="twitter:title" content="twitter title">',
|
||||
{title: 'twitter title'},
|
||||
],
|
||||
[
|
||||
'<meta property="twitter:description" content="twitter description">',
|
||||
{description: 'twitter description'},
|
||||
],
|
||||
[
|
||||
'<meta property="twitter:image" content="https://twitterimage.com/foo.png">',
|
||||
{image: 'https://twitterimage.com/foo.png'},
|
||||
],
|
||||
['<meta\n name="title"\n content="meta title"\n>', {title: 'meta title'}],
|
||||
]
|
||||
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
it('extracts title and description from a generic HTML page', () => {
|
||||
const input = exampleComHtml
|
||||
const expectedOutput = {
|
||||
title: 'Example Domain',
|
||||
description: 'An example website',
|
||||
}
|
||||
const output = extractHtmlMeta({html: input, hostname: 'example.com'})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('extracts title and description from a Tiktok HTML page', () => {
|
||||
const input = tiktokHtml
|
||||
const expectedOutput = {
|
||||
title:
|
||||
'Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #bal... | TikTok',
|
||||
description:
|
||||
'5.5M Likes, 20.8K Comments. TikTok video from Power Vision Tests (@_powervision_): "Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #balloon #sciencemoment #scienceexperiment #experiment #test #amazing #pvexp". оригинальный звук - Power Vision Tests.',
|
||||
}
|
||||
const output = extractHtmlMeta({html: input, hostname: 'tiktok.com'})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('extracts title and description from a generic youtube page', () => {
|
||||
const input = youtubeHTML
|
||||
const expectedOutput = {
|
||||
title: 'HD Video (1080p) with Relaxing Music of Native American Shamans',
|
||||
description:
|
||||
'Stunning HD Video ( 1080p ) of Patagonian Nature with Relaxing Native American Shamanic Music. HD footage used from ',
|
||||
image: 'https://i.ytimg.com/vi/x6UITRjhijI/sddefault.jpg',
|
||||
}
|
||||
const output = extractHtmlMeta({html: input, hostname: 'youtube.com'})
|
||||
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',
|
||||
})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('extracts username from the url a tweet', () => {
|
||||
const expectedOutput = {
|
||||
title: 'Tweet by @bluesky',
|
||||
}
|
||||
const output = extractHtmlMeta({
|
||||
html: '',
|
||||
hostname: 'twitter.com',
|
||||
pathname: '/bluesky/status/1582437529969917953',
|
||||
})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it("does not extract username from the url when it's not a tweet or profile page", () => {
|
||||
const expectedOutput = {
|
||||
title: 'Twitter',
|
||||
}
|
||||
const output = extractHtmlMeta({
|
||||
html: '',
|
||||
hostname: 'twitter.com',
|
||||
pathname: '/i/articles/follows/-1675653703?time_window=24',
|
||||
})
|
||||
expect(output).toEqual(expectedOutput)
|
||||
})
|
||||
})
|
||||
@@ -1,29 +1,25 @@
|
||||
import {createDownloadResumable, deleteAsync} from 'expo-file-system'
|
||||
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
|
||||
import {
|
||||
downloadAndResize,
|
||||
type DownloadAndResizeOpts,
|
||||
getResizedDimensions,
|
||||
DownloadAndResizeOpts,
|
||||
} from '../../src/lib/media/manip'
|
||||
|
||||
const mockResizedImage = {
|
||||
path: 'file://resized-image.jpg',
|
||||
size: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
mime: 'image/jpeg',
|
||||
}
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
describe('downloadAndResize', () => {
|
||||
const errorSpy = jest.spyOn(global.console, 'error')
|
||||
|
||||
const mockResizedImage = {
|
||||
path: jest.fn().mockReturnValue('file://resized-image.jpg'),
|
||||
size: 100,
|
||||
width: 50,
|
||||
height: 50,
|
||||
mime: 'image/jpeg',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const mockedCreateResizedImage = manipulateAsync as jest.Mock
|
||||
mockedCreateResizedImage.mockResolvedValue({
|
||||
uri: 'file://resized-image.jpg',
|
||||
...mockResizedImage,
|
||||
})
|
||||
const mockedCreateResizedImage =
|
||||
ImageResizer.createResizedImage as jest.Mock
|
||||
mockedCreateResizedImage.mockResolvedValue(mockResizedImage)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -31,12 +27,10 @@ describe('downloadAndResize', () => {
|
||||
})
|
||||
|
||||
it('should return resized image for valid URI and options', async () => {
|
||||
const mockedFetch = createDownloadResumable as jest.Mock
|
||||
mockedFetch.mockReturnValue({
|
||||
cancelAsync: jest.fn(),
|
||||
downloadAsync: jest
|
||||
.fn()
|
||||
.mockResolvedValue({uri: 'file://resized-image.jpg'}),
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
@@ -50,24 +44,25 @@ describe('downloadAndResize', () => {
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(result).toEqual(mockResizedImage)
|
||||
expect(createDownloadResumable).toHaveBeenCalledWith(
|
||||
opts.uri,
|
||||
expect.anything(),
|
||||
{
|
||||
cache: true,
|
||||
},
|
||||
)
|
||||
|
||||
// First time it gets called is to get dimensions
|
||||
expect(manipulateAsync).toHaveBeenCalledWith(expect.any(String), [], {})
|
||||
expect(manipulateAsync).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[{resize: {height: opts.height, width: opts.width}}],
|
||||
{format: SaveFormat.JPEG, compress: 1.0},
|
||||
)
|
||||
expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), {
|
||||
idempotent: true,
|
||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
||||
fileCache: true,
|
||||
appendExt: 'jpeg',
|
||||
})
|
||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'https://example.com/image.jpg',
|
||||
)
|
||||
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
|
||||
'file://downloaded-image.jpg',
|
||||
100,
|
||||
100,
|
||||
'JPEG',
|
||||
100,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{mode: 'cover'},
|
||||
)
|
||||
})
|
||||
|
||||
it('should return undefined for invalid URI', async () => {
|
||||
@@ -85,43 +80,42 @@ describe('downloadAndResize', () => {
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not downsize whenever dimensions are below the max dimensions', () => {
|
||||
const initialDimensionsOne = {
|
||||
width: 1200,
|
||||
height: 1000,
|
||||
}
|
||||
const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
|
||||
|
||||
const initialDimensionsTwo = {
|
||||
width: 1000,
|
||||
height: 1200,
|
||||
}
|
||||
const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
|
||||
|
||||
expect(resizedDimensionsOne).toEqual(initialDimensionsOne)
|
||||
expect(resizedDimensionsTwo).toEqual(initialDimensionsTwo)
|
||||
})
|
||||
|
||||
it('should resize dimensions and maintain aspect ratio if they are above the max dimensons', () => {
|
||||
const initialDimensionsOne = {
|
||||
width: 3000,
|
||||
height: 1500,
|
||||
}
|
||||
const resizedDimensionsOne = getResizedDimensions(initialDimensionsOne)
|
||||
|
||||
const initialDimensionsTwo = {
|
||||
width: 2000,
|
||||
height: 4000,
|
||||
}
|
||||
const resizedDimensionsTwo = getResizedDimensions(initialDimensionsTwo)
|
||||
|
||||
expect(resizedDimensionsOne).toEqual({
|
||||
width: 2000,
|
||||
height: 1000,
|
||||
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'),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
expect(resizedDimensionsTwo).toEqual({
|
||||
width: 1000,
|
||||
height: 2000,
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
uri: 'https://example.com/image',
|
||||
width: 100,
|
||||
height: 100,
|
||||
maxSize: 500000,
|
||||
mode: 'cover',
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
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'},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
+92
-349
@@ -1,23 +1,17 @@
|
||||
import {RichText} from '@atproto/api'
|
||||
|
||||
import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
|
||||
import {
|
||||
createStarterPackGooglePlayUri,
|
||||
createStarterPackLinkFromAndroidReferrer,
|
||||
parseStarterPackUri,
|
||||
} from '#/lib/strings/starter-pack'
|
||||
import {tenorUrlToBskyGifUrl} from '#/state/queries/tenor'
|
||||
import {cleanError} from '../../src/lib/strings/errors'
|
||||
import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
|
||||
import {enforceLen} from '../../src/lib/strings/helpers'
|
||||
import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
|
||||
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
|
||||
import {
|
||||
makeRecordUri,
|
||||
toNiceDomain,
|
||||
toShareUrl,
|
||||
toShortUrl,
|
||||
toShareUrl,
|
||||
} from '../../src/lib/strings/url-helpers'
|
||||
import {pluralize, enforceLen} from '../../src/lib/strings/helpers'
|
||||
import {ago} from '../../src/lib/strings/time'
|
||||
import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
|
||||
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
|
||||
import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
|
||||
import {cleanError} from '../../src/lib/strings/errors'
|
||||
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
|
||||
|
||||
describe('detectLinkables', () => {
|
||||
const inputs = [
|
||||
@@ -132,6 +126,35 @@ describe('detectLinkables', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('pluralize', () => {
|
||||
const inputs: [number, string, string?][] = [
|
||||
[1, 'follower'],
|
||||
[1, 'member'],
|
||||
[100, 'post'],
|
||||
[1000, 'repost'],
|
||||
[10000, 'upvote'],
|
||||
[100000, 'other'],
|
||||
[2, 'man', 'men'],
|
||||
]
|
||||
const outputs = [
|
||||
'follower',
|
||||
'member',
|
||||
'posts',
|
||||
'reposts',
|
||||
'upvotes',
|
||||
'others',
|
||||
'men',
|
||||
]
|
||||
|
||||
it('correctly pluralizes a set of words', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const output = pluralize(...input)
|
||||
expect(output).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeRecordUri', () => {
|
||||
const inputs: [string, string, string][] = [
|
||||
['alice.test', 'app.bsky.feed.post', '3jk7x4irgv52r'],
|
||||
@@ -147,6 +170,36 @@ describe('makeRecordUri', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ago', () => {
|
||||
const inputs = [
|
||||
1671461038,
|
||||
'04 Dec 1995 00:12:00 GMT',
|
||||
new Date(),
|
||||
new Date().setSeconds(new Date().getSeconds() - 10),
|
||||
new Date().setMinutes(new Date().getMinutes() - 10),
|
||||
new Date().setHours(new Date().getHours() - 1),
|
||||
new Date().setDate(new Date().getDate() - 1),
|
||||
new Date().setMonth(new Date().getMonth() - 1),
|
||||
]
|
||||
const outputs = [
|
||||
new Date(1671461038).toLocaleDateString(),
|
||||
new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString(),
|
||||
'now',
|
||||
'10s',
|
||||
'10m',
|
||||
'1h',
|
||||
'1d',
|
||||
'1mo',
|
||||
]
|
||||
|
||||
it('correctly calculates how much time passed, in a string', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const result = ago(inputs[i])
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeValidHandle', () => {
|
||||
const inputs = [
|
||||
'test-handle-123',
|
||||
@@ -336,21 +389,16 @@ describe('shortenLinks', () => {
|
||||
describe('parseEmbedPlayerFromUrl', () => {
|
||||
const inputs = [
|
||||
'https://youtu.be/videoId',
|
||||
'https://youtu.be/videoId?t=1s',
|
||||
'https://www.youtube.com/watch?v=videoId',
|
||||
'https://www.youtube.com/watch?v=videoId&feature=share',
|
||||
'https://www.youtube.com/watch?v=videoId&t=1s',
|
||||
'https://youtube.com/watch?v=videoId',
|
||||
'https://youtube.com/watch?v=videoId&feature=share',
|
||||
'https://youtube.com/shorts/videoId',
|
||||
'https://youtube.com/live/videoId',
|
||||
'https://m.youtube.com/watch?v=videoId',
|
||||
'https://music.youtube.com/watch?v=videoId',
|
||||
|
||||
'https://youtube.com/shorts/',
|
||||
'https://youtube.com/',
|
||||
'https://youtube.com/random',
|
||||
'https://youtube.com/live/',
|
||||
|
||||
'https://twitch.tv/channelName',
|
||||
'https://www.twitch.tv/channelName',
|
||||
@@ -386,8 +434,6 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
'https://giphy.com/gif/some-random-gif-name-gifId',
|
||||
'https://giphy.com/gifs/',
|
||||
|
||||
'https://giphy.com/gifs/39248209509382934029?hh=100&ww=100',
|
||||
|
||||
'https://media.giphy.com/media/gifId/giphy.webp',
|
||||
'https://media0.giphy.com/media/gifId/giphy.webp',
|
||||
'https://media1.giphy.com/media/gifId/giphy.gif',
|
||||
@@ -410,32 +456,6 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
'https://tenor.com/view',
|
||||
'https://tenor.com/view/gifId.gif',
|
||||
'https://tenor.com/intl/view/gifId.gif',
|
||||
|
||||
'https://media.tenor.com/someID_AAAAC/someName.gif?hh=100&ww=100',
|
||||
'https://media.tenor.com/someID_AAAAC/someName.gif',
|
||||
'https://media.tenor.com/someID/someName.gif',
|
||||
'https://media.tenor.com/someID',
|
||||
'https://media.tenor.com',
|
||||
|
||||
'https://www.flickr.com/photos/username/albums/72177720308493661',
|
||||
'https://flickr.com/photos/username/albums/72177720308493661',
|
||||
'https://flickr.com/photos/username/albums/72177720308493661/',
|
||||
'https://flickr.com/photos/username/albums/72177720308493661//',
|
||||
'https://flic.kr/s/aHBqjAES3i',
|
||||
|
||||
'https://flickr.com/foetoes/username/albums/3903',
|
||||
'https://flickr.com/albums/3903',
|
||||
'https://flic.kr/s/OolI',
|
||||
'https://flic.kr/t/aHBqjAES3i',
|
||||
|
||||
'https://www.flickr.com/groups/898944@N23/pool',
|
||||
'https://flickr.com/groups/898944@N23/pool',
|
||||
'https://flickr.com/groups/898944@N23/pool/',
|
||||
'https://flickr.com/groups/898944@N23/pool//',
|
||||
'https://flic.kr/go/8WJtR',
|
||||
|
||||
'https://www.flickr.com/groups/898944@N23/',
|
||||
'https://www.flickr.com/groups',
|
||||
]
|
||||
|
||||
const outputs = [
|
||||
@@ -444,11 +464,6 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
|
||||
},
|
||||
{
|
||||
type: 'youtube_video',
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=1',
|
||||
},
|
||||
{
|
||||
type: 'youtube_video',
|
||||
source: 'youtube',
|
||||
@@ -459,11 +474,6 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
|
||||
},
|
||||
{
|
||||
type: 'youtube_video',
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=1',
|
||||
},
|
||||
{
|
||||
type: 'youtube_video',
|
||||
source: 'youtube',
|
||||
@@ -485,21 +495,10 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
|
||||
},
|
||||
{
|
||||
type: 'youtube_video',
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
|
||||
},
|
||||
{
|
||||
type: 'youtube_video',
|
||||
source: 'youtube',
|
||||
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'twitch_video',
|
||||
@@ -622,17 +621,18 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
source: 'giphy',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/39248209509382934029',
|
||||
playerUri: 'https://i.giphy.com/media/39248209509382934029/200.webp',
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -640,7 +640,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -648,7 +648,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -656,7 +656,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -664,7 +664,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -672,15 +672,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
source: 'giphy',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
@@ -692,7 +684,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
|
||||
{
|
||||
@@ -701,7 +693,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -709,7 +701,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -717,7 +709,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
{
|
||||
type: 'giphy_gif',
|
||||
@@ -725,90 +717,32 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
metaUri: 'https://giphy.com/gifs/gifId',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/200.webp',
|
||||
playerUri: 'https://i.giphy.com/media/gifId/giphy.webp',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'tenor_gif',
|
||||
source: 'tenor',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
playerUri: 'https://t.gifs.bsky.app/someID_AAAAM/someName.gif',
|
||||
dimensions: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
playerUri: 'https://tenor.com/view/gifId.gif',
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
type: 'tenor_gif',
|
||||
source: 'tenor',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
playerUri: 'https://tenor.com/view/gifId.gif',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
type: 'tenor_gif',
|
||||
source: 'tenor',
|
||||
isGif: true,
|
||||
hideDetails: true,
|
||||
playerUri: 'https://tenor.com/intl/view/gifId.gif',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
{
|
||||
type: 'flickr_album',
|
||||
source: 'flickr',
|
||||
playerUri: 'https://embedr.flickr.com/groups/898944@N23',
|
||||
},
|
||||
|
||||
undefined,
|
||||
undefined,
|
||||
]
|
||||
|
||||
it('correctly grabs the correct id from uri', () => {
|
||||
@@ -822,194 +756,3 @@ describe('parseEmbedPlayerFromUrl', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('createStarterPackLinkFromAndroidReferrer', () => {
|
||||
const validOutput = 'at://haileyok.com/app.bsky.graph.starterpack/rkey'
|
||||
|
||||
it('returns a link when input contains utm_source and utm_content', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_source=bluesky&utm_content=starterpack_haileyok.com_rkey',
|
||||
),
|
||||
).toEqual(validOutput)
|
||||
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_source=bluesky&utm_content=starterpack_test-lover-9000.com_rkey',
|
||||
),
|
||||
).toEqual('at://test-lover-9000.com/app.bsky.graph.starterpack/rkey')
|
||||
})
|
||||
|
||||
it('returns a link when input contains utm_source and utm_content in different order', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_content=starterpack_haileyok.com_rkey&utm_source=bluesky',
|
||||
),
|
||||
).toEqual(validOutput)
|
||||
})
|
||||
|
||||
it('returns a link when input contains other parameters as well', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_source=bluesky&utm_medium=starterpack&utm_content=starterpack_haileyok.com_rkey',
|
||||
),
|
||||
).toEqual(validOutput)
|
||||
})
|
||||
|
||||
it('returns null when utm_source is not present', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_content=starterpack_haileyok.com_rkey',
|
||||
),
|
||||
).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when utm_content is not present', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer('utm_source=bluesky'),
|
||||
).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when utm_content is malformed', () => {
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_content=starterpack_haileyok.com',
|
||||
),
|
||||
).toEqual(null)
|
||||
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer('utm_content=starterpack'),
|
||||
).toEqual(null)
|
||||
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_content=starterpack_haileyok.com_rkey_more',
|
||||
),
|
||||
).toEqual(null)
|
||||
|
||||
expect(
|
||||
createStarterPackLinkFromAndroidReferrer(
|
||||
'utm_content=notastarterpack_haileyok.com_rkey',
|
||||
),
|
||||
).toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseStarterPackHttpUri', () => {
|
||||
const baseUri = 'https://bsky.app/start'
|
||||
|
||||
it('returns a valid at uri when http uri is valid', () => {
|
||||
const validHttpUri = `${baseUri}/haileyok.com/rkey`
|
||||
expect(parseStarterPackUri(validHttpUri)).toEqual({
|
||||
name: 'haileyok.com',
|
||||
rkey: 'rkey',
|
||||
})
|
||||
|
||||
const validHttpUri2 = `${baseUri}/haileyok.com/ilovetesting`
|
||||
expect(parseStarterPackUri(validHttpUri2)).toEqual({
|
||||
name: 'haileyok.com',
|
||||
rkey: 'ilovetesting',
|
||||
})
|
||||
|
||||
const validHttpUri3 = `${baseUri}/testlover9000.com/rkey`
|
||||
expect(parseStarterPackUri(validHttpUri3)).toEqual({
|
||||
name: 'testlover9000.com',
|
||||
rkey: 'rkey',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null when there is no rkey', () => {
|
||||
const validHttpUri = `${baseUri}/haileyok.com`
|
||||
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when there is an extra path', () => {
|
||||
const validHttpUri = `${baseUri}/haileyok.com/rkey/other`
|
||||
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when there is no handle or rkey', () => {
|
||||
const validHttpUri = `${baseUri}`
|
||||
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when the route is not /start or /starter-pack', () => {
|
||||
const validHttpUri = 'https://bsky.app/start/haileyok.com/rkey'
|
||||
expect(parseStarterPackUri(validHttpUri)).toEqual({
|
||||
name: 'haileyok.com',
|
||||
rkey: 'rkey',
|
||||
})
|
||||
|
||||
const validHttpUri2 = 'https://bsky.app/starter-pack/haileyok.com/rkey'
|
||||
expect(parseStarterPackUri(validHttpUri2)).toEqual({
|
||||
name: 'haileyok.com',
|
||||
rkey: 'rkey',
|
||||
})
|
||||
|
||||
const invalidHttpUri = 'https://bsky.app/profile/haileyok.com/rkey'
|
||||
expect(parseStarterPackUri(invalidHttpUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns the at uri when the input is a valid starterpack at uri', () => {
|
||||
const validAtUri = 'at://did:123/app.bsky.graph.starterpack/rkey'
|
||||
expect(parseStarterPackUri(validAtUri)).toEqual({
|
||||
name: 'did:123',
|
||||
rkey: 'rkey',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null when the at uri has no rkey', () => {
|
||||
const validAtUri = 'at://did:123/app.bsky.graph.starterpack'
|
||||
expect(parseStarterPackUri(validAtUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when the collection is not app.bsky.graph.starterpack', () => {
|
||||
const validAtUri = 'at://did:123/app.bsky.graph.list/rkey'
|
||||
expect(parseStarterPackUri(validAtUri)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when the input is undefined', () => {
|
||||
expect(parseStarterPackUri(undefined)).toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createStarterPackGooglePlayUri', () => {
|
||||
const base =
|
||||
'https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack_'
|
||||
|
||||
it('returns valid google play uri when input is valid', () => {
|
||||
expect(createStarterPackGooglePlayUri('name', 'rkey')).toEqual(
|
||||
`${base}name_rkey`,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns null when no rkey is supplied', () => {
|
||||
// @ts-expect-error test
|
||||
expect(createStarterPackGooglePlayUri('name', undefined)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when no name or rkey are supplied', () => {
|
||||
// @ts-expect-error test
|
||||
expect(createStarterPackGooglePlayUri(undefined, undefined)).toEqual(null)
|
||||
})
|
||||
|
||||
it('returns null when rkey is supplied but no name', () => {
|
||||
// @ts-expect-error test
|
||||
expect(createStarterPackGooglePlayUri(undefined, 'rkey')).toEqual(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('tenorUrlToBskyGifUrl', () => {
|
||||
const inputs = [
|
||||
'https://media.tenor.com/someID_AAAAC/someName.gif',
|
||||
'https://media.tenor.com/someID/someName.gif',
|
||||
]
|
||||
|
||||
it.each(inputs)(
|
||||
'returns url with t.gifs.bsky.app as hostname for input url',
|
||||
input => {
|
||||
const out = tenorUrlToBskyGifUrl(input)
|
||||
expect(out.startsWith('https://t.gifs.bsky.app/')).toEqual(true)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import {type IsValidHandle, validateServiceHandle} from '#/lib/strings/handles'
|
||||
|
||||
describe('handle validation', () => {
|
||||
const valid = [
|
||||
['ali', 'bsky.social'],
|
||||
['alice', 'bsky.social'],
|
||||
['a-lice', 'bsky.social'],
|
||||
['a-----lice', 'bsky.social'],
|
||||
['123', 'bsky.social'],
|
||||
['123456789012345678', 'bsky.social'],
|
||||
['alice', 'custom-pds.com'],
|
||||
['alice', 'my-custom-pds-with-long-name.social'],
|
||||
['123456789012345678', 'my-custom-pds-with-long-name.social'],
|
||||
]
|
||||
it.each(valid)(`should be valid: %s.%s`, (handle, service) => {
|
||||
const result = validateServiceHandle(handle, service)
|
||||
expect(result.overall).toEqual(true)
|
||||
})
|
||||
|
||||
const invalid = [
|
||||
['al', 'bsky.social', 'frontLengthNotTooShort'],
|
||||
['-alice', 'bsky.social', 'hyphenStartOrEnd'],
|
||||
['alice-', 'bsky.social', 'hyphenStartOrEnd'],
|
||||
['%%%', 'bsky.social', 'handleChars'],
|
||||
['1234567890123456789', 'bsky.social', 'frontLengthNotTooLong'],
|
||||
[
|
||||
'1234567890123456789',
|
||||
'my-custom-pds-with-long-name.social',
|
||||
'frontLengthNotTooLong',
|
||||
],
|
||||
['al', 'my-custom-pds-with-long-name.social', 'frontLengthNotTooShort'],
|
||||
['a'.repeat(300), 'toolong.com', 'totalLength'],
|
||||
] satisfies [string, string, keyof IsValidHandle][]
|
||||
it.each(invalid)(
|
||||
`should be invalid: %s.%s due to %s`,
|
||||
(handle, service, expectedError) => {
|
||||
const result = validateServiceHandle(handle, service)
|
||||
expect(result.overall).toEqual(false)
|
||||
expect(result[expectedError]).toEqual(false)
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -32,7 +32,6 @@ describe('getMentionAt', () => {
|
||||
['@alice hello', 7, undefined],
|
||||
['alice@alice', 0, undefined],
|
||||
['alice@alice', 6, undefined],
|
||||
['hello @alice-com goodbye', 8, 'alice-com'],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
@@ -73,7 +72,6 @@ describe('insertMentionAt', () => {
|
||||
['@alice hello', 7, '@alice hello'],
|
||||
['alice@alice', 0, 'alice@alice'],
|
||||
['alice@alice', 6, 'alice@alice'],
|
||||
['hello @alice-com goodbye', 10, 'hello @alice.com goodbye'],
|
||||
]
|
||||
|
||||
it.each(cases)(
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {describe, expect, it} from '@jest/globals'
|
||||
import {it, describe, expect} from '@jest/globals'
|
||||
|
||||
import {
|
||||
isPossiblyAUrl,
|
||||
isTrustedUrl,
|
||||
linkRequiresWarning,
|
||||
isPossiblyAUrl,
|
||||
splitApexDomain,
|
||||
} from '../../../src/lib/strings/url-helpers'
|
||||
|
||||
@@ -75,10 +74,6 @@ describe('linkRequiresWarning', () => {
|
||||
// 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)(
|
||||
@@ -144,37 +139,3 @@ describe('splitApexDomain', () => {
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
+78
-295
@@ -1,11 +1,34 @@
|
||||
const pkg = require('./package.json')
|
||||
|
||||
module.exports = function (_config) {
|
||||
const SPLASH_CONFIG = {
|
||||
backgroundColor: '#ffffff',
|
||||
image: './assets/splash.png',
|
||||
resizeMode: 'cover',
|
||||
}
|
||||
const DARK_SPLASH_CONFIG = {
|
||||
backgroundColor: '#001429',
|
||||
image: './assets/splash-dark.png',
|
||||
resizeMode: 'cover',
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
/**
|
||||
* App version number. Should be incremented as part of a release cycle.
|
||||
*/
|
||||
const VERSION = pkg.version
|
||||
|
||||
/**
|
||||
* iOS build number. Must be incremented for each TestFlight version.
|
||||
* WARNING: Always leave this variable on line 24! If it is moved, you need to update ./scripts/bumpIosBuildNumber.sh
|
||||
*/
|
||||
const IOS_BUILD_NUMBER = '7'
|
||||
|
||||
/**
|
||||
* Android build number. Must be incremented for each release.
|
||||
* WARNING: Always leave this variable on line 30! If it is moved, you need to update ./scripts/bumpAndroidBuildNumber.sh
|
||||
*/
|
||||
const ANDROID_VERSION_CODE = 62
|
||||
|
||||
/**
|
||||
* Uses built-in Expo env vars
|
||||
*
|
||||
@@ -13,22 +36,11 @@ module.exports = function (_config) {
|
||||
*/
|
||||
const PLATFORM = process.env.EAS_BUILD_PLATFORM
|
||||
|
||||
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
|
||||
const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production'
|
||||
const IS_DEV = !IS_TESTFLIGHT || !IS_PRODUCTION
|
||||
|
||||
const ASSOCIATED_DOMAINS = [
|
||||
'applinks:bsky.app',
|
||||
'applinks:staging.bsky.app',
|
||||
'appclips:bsky.app',
|
||||
'appclips:go.bsky.app', // Allows App Clip to work when scanning QR codes
|
||||
// When testing local services, enter an ngrok (et al) domain here. It must use a standard HTTP/HTTPS port.
|
||||
...(IS_DEV || IS_TESTFLIGHT ? [] : []),
|
||||
]
|
||||
|
||||
const UPDATES_ENABLED = IS_TESTFLIGHT || IS_PRODUCTION
|
||||
|
||||
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
|
||||
/**
|
||||
* Additional granularity for the `dist` field
|
||||
*/
|
||||
const DIST_BUILD_NUMBER =
|
||||
PLATFORM === 'android' ? ANDROID_VERSION_CODE : IOS_BUILD_NUMBER
|
||||
|
||||
return {
|
||||
expo: {
|
||||
@@ -40,10 +52,12 @@ module.exports = function (_config) {
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
icon: './assets/app-icons/ios_icon_default_light.png',
|
||||
orientation: 'portrait',
|
||||
icon: './assets/icon.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
primaryColor: '#1083fe',
|
||||
splash: SPLASH_CONFIG,
|
||||
ios: {
|
||||
buildNumber: IOS_BUILD_NUMBER,
|
||||
supportsTablet: false,
|
||||
bundleIdentifier: 'xyz.blueskyweb.app',
|
||||
config: {
|
||||
@@ -59,89 +73,20 @@ module.exports = function (_config) {
|
||||
'Used to save images to your library.',
|
||||
NSPhotoLibraryUsageDescription:
|
||||
'Used for profile pictures, posts, and other kinds of content',
|
||||
CFBundleSpokenName: 'Blue Sky',
|
||||
CFBundleLocalizations: [
|
||||
'en',
|
||||
'an',
|
||||
'ast',
|
||||
'ca',
|
||||
'cy',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'eo',
|
||||
'es',
|
||||
'eu',
|
||||
'fi',
|
||||
'fr',
|
||||
'fy',
|
||||
'ga',
|
||||
'gd',
|
||||
'gl',
|
||||
'hi',
|
||||
'hu',
|
||||
'ia',
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'km',
|
||||
'ko',
|
||||
'ne',
|
||||
'nl',
|
||||
'pl',
|
||||
'pt-BR',
|
||||
'pt-PT',
|
||||
'ro',
|
||||
'ru',
|
||||
'sv',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
'yue',
|
||||
'zh-Hans',
|
||||
'zh-Hant',
|
||||
],
|
||||
},
|
||||
associatedDomains: ASSOCIATED_DOMAINS,
|
||||
entitlements: {
|
||||
'com.apple.developer.kernel.increased-memory-limit': true,
|
||||
'com.apple.developer.kernel.extended-virtual-addressing': true,
|
||||
'com.apple.security.application-groups': 'group.app.bsky',
|
||||
},
|
||||
privacyManifests: {
|
||||
NSPrivacyAccessedAPITypes: [
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
'NSPrivacyAccessedAPICategoryFileTimestamp',
|
||||
NSPrivacyAccessedAPITypeReasons: ['C617.1', '3B52.1', '0A2A.1'],
|
||||
},
|
||||
{
|
||||
NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryDiskSpace',
|
||||
NSPrivacyAccessedAPITypeReasons: ['E174.1', '85F4.1'],
|
||||
},
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
'NSPrivacyAccessedAPICategorySystemBootTime',
|
||||
NSPrivacyAccessedAPITypeReasons: ['35F9.1'],
|
||||
},
|
||||
{
|
||||
NSPrivacyAccessedAPIType:
|
||||
'NSPrivacyAccessedAPICategoryUserDefaults',
|
||||
NSPrivacyAccessedAPITypeReasons: ['CA92.1', '1C8F.1'],
|
||||
},
|
||||
],
|
||||
associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'],
|
||||
splash: {
|
||||
...SPLASH_CONFIG,
|
||||
dark: DARK_SPLASH_CONFIG,
|
||||
},
|
||||
},
|
||||
androidStatusBar: {
|
||||
barStyle: 'light-content',
|
||||
},
|
||||
// Dark nav bar in light mode is better than light nav bar in dark mode
|
||||
androidNavigationBar: {
|
||||
barStyle: 'light-content',
|
||||
barStyle: 'dark-content',
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
android: {
|
||||
icon: './assets/app-icons/android_icon_default_light.png',
|
||||
versionCode: ANDROID_VERSION_CODE,
|
||||
icon: './assets/icon.png',
|
||||
adaptiveIcon: {
|
||||
foregroundImage: './assets/icon-android-foreground.png',
|
||||
monochromeImage: './assets/icon-android-foreground.png',
|
||||
@@ -159,238 +104,76 @@ module.exports = function (_config) {
|
||||
scheme: 'https',
|
||||
host: 'bsky.app',
|
||||
},
|
||||
IS_DEV && {
|
||||
scheme: 'http',
|
||||
host: 'localhost:19006',
|
||||
},
|
||||
],
|
||||
category: ['BROWSABLE', 'DEFAULT'],
|
||||
},
|
||||
],
|
||||
splash: {
|
||||
...SPLASH_CONFIG,
|
||||
dark: DARK_SPLASH_CONFIG,
|
||||
},
|
||||
},
|
||||
web: {
|
||||
favicon: './assets/favicon.png',
|
||||
},
|
||||
updates: {
|
||||
url: 'https://updates.bsky.app/manifest',
|
||||
enabled: UPDATES_ENABLED,
|
||||
fallbackToCacheTimeout: 30000,
|
||||
codeSigningCertificate: UPDATES_ENABLED
|
||||
? './code-signing/certificate.pem'
|
||||
: undefined,
|
||||
codeSigningMetadata: UPDATES_ENABLED
|
||||
? {
|
||||
keyid: 'main',
|
||||
alg: 'rsa-v1_5-sha256',
|
||||
}
|
||||
: undefined,
|
||||
checkAutomatically: 'NEVER',
|
||||
enabled: true,
|
||||
fallbackToCacheTimeout: 1000,
|
||||
url: 'https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302',
|
||||
},
|
||||
plugins: [
|
||||
'expo-video',
|
||||
'expo-localization',
|
||||
'expo-web-browser',
|
||||
[
|
||||
'react-native-edge-to-edge',
|
||||
{android: {enforceNavigationBarContrast: false}},
|
||||
],
|
||||
USE_SENTRY && [
|
||||
'@sentry/react-native/expo',
|
||||
{
|
||||
organization: 'blueskyweb',
|
||||
project: 'app',
|
||||
url: 'https://sentry.io',
|
||||
},
|
||||
],
|
||||
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
|
||||
[
|
||||
'expo-build-properties',
|
||||
{
|
||||
ios: {
|
||||
deploymentTarget: '15.1',
|
||||
newArchEnabled: false,
|
||||
deploymentTarget: '13.4',
|
||||
},
|
||||
android: {
|
||||
compileSdkVersion: 35,
|
||||
targetSdkVersion: 35,
|
||||
buildToolsVersion: '35.0.0',
|
||||
newArchEnabled: false,
|
||||
compileSdkVersion: 34,
|
||||
targetSdkVersion: 34,
|
||||
buildToolsVersion: '34.0.0',
|
||||
kotlinVersion: '1.8.0',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'expo-updates',
|
||||
{
|
||||
username: 'blueskysocial',
|
||||
},
|
||||
],
|
||||
[
|
||||
'expo-notifications',
|
||||
{
|
||||
icon: './assets/icon-android-notification.png',
|
||||
color: '#1185fe',
|
||||
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
||||
color: '#ffffff',
|
||||
},
|
||||
],
|
||||
'react-native-compressor',
|
||||
[
|
||||
'@bitdrift/react-native',
|
||||
{
|
||||
networkInstrumentation: true,
|
||||
},
|
||||
],
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withGradleJVMHeapSizeIncrease.js',
|
||||
'./plugins/withAndroidManifestLargeHeapPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidManifestIntentQueriesPlugin.js',
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
'./plugins/withAndroidDayNightThemePlugin.js',
|
||||
'./plugins/withAndroidNoJitpackPlugin.js',
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||
[
|
||||
'expo-font',
|
||||
{
|
||||
fonts: [
|
||||
'./assets/fonts/inter/InterVariable.woff2',
|
||||
'./assets/fonts/inter/InterVariable-Italic.woff2',
|
||||
// Android only
|
||||
'./assets/fonts/inter/Inter-Regular.otf',
|
||||
'./assets/fonts/inter/Inter-Italic.otf',
|
||||
'./assets/fonts/inter/Inter-Medium.otf',
|
||||
'./assets/fonts/inter/Inter-MediumItalic.otf',
|
||||
'./assets/fonts/inter/Inter-SemiBold.otf',
|
||||
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
|
||||
'./assets/fonts/inter/Inter-Bold.otf',
|
||||
'./assets/fonts/inter/Inter-BoldItalic.otf',
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'expo-splash-screen',
|
||||
{
|
||||
ios: {
|
||||
enableFullScreenImage_legacy: true,
|
||||
backgroundColor: '#ffffff',
|
||||
image: './assets/splash.png',
|
||||
resizeMode: 'cover',
|
||||
dark: {
|
||||
enableFullScreenImage_legacy: true,
|
||||
backgroundColor: '#001429',
|
||||
image: './assets/splash-dark.png',
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
},
|
||||
android: {
|
||||
backgroundColor: '#0c7cff',
|
||||
image: './assets/splash-android-icon.png',
|
||||
imageWidth: 150,
|
||||
dark: {
|
||||
backgroundColor: '#0c2a49',
|
||||
image: './assets/splash-android-icon-dark.png',
|
||||
imageWidth: 150,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'@mozzius/expo-dynamic-app-icon',
|
||||
{
|
||||
/**
|
||||
* Default set
|
||||
*/
|
||||
default_light: {
|
||||
ios: './assets/app-icons/ios_icon_default_light.png',
|
||||
android: './assets/app-icons/android_icon_default_light.png',
|
||||
prerendered: true,
|
||||
},
|
||||
default_dark: {
|
||||
ios: './assets/app-icons/ios_icon_default_dark.png',
|
||||
android: './assets/app-icons/android_icon_default_dark.png',
|
||||
prerendered: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Bluesky+ core set
|
||||
*/
|
||||
core_aurora: {
|
||||
ios: './assets/app-icons/ios_icon_core_aurora.png',
|
||||
android: './assets/app-icons/android_icon_core_aurora.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_bonfire: {
|
||||
ios: './assets/app-icons/ios_icon_core_bonfire.png',
|
||||
android: './assets/app-icons/android_icon_core_bonfire.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_sunrise: {
|
||||
ios: './assets/app-icons/ios_icon_core_sunrise.png',
|
||||
android: './assets/app-icons/android_icon_core_sunrise.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_sunset: {
|
||||
ios: './assets/app-icons/ios_icon_core_sunset.png',
|
||||
android: './assets/app-icons/android_icon_core_sunset.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_midnight: {
|
||||
ios: './assets/app-icons/ios_icon_core_midnight.png',
|
||||
android: './assets/app-icons/android_icon_core_midnight.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_flat_blue: {
|
||||
ios: './assets/app-icons/ios_icon_core_flat_blue.png',
|
||||
android: './assets/app-icons/android_icon_core_flat_blue.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_flat_white: {
|
||||
ios: './assets/app-icons/ios_icon_core_flat_white.png',
|
||||
android: './assets/app-icons/android_icon_core_flat_white.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_flat_black: {
|
||||
ios: './assets/app-icons/ios_icon_core_flat_black.png',
|
||||
android: './assets/app-icons/android_icon_core_flat_black.png',
|
||||
prerendered: true,
|
||||
},
|
||||
core_classic: {
|
||||
ios: './assets/app-icons/ios_icon_core_classic.png',
|
||||
android: './assets/app-icons/android_icon_core_classic.png',
|
||||
prerendered: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
['expo-screen-orientation', {initialOrientation: 'PORTRAIT_UP'}],
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
eas: {
|
||||
build: {
|
||||
experimental: {
|
||||
ios: {
|
||||
appExtensions: [
|
||||
{
|
||||
targetName: 'Share-with-Bluesky',
|
||||
bundleIdentifier: 'xyz.blueskyweb.app.Share-with-Bluesky',
|
||||
entitlements: {
|
||||
'com.apple.security.application-groups': [
|
||||
'group.app.bsky',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
targetName: 'BlueskyNSE',
|
||||
bundleIdentifier: 'xyz.blueskyweb.app.BlueskyNSE',
|
||||
entitlements: {
|
||||
'com.apple.security.application-groups': [
|
||||
'group.app.bsky',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
targetName: 'BlueskyClip',
|
||||
bundleIdentifier: 'xyz.blueskyweb.app.AppClip',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
projectId: '55bd077a-d905-4184-9c7f-94789ba0f302',
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
postPublish: [
|
||||
/*
|
||||
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration
|
||||
*/
|
||||
{
|
||||
file: 'sentry-expo/upload-sourcemaps',
|
||||
config: {
|
||||
organization: 'blueskyweb',
|
||||
project: 'react-native',
|
||||
release: VERSION,
|
||||
dist: `${PLATFORM}.${VERSION}.${DIST_BUILD_NUMBER}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 783 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 714 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 947 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user