Compare commits

..

2 Commits

Author SHA1 Message Date
Eric Bailey 7d840faacc Make it safer 2024-06-04 14:19:34 -05:00
Eric Bailey ded615d9f9 Report persisted schema validation failures 2024-06-04 14:17:00 -05:00
1425 changed files with 84874 additions and 258502 deletions
-2
View File
@@ -1,2 +0,0 @@
[*.{kt,kts}]
indent_size=2
+1 -1
View File
@@ -1,7 +1,7 @@
# Copy this to `.env` and `.env.test` files
BITDRIFT_API_KEY=
SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_ENV=development
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG=
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
+8 -31
View File
@@ -1,9 +1,8 @@
module.exports = {
root: true,
extends: [
'@react-native',
'@react-native-community',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-native-a11y/ios',
'prettier',
],
@@ -17,22 +16,14 @@ module.exports = {
'eslint-plugin-react-compiler',
],
rules: {
// Temporary until https://github.com/facebook/react-native/pull/43756 gets into a release.
'prettier/prettier': 0,
'react/no-unescaped-entities': 0,
'react/prop-types': 0,
'react-native/no-inline-styles': 0,
'bsky-internal/avoid-unwrapped-text': [
'error',
{
impliedTextComponents: [
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'P',
'Admonition',
],
impliedTextComponents: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'P'],
impliedTextProps: [],
suggestedTextWrappers: {
Button: 'ButtonText',
@@ -40,11 +31,9 @@ module.exports = {
},
},
],
'bsky-internal/use-exact-imports': 'error',
'bsky-internal/use-typed-gates': 'error',
'bsky-internal/use-prefixed-imports': 'error',
'simple-import-sort/imports': [
'error',
'warn',
{
groups: [
// Side effect imports.
@@ -78,21 +67,9 @@ module.exports = {
],
},
],
'simple-import-sort/exports': 'error',
'react-compiler/react-compiler': 'warn',
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@atproto/api',
importNames: ['moderatePost'],
message:
'Please use `moderatePost_wrapped` from `#/lib/moderatePost_wrapped` instead.',
},
],
},
],
'simple-import-sort/exports': 'warn',
// TODO: Reenable when we figure out why it gets stuck on CI.
// 'react-compiler/react-compiler': 'error',
},
ignorePatterns: [
'**/__mocks__/*.ts',
+35
View File
@@ -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. -->
-64
View File
@@ -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
+24
View File
@@ -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
@@ -3,6 +3,8 @@ on:
push:
branches:
- main
- bnewbold/embedr
- bnewbold/embedr-rebase
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
@@ -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
+2 -9
View File
@@ -34,9 +34,6 @@ jobs:
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:
@@ -99,17 +96,13 @@ jobs:
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 }}```"
"text": "Android build is ready for submission. This is a production build! Download the artifact here: ${{ steps.upload-artifact-production.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
@@ -147,7 +140,7 @@ jobs:
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 }}```"
"text": "Android production APK build is ready for download. This is a production build, and you should add it to the GitHub release! Download the artifact here: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
-19
View File
@@ -34,9 +34,6 @@ jobs:
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:
@@ -84,22 +81,6 @@ jobs:
- 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
+4 -4
View File
@@ -19,9 +19,9 @@ jobs:
- name: Set up Go tooling
uses: actions/setup-go@v3
with:
go-version: '1.22'
go-version: '1.21'
- name: Dummy Static Files
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/media/blah.txt
- 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.22'
go-version: '1.21'
- name: Dummy Static Files
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/css/blah.txt && touch bskyweb/static/media/blah.txt
run: touch bskyweb/static/js/blah.js && touch bskyweb/static/media/blah.txt
- name: Lint
run: cd bskyweb/ && make lint
+1 -5
View File
@@ -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:
@@ -54,4 +50,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
-3
View File
@@ -115,6 +115,3 @@ src/locale/locales/**/*.js
*.apk
*.aab
*.ipa
# ogcard assets
bskyogcard/src/assets/fonts/noto-*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
yarn lint-staged
+1 -1
View File
@@ -1 +1 @@
20
18
+1 -6
View File
@@ -1,4 +1,4 @@
# Ignore everything except JS-ey or CSS code.
# Ignore everything except JS-ey code.
# Based on https://stackoverflow.com/a/70715829/458193
*
!**/*.js
@@ -7,14 +7,9 @@
!**/*.tsx
!*/
!**/*.css
# More specific ignores go below.
.expo
android
ios
src/locale/locales
lib/react-compiler-runtime
bskyweb/static
coverage
web-build
+3 -3
View File
@@ -1,11 +1,11 @@
FROM golang:1.22-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
@@ -17,7 +17,7 @@ ENV GOEXPERIMENT="loopvar"
# Expo
ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev}
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER ${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev}
COPY . .
-41
View File
@@ -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
-41
View File
@@ -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
+2 -3
View File
@@ -1,11 +1,11 @@
FROM golang:1.22-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
@@ -40,7 +40,6 @@ RUN find ./bskyweb/embedr-static && find ./bskyweb/embedr-templates && find ./bs
# 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
#
+1 -1
View File
@@ -20,7 +20,7 @@ build-web-embed: ## Compile web embed bundle, copy to bskyweb/embedr* directorie
.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
+4 -4
View File
@@ -10,7 +10,7 @@ Get the app itself:
## Development Resources
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also 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.
@@ -42,10 +42,10 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th
- Open an issue and give some time for discussion before submitting a PR.
- Stay away from PRs like...
- Changing "Post" to "Skeet."
- Refactoring the codebase, e.g., to replace MobX with Redux 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
@@ -63,7 +63,7 @@ If you discover any security issues, please send an email to security@bsky.app.
## Are you a developer interested in building on atproto?
Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party 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.
## License (MIT)
+7 -7
View File
@@ -10,21 +10,21 @@ appId: xyz.blueskyweb.app
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: "Content warnings"
- tapOn: "Porn"
- tapOn:
id: "labelsBtn"
- tapOn:
label: "Tap on porn"
point: 78%,67%
- tapOn:
label: "Tap on confirm"
id: "confirmBtn"
point: 51%,92%
- tapOn:
id: "composerPublishBtn"
- tapOn:
id: "e2eRefreshHome"
- assertVisible: "Adult Content"
- assertVisible: "Adult Content"
+1 -3
View File
@@ -8,8 +8,6 @@ appId: xyz.blueskyweb.app
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- assertVisible:
id: "composeFAB"
- tapOn:
id: "composeFAB"
- inputText: "Post text only"
@@ -62,7 +60,7 @@ appId: xyz.blueskyweb.app
id: "repostBtn"
- tapOn:
id: "quoteBtn"
- inputText: "QP text only"
- inputText: "QP text only"
- tapOn:
id: "composerPublishBtn"
- assertVisible:
+18 -3
View File
@@ -74,7 +74,8 @@ appId: xyz.blueskyweb.app
- tapOn: "Delete List"
- tapOn:
id: "confirmBtn"
- assertVisible: "This list is empty!"
- assertVisible:
id: "listsEmpty"
- tapOn:
label: "Create a new curatelist"
@@ -143,7 +144,8 @@ appId: xyz.blueskyweb.app
id: "homeScreenFeedTabs-Good Ppl"
- tapOn:
id: "e2eGotoLists"
- tapOn: "Good Ppl"
- tapOn:
id: "list-Good Ppl"
- tapOn: "About"
- assertVisible:
@@ -160,10 +162,23 @@ appId: xyz.blueskyweb.app
- assertNotVisible:
id: "userAddRemoveListsModal"
- tapOn:
label: "Shows the curatelist on my profile"
id: "bottomBarProfileBtn"
- swipe:
from:
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-5"
- tapOn:
id: "list-Good Ppl"
- tapOn:
label: "Adds and removes users on curatelists from the profile"
id: "bottomBarSearchBtn"
- tapOn: "Search"
- tapOn:
id: "searchTextInput"
- inputText: "bob"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
+14 -28
View File
@@ -10,16 +10,16 @@ appId: xyz.blueskyweb.app
id: "e2eSignInAlice"
# Pin alice's feed
- extendedWaitUntil:
visible:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn:
id: "profileCardButton"
id: "bottomBarProfileBtn"
- swipe:
from:
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-4"
- tapOn: "alice-favs"
- tapOn:
id: "feed-alice-favs"
- tapOn: "Pin to Home"
- tapOn:
id: "bottomBarHomeBtn"
@@ -36,16 +36,12 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn: "Edit Saved Feeds"
- tapOn:
label: "Tap on down arrow"
id: "feed-timeline-moveDown"
point: "79%,23%"
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
id: "bottomBarHomeBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
@@ -58,16 +54,11 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn:
label: "Tap on down arrow"
id: "feed-feed-moveDown"
point: "79%,23%"
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
id: "bottomBarHomeBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "Following"
@@ -80,16 +71,11 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn:
label: "Tap on unpin"
id: "feed-timeline-togglePin"
point: "91%,23%"
- tapOn:
label: "Save button"
id: "saveChangesBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
id: "bottomBarHomeBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
+17 -7
View File
@@ -9,9 +9,6 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "e2eSignInAlice"
- extendedWaitUntil:
visible:
text: "Feeds ✨"
- tapOn:
label: "Can go to feeds page using feeds button in tab bar"
text: "Feeds ✨"
@@ -20,29 +17,42 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Feeds button disappears after pinning a feed"
id: "bottomBarProfileBtn"
- swipe:
from:
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-4"
- tapOn: "alice-favs"
- tapOn:
id: "feed-alice-favs"
- tapOn: "Pin to Home"
- tapOn:
id: "bottomBarHomeBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertNotVisible: "Feeds ✨"
- tapOn:
label: "Can like posts"
id: "likeBtn"
- assertVisible:
id: "likeCount"
text: "1"
- tapOn:
id: "likeBtn"
- assertNotVisible:
id: "likeCount"
- tapOn:
label: "Can repost posts"
id: "repostBtn"
- tapOn: "Repost"
- assertVisible:
id: "repostCount"
text: "1"
- tapOn:
id: "repostBtn"
- tapOn: "Remove repost"
- tapOn: "Undo repost"
- assertNotVisible:
id: "repostCount"
- tapOn:
label: "Can delete posts"
+2 -2
View File
@@ -9,7 +9,7 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "e2eOpenLoggedOutView"
- tapOn: "Sign in"
- tapOn:
- tapOn:
id: "selectServiceButton"
- tapOn: "Custom"
- tapOn:
@@ -23,4 +23,4 @@ appId: xyz.blueskyweb.app
id: "loginPasswordInput"
- inputText: "hunter2"
- pressKey: Enter
- assertVisible: "Following"
- assertVisible: "Following"
+6 -8
View File
@@ -28,20 +28,18 @@ appId: xyz.blueskyweb.app
- assertVisible: "Muted Users"
- assertVisible: "Shhh"
# toggle mute subscription
- tapOn:
label: "Dropdown"
point: "71%,9%"
point: "70%,9%"
- tapOn: "Mute accounts"
- tapOn: "Mute list"
- tapOn: "Unmute"
# toggle block subscription
- tapOn:
label: "Dropdown"
point: "71%,9%"
point: "70%,9%"
- tapOn: "Block accounts"
- tapOn: "Block list"
- tapOn: "Unblock"
# the rest of the behaviors are tested in curate-lists.yml
# the rest of the behaviors are tested in curate-lists.yml
+31
View File
@@ -0,0 +1,31 @@
# Remove this test when the old onboarding is deprecated
appId: xyz.blueskyweb.app
---
- runScript:
file: ../setupServer.js
env:
SERVER_PATH: "?users"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- tapOn:
id: "e2eStartLongboarding"
- tapOn: "Continue to next step"
- tapOn: "Continue to the next step without following any accounts"
- tapOn: Show replies in Following feed
- tapOn: Show quote-posts in Following feed
- tapOn: Show re-posts in Following feed
- tapOn: Show replies in Following feed
- waitForAnimationToEnd
- tapOn: Continue to next step
- waitForAnimationToEnd
- tapOn: "Continue to the next step"
- waitForAnimationToEnd
- tapOn: Continue to next step
- waitForAnimationToEnd
- tapOn: Continue to next step
- waitForAnimationToEnd
- tapOn: "Complete onboarding and start using your account"
- waitForAnimationToEnd
- assertVisible: Following
+1 -1
View File
@@ -13,7 +13,7 @@ appId: xyz.blueskyweb.app
- tapOn: "Select an avatar"
- waitForAnimationToEnd
- tapOn:
point: "50%,22%"
point: "16%,22%"
- waitForAnimationToEnd
- tapOn: "Choose"
- waitForAnimationToEnd
+16 -3
View File
@@ -11,12 +11,25 @@ appId: xyz.blueskyweb.app
# Navigate to my profile
- extendedWaitUntil:
visible:
id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarProfileBtn"
# Can see feeds
- swipe:
from:
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-4"
- assertVisible:
id: "feed-alice-favs"
- swipe:
from:
id: "profilePager-selector"
direction: RIGHT
- tapOn:
id: "profilePager-selector-0"
# Open and close edit profile modal
- tapOn:
id: "profileHeaderEditProfileButton"
+2 -4
View File
@@ -10,12 +10,10 @@ appId: xyz.blueskyweb.app
id: "e2eSignInAlice"
# Navigate to another user profile
- extendedWaitUntil:
visible:
id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarSearchBtn"
- tapOn: "Search"
- tapOn:
id: "searchTextInput"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
+2 -2
View File
@@ -12,8 +12,8 @@ appId: xyz.blueskyweb.app
# Navigate to another user profile via autocomplete
- tapOn:
id: "bottomBarSearchBtn"
- assertVisible: "Search"
- tapOn: "Search"
- tapOn:
id: "searchTextInput"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
-34
View File
@@ -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 -2
View File
@@ -7,11 +7,10 @@ appId: xyz.blueskyweb.app
- runFlow:
file: ../setupApp.yml
# Login, create a thread, and log out
- tapOn:
id: "e2eSignInAlice"
- assertVisible:
id: "composeFAB"
- tapOn:
id: "composeFAB"
- inputText: "Test thread"
+14 -6
View File
@@ -11,8 +11,6 @@ appId: xyz.blueskyweb.app
# Navigate to thread
- extendedWaitUntil:
visible: "Thread root"
- tapOn: "Thread root"
- assertVisible: "Thread reply"
@@ -21,22 +19,32 @@ appId: xyz.blueskyweb.app
id: "likeBtn"
childOf:
id: "postThreadItem-by-bob.test"
- assertVisible: "1 like"
- assertVisible:
id: "likeCount-expanded"
- tapOn:
id: "likeBtn"
childOf:
id: "postThreadItem-by-bob.test"
- assertNotVisible: "1 like"
- assertNotVisible:
id: "likeCount-expanded"
# Can like a reply post
- tapOn:
id: "likeBtn"
childOf:
id: "postThreadItem-by-carla.test"
- assertVisible:
id: "likeCount"
childOf:
id: "postThreadItem-by-carla.test"
- tapOn:
id: "likeBtn"
childOf:
id: "postThreadItem-by-carla.test"
- assertNotVisible:
id: "likeCount"
childOf:
id: "postThreadItem-by-carla.test"
# Can repost the root post
- tapOn:
@@ -50,7 +58,7 @@ appId: xyz.blueskyweb.app
id: "repostBtn"
childOf:
id: "postThreadItem-by-bob.test"
- tapOn: "Remove repost"
- tapOn: "Undo repost"
- assertNotVisible:
id: "repostCount-expanded"
@@ -69,7 +77,7 @@ appId: xyz.blueskyweb.app
id: "repostBtn"
childOf:
id: "postThreadItem-by-carla.test"
- tapOn: "Remove repost"
- tapOn: "Undo repost"
- assertNotVisible:
id: "repostCount"
childOf:
+47
View File
@@ -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
-82
View File
@@ -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)
})
})
+7 -7
View File
@@ -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])
}
})
})
+134
View File
@@ -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_): &quot;Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #balloon #sciencemoment #scienceexperiment #experiment #test #amazing #pvexp&quot;. оригинальный звук - 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&#39;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)
})
})
+62 -66
View File
@@ -1,30 +1,26 @@
import {deleteAsync} from 'expo-file-system'
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
import ImageResizer from '@bam.tech/react-native-image-resizer'
import RNFetchBlob from 'rn-fetch-blob'
import {
downloadAndResize,
DownloadAndResizeOpts,
getResizedDimensions,
} from '../../src/lib/media/manip'
const mockResizedImage = {
path: 'file://resized-image.jpg',
size: 100,
width: 100,
height: 100,
mime: 'image/jpeg',
}
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(() => {
@@ -58,17 +54,17 @@ describe('downloadAndResize', () => {
'GET',
'https://example.com/image.jpg',
)
// 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(ImageResizer.createResizedImage).toHaveBeenCalledWith(
'file://downloaded-image.jpg',
100,
100,
'JPEG',
100,
undefined,
undefined,
undefined,
{mode: 'cover'},
)
expect(deleteAsync).toHaveBeenCalledWith(expect.any(String), {
idempotent: true,
})
})
it('should return undefined for invalid URI', async () => {
@@ -86,6 +82,46 @@ describe('downloadAndResize', () => {
expect(result).toBeUndefined()
})
it('should return undefined for unsupported file type', async () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock
mockedFetch.mockResolvedValueOnce({
path: jest.fn().mockReturnValue('file://downloaded-image'),
info: jest.fn().mockReturnValue({status: 200}),
flush: jest.fn(),
})
const opts: DownloadAndResizeOpts = {
uri: 'https://example.com/image',
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'},
)
})
it('should return undefined for non-200 response', async () => {
const mockedFetch = RNFetchBlob.fetch as jest.Mock
mockedFetch.mockResolvedValueOnce({
@@ -107,44 +143,4 @@ describe('downloadAndResize', () => {
expect(errorSpy).not.toHaveBeenCalled()
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,
})
expect(resizedDimensionsTwo).toEqual({
width: 1000,
height: 2000,
})
})
})
+74 -201
View File
@@ -1,16 +1,12 @@
import {RichText} from '@atproto/api'
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
import {
createStarterPackGooglePlayUri,
createStarterPackLinkFromAndroidReferrer,
parseStarterPackUri,
} from 'lib/strings/starter-pack'
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 {ago} from '../../src/lib/strings/time'
import {
makeRecordUri,
toNiceDomain,
@@ -146,6 +142,79 @@ describe('makeRecordUri', () => {
})
})
// FIXME: Reenable after fixing non-deterministic test.
describe.skip('ago', () => {
const oneYearDate = new Date(
new Date().setMonth(new Date().getMonth() - 11),
).setDate(new Date().getDate() - 28)
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().setDate(new Date().getDate() - 20),
new Date().setDate(new Date().getDate() - 25),
new Date().setDate(new Date().getDate() - 28),
new Date().setDate(new Date().getDate() - 29),
new Date().setDate(new Date().getDate() - 30),
new Date().setMonth(new Date().getMonth() - 1),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 20,
),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 25,
),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 28,
),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 29,
),
new Date().setMonth(new Date().getMonth() - 11),
new Date(new Date().setMonth(new Date().getMonth() - 11)).setDate(
new Date().getDate() - 20,
),
new Date(new Date().setMonth(new Date().getMonth() - 11)).setDate(
new Date().getDate() - 25,
),
oneYearDate,
]
const outputs = [
new Date(1671461038).toLocaleDateString(),
new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString(),
'now',
'10s',
'10m',
'1h',
'1d',
'20d',
'25d',
'28d',
'29d',
'1mo',
'1mo',
'1mo',
'1mo',
'2mo',
'2mo',
'11mo',
'11mo',
'11mo',
new Date(oneYearDate).toLocaleDateString(),
]
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',
@@ -335,21 +404,17 @@ 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',
@@ -443,11 +508,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',
@@ -458,11 +518,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',
@@ -489,16 +544,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',
},
undefined,
undefined,
undefined,
undefined,
{
type: 'twitch_video',
@@ -821,179 +870,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)
})
})
+45 -180
View File
@@ -1,6 +1,26 @@
const pkg = require('./package.json')
const DARK_SPLASH_ANDROID_BACKGROUND = '#0f141b'
const SPLASH_CONFIG = {
backgroundColor: '#ffffff',
image: './assets/splash.png',
resizeMode: 'cover',
}
const DARK_SPLASH_CONFIG = {
backgroundColor: '#001429',
image: './assets/splash-dark.png',
resizeMode: 'cover',
}
const SPLASH_CONFIG_ANDROID = {
backgroundColor: '#0c7cff',
image: './assets/splash.png',
resizeMode: 'cover',
}
const DARK_SPLASH_CONFIG_ANDROID = {
backgroundColor: '#0f141b',
image: './assets/splash-dark.png',
resizeMode: 'cover',
}
module.exports = function (config) {
/**
@@ -15,18 +35,9 @@ module.exports = function (config) {
*/
const PLATFORM = process.env.EAS_BUILD_PLATFORM
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
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_CHANNEL = IS_TESTFLIGHT
? 'testflight'
@@ -35,7 +46,6 @@ module.exports = function (config) {
: undefined
const UPDATES_ENABLED = !!UPDATES_CHANNEL
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${
IS_DEV ? 'dev' : ''
}`
@@ -51,8 +61,10 @@ module.exports = function (config) {
policy: 'appVersion',
},
orientation: 'portrait',
icon: './assets/app-icons/ios_icon_default_light.png',
icon: './assets/icon.png',
userInterfaceStyle: 'automatic',
splash: SPLASH_CONFIG,
// hsl(211, 99%, 53%), same as palette.default.brandText
primaryColor: '#1083fe',
ios: {
supportsTablet: false,
@@ -70,41 +82,13 @@ 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',
'de',
'es',
'fi',
'fr',
'ga',
'gl',
'hi',
'hu',
'id',
'it',
'ja',
'ko',
'nl',
'pl',
'pt-BR',
'ru',
'th',
'tr',
'uk',
'vi',
'yue-Hant',
'zh-Hans',
'zh-Hant',
],
},
associatedDomains: ASSOCIATED_DOMAINS,
associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'],
splash: {
...SPLASH_CONFIG,
dark: DARK_SPLASH_CONFIG,
},
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: {
@@ -138,10 +122,10 @@ module.exports = function (config) {
// Dark nav bar in light mode is better than light nav bar in dark mode
androidNavigationBar: {
barStyle: 'light-content',
backgroundColor: DARK_SPLASH_ANDROID_BACKGROUND,
backgroundColor: DARK_SPLASH_CONFIG_ANDROID.backgroundColor,
},
android: {
icon: './assets/app-icons/android_icon_default_light.png',
icon: './assets/icon.png',
adaptiveIcon: {
foregroundImage: './assets/icon-android-foreground.png',
monochromeImage: './assets/icon-android-foreground.png',
@@ -167,6 +151,10 @@ module.exports = function (config) {
category: ['BROWSABLE', 'DEFAULT'],
},
],
splash: {
...SPLASH_CONFIG_ANDROID,
dark: DARK_SPLASH_CONFIG_ANDROID,
},
},
web: {
favicon: './assets/favicon.png',
@@ -189,26 +177,19 @@ module.exports = function (config) {
},
plugins: [
'expo-localization',
USE_SENTRY && [
'@sentry/react-native/expo',
{
organization: 'blueskyweb',
project: 'react-native',
release: VERSION,
dist: SENTRY_DIST,
},
],
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
[
'expo-build-properties',
{
ios: {
deploymentTarget: '15.1',
deploymentTarget: '14.0',
newArchEnabled: false,
},
android: {
compileSdkVersion: 35,
targetSdkVersion: 35,
buildToolsVersion: '35.0.0',
compileSdkVersion: 34,
targetSdkVersion: 34,
buildToolsVersion: '34.0.0',
kotlinVersion: '1.8.0',
newArchEnabled: false,
},
},
@@ -221,126 +202,14 @@ module.exports = function (config) {
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
},
],
'react-native-compressor',
'@bitdrift/react-native',
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidManifestLaunchModePlugin.js',
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
'./plugins/shareExtension/withShareExtensions.js',
'./plugins/notificationsExtension/withNotificationsExtension.js',
'./plugins/withAppDelegateReferrer.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-SemiBold.otf',
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
'./assets/fonts/inter/Inter-ExtraBold.otf',
'./assets/fonts/inter/Inter-ExtraBoldItalic.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,
},
},
],
].filter(Boolean),
extra: {
eas: {
@@ -366,10 +235,6 @@ module.exports = function (config) {
],
},
},
{
targetName: 'BlueskyClip',
bundleIdentifier: 'xyz.blueskyweb.app.AppClip',
},
],
},
},
@@ -383,7 +248,7 @@ module.exports = function (config) {
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration
*/
{
file: './postHooks/uploadSentrySourcemapsPostHook',
file: 'sentry-expo/upload-sourcemaps',
config: {
organization: 'blueskyweb',
project: 'react-native',
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More