+29
-5
.env.example
+29
-5
.env.example
···
1
-
# Copy this to `.env` and `.env.test` files
1
+
# The env the app is running in e.g. development, testflight, production
2
+
EXPO_PUBLIC_ENV=development
3
+
4
+
# This is the semver release version of the app, pulled from package.json
5
+
EXPO_PUBLIC_RELEASE_VERSION=
2
6
3
-
BITDRIFT_API_KEY=
4
-
SENTRY_AUTH_TOKEN=
5
-
EXPO_PUBLIC_LOG_LEVEL=debug
6
-
EXPO_PUBLIC_LOG_DEBUG=
7
+
# This is the commit hash that the current bundle was made from.
7
8
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
9
+
10
+
# Should be formatted YYMMDDHH so that it increases for each build.
8
11
EXPO_PUBLIC_BUNDLE_DATE=0
12
+
13
+
# The log level for the app's logger transports
14
+
EXPO_PUBLIC_LOG_LEVEL=debug
15
+
16
+
# Enable debug logs for specific logger instances
17
+
EXPO_PUBLIC_LOG_DEBUG=session
18
+
19
+
# Chat service DID
20
+
EXPO_PUBLIC_CHAT_PROXY_DID=
21
+
22
+
#
23
+
#
24
+
# Bluesky specific values
25
+
#
26
+
#
27
+
28
+
# Sentry DSN for telemetry
29
+
EXPO_PUBLIC_SENTRY_DSN=
30
+
31
+
# Bitdrift API key. If undefined, Bitdrift will be disabled.
32
+
EXPO_PUBLIC_BITDRIFT_API_KEY=
+8
-9
.github/workflows/build-and-push-bskyweb-aws.yaml
+8
-9
.github/workflows/build-and-push-bskyweb-aws.yaml
···
43
43
tags: |
44
44
type=sha,enable=true,priority=100,prefix=,suffix=,format=long
45
45
46
-
- name: Set outputs
47
-
id: vars
46
+
- name: Env
47
+
id: env
48
48
run: |
49
-
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
50
-
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
51
-
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
49
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
50
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
52
51
53
52
- name: Build and push Docker image
54
53
id: build-and-push
···
62
61
cache-from: type=gha
63
62
cache-to: type=gha,mode=max
64
63
build-args: |
65
-
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.vars.outputs.sha_short }}
66
-
SENTRY_DIST=${{ steps.vars.outputs.SENTRY_DIST }}
67
-
SENTRY_RELEASE=${{ steps.vars.outputs.SENTRY_RELEASE }}
64
+
EXPO_PUBLIC_ENV=production
65
+
EXPO_PUBLIC_RELEASE_VERSION=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
66
+
EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
67
+
EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
68
68
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
69
-
SENTRY_DSN=${{ secrets.SENTRY_DSN }}
+25
-11
.github/workflows/build-submit-android.yml
+25
-11
.github/workflows/build-submit-android.yml
···
62
62
- name: Check for i18n compilation errors
63
63
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
64
64
65
-
- name: ✏️ Write environment variables
65
+
# EXPO_PUBLIC_ENV is handled in eas.json
66
+
- name: Env
67
+
id: env
66
68
run: |
67
69
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
68
70
echo "${{ secrets.ENV_TOKEN }}" > .env
69
-
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
71
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
72
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
73
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
74
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
70
75
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
71
-
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
76
+
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
77
+
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
72
78
echo "$json" > google-services.json
73
79
74
-
- name: Setup Sentry vars for build-time injection
75
-
id: sentry
76
-
run: |
77
-
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
78
-
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
79
-
80
80
- name: 🏗️ EAS Build
81
-
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive
81
+
run: >
82
+
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
83
+
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
84
+
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
85
+
yarn use-build-number-with-bump
86
+
eas build -p android
87
+
--profile ${{ inputs.profile || 'testflight-android' }}
88
+
--local --output build.aab --non-interactive
82
89
83
90
- name: ✍️ Rename Testflight bundle
84
91
if: ${{ inputs.profile != 'production' }}
···
140
147
141
148
- name: 🏗️ Build Production APK
142
149
if: ${{ inputs.profile == 'production' }}
143
-
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p android --profile production-apk --local --output build.apk --non-interactive
150
+
run: >
151
+
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
152
+
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
153
+
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
154
+
yarn use-build-number-with-bump
155
+
eas build -p android
156
+
--profile production-apk
157
+
--local --output build.apk --non-interactive
144
158
145
159
- name: 🚀 Upload Production APK Artifact
146
160
id: upload-artifact-production-apk
+16
-9
.github/workflows/build-submit-ios.yml
+16
-9
.github/workflows/build-submit-ios.yml
···
75
75
- name: Check for i18n compilation errors
76
76
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
77
77
78
+
# EXPO_PUBLIC_ENV is handled in eas.json
78
79
- name: ✏️ Write environment variables
80
+
id: env
79
81
run: |
80
82
echo "${{ secrets.ENV_TOKEN }}" > .env
81
-
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
83
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
84
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
85
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
86
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
82
87
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
83
-
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
88
+
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
89
+
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
84
90
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
85
91
86
-
- name: Setup Sentry vars for build-time injection
87
-
id: sentry
88
-
run: |
89
-
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
90
-
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
91
-
92
92
- name: 🏗️ EAS Build
93
-
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive
93
+
run: >
94
+
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
95
+
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
96
+
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
97
+
yarn use-build-number-with-bump
98
+
eas build -p ios
99
+
--profile ${{ inputs.profile || 'testflight' }}
100
+
--local --output build.ipa --non-interactive
94
101
95
102
- name: 🚀 Deploy
96
103
run: eas submit -p ios --non-interactive --path build.ipa
+49
-18
.github/workflows/bundle-deploy-eas-update.yml
+49
-18
.github/workflows/bundle-deploy-eas-update.yml
···
101
101
if: ${{ !steps.fingerprint.outputs.includes-changes }}
102
102
uses: dcarbone/install-jq-action@v2
103
103
104
-
- name: ✏️ Write environment variables
104
+
# eas.json not used here, set EXPO_PUBLIC_ENV
105
+
- name: Env
106
+
id: env
105
107
if: ${{ !steps.fingerprint.outputs.includes-changes }}
106
108
run: |
107
109
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
108
110
echo "${{ secrets.ENV_TOKEN }}" > .env
109
-
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
111
+
echo "EXPO_PUBLIC_ENV=${{ inputs.channel || 'testflight' }}" >> .env
112
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
113
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
114
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
115
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
110
116
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
111
-
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
117
+
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
118
+
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
112
119
echo "$json" > google-services.json
113
120
114
-
- name: Setup Sentry vars for build-time injection
115
-
id: sentry
116
-
run: |
117
-
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
118
-
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
119
-
120
121
- name: 🏗️ Create Bundle
121
122
if: ${{ !steps.fingerprint.outputs.includes-changes }}
122
-
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
123
+
run: >
124
+
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
125
+
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
126
+
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
127
+
yarn export
123
128
124
129
- name: 📦 Package Bundle and 🚀 Deploy
125
130
if: ${{ !steps.fingerprint.outputs.includes-changes }}
···
205
210
- name: 🔤 Compile translations
206
211
run: yarn intl:build
207
212
208
-
- name: ✏️ Write environment variables
213
+
# EXPO_PUBLIC_ENV is handled in eas.json
214
+
- name: Env
215
+
id: env
209
216
run: |
210
217
echo "${{ secrets.ENV_TOKEN }}" > .env
211
-
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
218
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
219
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
220
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
221
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
212
222
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
213
-
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
223
+
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
224
+
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
214
225
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
215
226
216
227
- name: 🏗️ EAS Build
217
-
run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive
228
+
run: >
229
+
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
230
+
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
231
+
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
232
+
yarn use-build-number-with-bump
233
+
eas build -p ios
234
+
--profile testflight
235
+
--local --output build.ipa --non-interactive
218
236
219
237
- name: 🚀 Deploy
220
238
run: eas submit -p ios --non-interactive --path build.ipa
···
282
300
- name: 🔤 Compile translations
283
301
run: yarn intl:build
284
302
285
-
- name: ✏️ Write environment variables
303
+
# EXPO_PUBLIC_ENV is handled in eas.json
304
+
- name: Env
305
+
id: env
286
306
run: |
287
307
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
288
308
echo "${{ secrets.ENV_TOKEN }}" > .env
289
-
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
309
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
310
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
311
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
312
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
290
313
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
291
-
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
314
+
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
315
+
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
292
316
echo "$json" > google-services.json
293
317
294
318
- name: 🏗️ EAS Build
295
-
run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive
319
+
run: >
320
+
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
321
+
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
322
+
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
323
+
yarn use-build-number-with-bump
324
+
eas build -p android
325
+
--profile testflight-android
326
+
--local --output build.apk --non-interactive
296
327
297
328
- name: ⏰ Get a timestamp
298
329
id: timestamp
+14
-10
.github/workflows/pull-request-comment.yml
+14
-10
.github/workflows/pull-request-comment.yml
···
152
152
- name: 🪛 Setup jq
153
153
uses: dcarbone/install-jq-action@v2
154
154
155
-
- name: ✏️ Write environment variables
155
+
- name: Env
156
+
id: env
156
157
run: |
157
158
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
158
159
echo "${{ secrets.ENV_TOKEN }}" > .env
159
-
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
160
+
echo "EXPO_PUBLIC_ENV=testflight" >> .env
161
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
162
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
163
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
164
+
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
160
165
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
161
-
echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
166
+
echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
167
+
echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
162
168
echo "$json" > google-services.json
163
169
164
-
- name: Setup Sentry vars for build-time injection
165
-
id: sentry
166
-
run: |
167
-
echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
168
-
echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
169
-
170
170
- name: 🏗️ Create Bundle
171
-
run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="testflight" yarn export
171
+
run: >
172
+
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
173
+
SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
174
+
SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
175
+
yarn export
172
176
173
177
- name: 📦 Package Bundle and 🚀 Deploy
174
178
run: yarn use-build-number bash scripts/bundleUpdate.sh
+17
-14
Dockerfile
+17
-14
Dockerfile
···
1
-
FROM golang:1.24-bullseye AS build-env
1
+
FROM golang:1.24.5-bullseye AS build-env
2
2
3
3
WORKDIR /usr/src/social-app
4
4
···
19
19
ENV CGO_ENABLED=1
20
20
ENV GOEXPERIMENT="loopvar"
21
21
22
+
# The latest git hash of the preview branch on render.com
23
+
# https://render.com/docs/docker-secrets#environment-variables-in-docker-builds
24
+
ARG RENDER_GIT_COMMIT
25
+
22
26
#
23
27
# Expo
24
28
#
29
+
ARG EXPO_PUBLIC_ENV
30
+
ENV EXPO_PUBLIC_ENV=${EXPO_PUBLIC_ENV:-development}
31
+
ARG EXPO_PUBLIC_RELEASE_VERSION
32
+
ENV EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION
25
33
ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
26
-
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev}
27
-
28
-
# The latest git hash of the preview branch on render.com
29
-
ARG RENDER_GIT_COMMIT
34
+
# If not set by GitHub workflows, we're probably in Render
35
+
ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-$RENDER_GIT_COMMIT}
30
36
31
37
#
32
38
# Sentry
33
39
#
34
40
ARG SENTRY_AUTH_TOKEN
35
41
ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown}
36
-
# Will fall back to package.json#version, but this is handled elsewhere
37
-
ARG SENTRY_RELEASE
38
-
ENV SENTRY_RELEASE=$SENTRY_RELEASE
39
-
ARG SENTRY_DIST
40
-
# Default to RENDER_GIT_COMMIT if not set by GitHub workflows
41
-
ENV SENTRY_DIST=${SENTRY_DIST:-$RENDER_GIT_COMMIT}
42
-
ARG SENTRY_DSN
43
-
ENV SENTRY_DSN=$SENTRY_DSN
42
+
ARG EXPO_PUBLIC_SENTRY_DSN
43
+
ENV EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN
44
44
45
45
#
46
46
# Copy everything into the container
···
60
60
nvm install $NODE_VERSION && \
61
61
nvm use $NODE_VERSION && \
62
62
echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \
63
+
echo "EXPO_PUBLIC_ENV=$EXPO_PUBLIC_ENV" >> .env && \
64
+
echo "EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION" >> .env && \
63
65
echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \
64
66
echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env && \
67
+
echo "EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN" >> .env && \
65
68
npm install --global yarn && \
66
69
yarn && \
67
70
yarn intl:build 2>&1 | tee i18n.log && \
68
71
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 && \
69
-
EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER EXPO_PUBLIC_BUNDLE_DATE=$() SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$SENTRY_RELEASE SENTRY_DIST=$SENTRY_DIST SENTRY_DSN=$SENTRY_DSN yarn build-web
72
+
SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$EXPO_PUBLIC_RELEASE_VERSION SENTRY_DIST=$EXPO_PUBLIC_BUNDLE_IDENTIFIER yarn build-web
70
73
71
74
# DEBUG
72
75
RUN find ./bskyweb/static && find ./web-build/static
+1
-1
Dockerfile.embedr
+1
-1
Dockerfile.embedr
+4
-5
bskyweb/go.mod
+4
-5
bskyweb/go.mod
···
1
1
module github.com/bluesky-social/social-app/bskyweb
2
2
3
-
go 1.23.0
4
-
5
-
toolchain go1.24.5
3
+
go 1.24.5
6
4
7
5
require (
8
-
github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc
6
+
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a
9
7
github.com/flosch/pongo2/v6 v6.0.0
10
8
github.com/ipfs/go-log v1.0.5
11
9
github.com/joho/godotenv v1.5.1
···
15
13
github.com/prometheus/client_golang v1.22.0
16
14
github.com/urfave/cli/v2 v2.25.7
17
15
)
16
+
17
+
require github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
18
18
19
19
require (
20
20
github.com/beorn7/perks v1.0.1 // indirect
···
70
70
github.com/multiformats/go-multibase v0.2.0 // indirect
71
71
github.com/multiformats/go-multihash v0.2.3 // indirect
72
72
github.com/multiformats/go-varint v0.0.7 // indirect
73
-
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
74
73
github.com/opentracing/opentracing-go v1.2.0 // indirect
75
74
github.com/orandin/slog-gorm v1.3.2 // indirect
76
75
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
+2
-2
bskyweb/go.sum
+2
-2
bskyweb/go.sum
···
2
2
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
3
3
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
4
4
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
5
-
github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc h1:chbGD59Cn1mw07kbq9Uvb8WUFIr1dcoL5TOhT+I9bV4=
6
-
github.com/bluesky-social/indigo v0.0.0-20250605010711-db9bb60025dc/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng=
5
+
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a h1:S12KN45uIkRglMHC8PqD/Vsz0+u3KbIaBF/6rit8/Pg=
6
+
github.com/bluesky-social/indigo v0.0.0-20250729223159-573ae927246a/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q=
7
7
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
8
8
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
9
9
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
+2
-2
docs/build.md
+2
-2
docs/build.md
···
89
89
90
90
### Adding bitdrift
91
91
92
-
Adding bitdirft is NOT required. You can keep `BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
92
+
Adding bitdrift is NOT required. You can keep `EXPO_PUBLIC_BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
93
93
94
-
However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
94
+
However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `EXPO_PUBLIC_BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
95
95
96
96
### Adding and Updating Locales
97
97
+125
-76
src/components/FeedInterstitials.tsx
+125
-76
src/components/FeedInterstitials.tsx
···
25
25
type ViewStyleProp,
26
26
web,
27
27
} from '#/alf'
28
-
import {Button} from '#/components/Button'
28
+
import {Button, ButtonText} from '#/components/Button'
29
29
import * as FeedCard from '#/components/FeedCard'
30
30
import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
31
31
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
32
-
import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
33
32
import {InlineLinkText} from '#/components/Link'
34
33
import * as ProfileCard from '#/components/ProfileCard'
35
34
import {Text} from '#/components/Typography'
36
35
import type * as bsky from '#/types/bsky'
37
36
import {ProgressGuideList} from './ProgressGuide/List'
38
37
39
-
const MOBILE_CARD_WIDTH = 300
38
+
const MOBILE_CARD_WIDTH = 165
40
39
41
40
function CardOuter({
42
41
children,
···
48
47
<View
49
48
style={[
50
49
a.w_full,
51
-
a.p_lg,
52
-
a.rounded_md,
50
+
a.p_md,
51
+
a.rounded_lg,
53
52
a.border,
54
53
t.atoms.bg,
55
54
t.atoms.border_contrast_low,
···
65
64
66
65
export function SuggestedFollowPlaceholder() {
67
66
const t = useTheme()
67
+
68
68
return (
69
-
<CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}>
70
-
<ProfileCard.Header>
71
-
<ProfileCard.AvatarPlaceholder />
72
-
<ProfileCard.NameAndHandlePlaceholder />
73
-
</ProfileCard.Header>
69
+
<CardOuter
70
+
style={[a.gap_md, t.atoms.border_contrast_low, t.atoms.shadow_sm]}>
71
+
<ProfileCard.Outer>
72
+
<View
73
+
style={[a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto]}>
74
+
<ProfileCard.AvatarPlaceholder size={88} />
75
+
<ProfileCard.NamePlaceholder />
76
+
<View style={[a.w_full]}>
77
+
<ProfileCard.DescriptionPlaceholder numberOfLines={2} />
78
+
</View>
79
+
</View>
74
80
75
-
<ProfileCard.DescriptionPlaceholder numberOfLines={2} />
81
+
<Button
82
+
label=""
83
+
size="small"
84
+
variant="solid"
85
+
color="secondary"
86
+
disabled
87
+
style={[a.w_full, a.rounded_sm]}>
88
+
<ButtonText>Follow</ButtonText>
89
+
</Button>
90
+
</ProfileCard.Outer>
76
91
</CardOuter>
77
92
)
78
93
}
···
243
258
const t = useTheme()
244
259
const {_} = useLingui()
245
260
const moderationOpts = useModerationOpts()
246
-
const navigation = useNavigation<NavigationProp>()
247
261
const {gtMobile} = useBreakpoints()
248
262
const isLoading = isSuggestionsLoading || !moderationOpts
249
-
const maxLength = gtMobile ? 4 : 6
263
+
const maxLength = gtMobile ? 3 : 6
250
264
251
265
const content = isLoading ? (
252
266
Array(maxLength)
···
254
268
.map((_, i) => (
255
269
<View
256
270
key={i}
257
-
style={[gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}])]}>
271
+
style={[
272
+
gtMobile &&
273
+
web([
274
+
a.flex_0,
275
+
a.flex_grow,
276
+
{width: `calc(30% - ${a.gap_md.gap / 2}px)`},
277
+
]),
278
+
]}>
258
279
<SuggestedFollowPlaceholder />
259
280
</View>
260
281
))
···
276
297
}}
277
298
style={[
278
299
a.flex_1,
279
-
gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}]),
300
+
gtMobile &&
301
+
web([
302
+
a.flex_0,
303
+
a.flex_grow,
304
+
{width: `calc(30% - ${a.gap_md.gap / 2}px)`},
305
+
]),
280
306
]}>
281
307
{({hovered, pressed}) => (
282
308
<CardOuter
283
309
style={[
284
310
a.flex_1,
311
+
t.atoms.shadow_sm,
285
312
(hovered || pressed) && t.atoms.border_contrast_high,
286
313
]}>
287
314
<ProfileCard.Outer>
288
-
<ProfileCard.Header>
315
+
<View
316
+
style={[
317
+
a.flex_col,
318
+
a.align_center,
319
+
a.gap_sm,
320
+
a.pb_sm,
321
+
a.mb_auto,
322
+
]}>
289
323
<ProfileCard.Avatar
290
324
profile={profile}
291
325
moderationOpts={moderationOpts}
326
+
size={88}
292
327
/>
293
-
<ProfileCard.NameAndHandle
294
-
profile={profile}
295
-
moderationOpts={moderationOpts}
296
-
/>
297
-
<ProfileCard.FollowButton
298
-
profile={profile}
299
-
moderationOpts={moderationOpts}
300
-
logContext="FeedInterstitial"
301
-
shape="round"
302
-
colorInverted
303
-
onFollow={() => {
304
-
logEvent('suggestedUser:follow', {
305
-
logContext:
306
-
viewContext === 'feed'
307
-
? 'InterstitialDiscover'
308
-
: 'InterstitialProfile',
309
-
location: 'Card',
310
-
recId,
311
-
position: index,
312
-
})
313
-
}}
314
-
/>
315
-
</ProfileCard.Header>
316
-
<ProfileCard.Description profile={profile} numberOfLines={2} />
328
+
<View style={[a.flex_col, a.align_center, a.max_w_full]}>
329
+
<ProfileCard.Name
330
+
profile={profile}
331
+
moderationOpts={moderationOpts}
332
+
/>
333
+
<ProfileCard.Description
334
+
profile={profile}
335
+
numberOfLines={2}
336
+
style={[
337
+
t.atoms.text_contrast_medium,
338
+
a.text_center,
339
+
a.text_xs,
340
+
]}
341
+
/>
342
+
</View>
343
+
</View>
344
+
345
+
<ProfileCard.FollowButton
346
+
profile={profile}
347
+
moderationOpts={moderationOpts}
348
+
logContext="FeedInterstitial"
349
+
withIcon={false}
350
+
style={[a.rounded_sm]}
351
+
onFollow={() => {
352
+
logEvent('suggestedUser:follow', {
353
+
logContext:
354
+
viewContext === 'feed'
355
+
? 'InterstitialDiscover'
356
+
: 'InterstitialProfile',
357
+
location: 'Card',
358
+
recId,
359
+
position: index,
360
+
})
361
+
}}
362
+
/>
317
363
</ProfileCard.Outer>
318
364
</CardOuter>
319
365
)}
···
333
379
<View
334
380
style={[
335
381
a.p_lg,
336
-
a.pb_xs,
382
+
a.py_md,
337
383
a.flex_row,
338
384
a.align_center,
339
385
a.justify_between,
340
386
]}>
341
-
<Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}>
387
+
<Text style={[a.text_sm, a.font_bold, t.atoms.text]}>
342
388
{viewContext === 'profile' ? (
343
389
<Trans>Similar accounts</Trans>
344
390
) : (
345
391
<Trans>Suggested for you</Trans>
346
392
)}
347
393
</Text>
348
-
<Person fill={t.atoms.text_contrast_low.color} size="sm" />
394
+
<InlineLinkText
395
+
label={_(msg`See more suggested profiles on the Explore page`)}
396
+
to="/search">
397
+
<Trans>See more</Trans>
398
+
</InlineLinkText>
349
399
</View>
350
400
351
401
{gtMobile ? (
352
-
<View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}>
353
-
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}>
402
+
<View style={[a.px_lg, a.pb_lg]}>
403
+
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
354
404
{content}
355
405
</View>
356
-
357
-
<View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}>
358
-
<InlineLinkText
359
-
label={_(msg`Browse more suggestions`)}
360
-
to="/search"
361
-
style={[t.atoms.text_contrast_medium]}>
362
-
<Trans>Browse more suggestions</Trans>
363
-
</InlineLinkText>
364
-
<Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
365
-
</View>
366
406
</View>
367
407
) : (
368
408
<BlockDrawerGesture>
···
371
411
horizontal
372
412
showsHorizontalScrollIndicator={false}
373
413
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
374
-
decelerationRate="fast">
375
-
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
414
+
decelerationRate="fast"
415
+
style={[a.overflow_visible]}>
416
+
<View style={[a.px_lg, a.pb_lg, a.flex_row, a.gap_md]}>
376
417
{content}
377
418
378
-
<Button
379
-
label={_(msg`Browse more accounts on the Explore page`)}
380
-
onPress={() => {
381
-
navigation.navigate('SearchTab')
382
-
}}>
383
-
<CardOuter style={[a.flex_1, {borderWidth: 0}]}>
384
-
<View style={[a.flex_1, a.justify_center]}>
385
-
<View style={[a.flex_row, a.px_lg]}>
386
-
<Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
387
-
<Trans>
388
-
Browse more suggestions on the Explore page
389
-
</Trans>
390
-
</Text>
391
-
392
-
<Arrow size="xl" />
393
-
</View>
394
-
</View>
395
-
</CardOuter>
396
-
</Button>
419
+
<SeeMoreSuggestedProfilesCard />
397
420
</View>
398
421
</ScrollView>
399
422
</View>
400
423
</BlockDrawerGesture>
401
424
)}
402
425
</View>
426
+
)
427
+
}
428
+
429
+
function SeeMoreSuggestedProfilesCard() {
430
+
const navigation = useNavigation<NavigationProp>()
431
+
const t = useTheme()
432
+
const {_} = useLingui()
433
+
434
+
return (
435
+
<Button
436
+
label={_(msg`Browse more accounts on the Explore page`)}
437
+
onPress={() => {
438
+
navigation.navigate('SearchTab')
439
+
}}>
440
+
<CardOuter style={[a.flex_1, t.atoms.shadow_sm]}>
441
+
<View style={[a.flex_1, a.justify_center]}>
442
+
<View style={[a.flex_col, a.align_center, a.gap_md]}>
443
+
<Text style={[a.leading_snug, a.text_center]}>
444
+
<Trans>See more accounts you might like</Trans>
445
+
</Text>
446
+
447
+
<Arrow size="xl" />
448
+
</View>
449
+
</View>
450
+
</CardOuter>
451
+
</Button>
403
452
)
404
453
}
405
454
+1
-1
src/components/PostControls/DiscoverDebug.tsx
+1
-1
src/components/PostControls/DiscoverDebug.tsx
···
2
2
import * as Clipboard from 'expo-clipboard'
3
3
import {t} from '@lingui/macro'
4
4
5
-
import {IS_INTERNAL} from '#/lib/app-info'
6
5
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
7
6
import {useGate} from '#/lib/statsig/statsig'
8
7
import {useSession} from '#/state/session'
9
8
import * as Toast from '#/view/com/util/Toast'
10
9
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
11
10
import {Text} from '#/components/Typography'
11
+
import {IS_INTERNAL} from '#/env'
12
12
13
13
export function DiscoverDebug({
14
14
feedContext,
+1
-1
src/components/PostControls/PostMenu/PostMenuItems.tsx
+1
-1
src/components/PostControls/PostMenu/PostMenuItems.tsx
···
17
17
import {useLingui} from '@lingui/react'
18
18
import {useNavigation} from '@react-navigation/native'
19
19
20
-
import {IS_INTERNAL} from '#/lib/app-info'
21
20
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
22
21
import {useOpenLink} from '#/lib/hooks/useOpenLink'
23
22
import {getCurrentRoute} from '#/lib/routes/helpers'
···
83
82
useReportDialogControl,
84
83
} from '#/components/moderation/ReportDialog'
85
84
import * as Prompt from '#/components/Prompt'
85
+
import {IS_INTERNAL} from '#/env'
86
86
import * as bsky from '#/types/bsky'
87
87
88
88
let PostMenuItems = ({
+36
-9
src/components/ProfileCard.tsx
+36
-9
src/components/ProfileCard.tsx
···
20
20
import {useSession} from '#/state/session'
21
21
import * as Toast from '#/view/com/util/Toast'
22
22
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
23
-
import {atoms as a, platform, useTheme} from '#/alf'
23
+
import {
24
+
atoms as a,
25
+
platform,
26
+
type TextStyleProp,
27
+
useTheme,
28
+
type ViewStyleProp,
29
+
} from '#/alf'
24
30
import {
25
31
Button,
26
32
ButtonIcon,
···
136
142
onPress,
137
143
disabledPreview,
138
144
liveOverride,
145
+
size = 40,
139
146
}: {
140
147
profile: bsky.profile.AnyProfileView
141
148
moderationOpts: ModerationOpts
142
149
onPress?: () => void
143
150
disabledPreview?: boolean
144
151
liveOverride?: boolean
152
+
size?: number
145
153
}) {
146
154
const moderation = moderateProfile(profile, moderationOpts)
147
155
···
149
157
150
158
return disabledPreview ? (
151
159
<UserAvatar
152
-
size={40}
160
+
size={size}
153
161
avatar={profile.avatar}
154
162
type={profile.associated?.labeler ? 'labeler' : 'user'}
155
163
moderation={moderation.ui('avatar')}
···
157
165
/>
158
166
) : (
159
167
<PreviewableUserAvatar
160
-
size={40}
168
+
size={size}
161
169
profile={profile}
162
170
moderation={moderation.ui('avatar')}
163
171
onBeforePress={onPress}
···
166
174
)
167
175
}
168
176
169
-
export function AvatarPlaceholder() {
177
+
export function AvatarPlaceholder({size = 40}: {size?: number}) {
170
178
const t = useTheme()
171
179
return (
172
180
<View
···
174
182
a.rounded_full,
175
183
t.atoms.bg_contrast_25,
176
184
{
177
-
width: 40,
178
-
height: 40,
185
+
width: size,
186
+
height: size,
179
187
},
180
188
]}
181
189
/>
···
274
282
)
275
283
const verification = useSimpleVerificationState({profile})
276
284
return (
277
-
<View style={[a.flex_row, a.align_center]}>
285
+
<View style={[a.flex_row, a.align_center, a.max_w_full]}>
278
286
<Text
279
287
emoji
280
288
style={[
···
343
351
)
344
352
}
345
353
354
+
export function NamePlaceholder({style}: ViewStyleProp) {
355
+
const t = useTheme()
356
+
357
+
return (
358
+
<View
359
+
style={[
360
+
a.rounded_xs,
361
+
t.atoms.bg_contrast_25,
362
+
{
363
+
width: '60%',
364
+
height: 14,
365
+
},
366
+
style,
367
+
]}
368
+
/>
369
+
)
370
+
}
371
+
346
372
export function Description({
347
373
profile: profileUnshadowed,
348
374
numberOfLines = 3,
375
+
style,
349
376
}: {
350
377
profile: bsky.profile.AnyProfileView
351
378
numberOfLines?: number
352
-
}) {
379
+
} & TextStyleProp) {
353
380
const profile = useProfileShadow(profileUnshadowed)
354
381
const rt = useMemo(() => {
355
382
if (!('description' in profile)) return
···
369
396
<View style={[a.pt_xs]}>
370
397
<RichText
371
398
value={rt}
372
-
style={[a.leading_snug]}
399
+
style={[a.leading_snug, style]}
373
400
numberOfLines={numberOfLines}
374
401
disableLinks
375
402
/>
-6
src/env.ts
-6
src/env.ts
+79
src/env/common.ts
+79
src/env/common.ts
···
1
+
import {type Did} from '@atproto/api'
2
+
3
+
import packageJson from '#/../package.json'
4
+
5
+
/**
6
+
* The semver version of the app, as defined in `package.json.`
7
+
*
8
+
* N.B. The fallback is needed for Render.com deployments
9
+
*/
10
+
export const RELEASE_VERSION: string =
11
+
process.env.EXPO_PUBLIC_RELEASE_VERSION || packageJson.version
12
+
13
+
/**
14
+
* The env the app is running in e.g. development, testflight, production
15
+
*/
16
+
export const ENV: string = process.env.EXPO_PUBLIC_ENV
17
+
18
+
/**
19
+
* Indicates whether the app is running in TestFlight
20
+
*/
21
+
export const IS_TESTFLIGHT = ENV === 'testflight'
22
+
23
+
/**
24
+
* Indicates whether the app is __DEV__
25
+
*/
26
+
export const IS_DEV = __DEV__
27
+
28
+
/**
29
+
* Indicates whether the app is __DEV__ or TestFlight
30
+
*/
31
+
export const IS_INTERNAL = IS_DEV || IS_TESTFLIGHT
32
+
33
+
/**
34
+
* The commit hash that the current bundle was made from. The user can
35
+
* see the commit hash in the app's settings along with the other version info.
36
+
* Useful for debugging/reporting.
37
+
*/
38
+
export const BUNDLE_IDENTIFIER: string =
39
+
process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER || 'dev'
40
+
41
+
/**
42
+
* This will always be in the format of YYMMDDHH, so that it always increases
43
+
* for each build. This should only be used for StatSig reporting and shouldn't
44
+
* be used to identify a specific bundle.
45
+
*/
46
+
export const BUNDLE_DATE: number = !process.env.EXPO_PUBLIC_BUNDLE_DATE
47
+
? 0
48
+
: Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
49
+
50
+
/**
51
+
* The log level for the app.
52
+
*/
53
+
export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as
54
+
| 'debug'
55
+
| 'info'
56
+
| 'warn'
57
+
| 'error'
58
+
59
+
/**
60
+
* Enable debug logs for specific logger instances
61
+
*/
62
+
export const LOG_DEBUG: string = process.env.EXPO_PUBLIC_LOG_DEBUG || ''
63
+
64
+
/**
65
+
* The DID of the chat service to proxy to
66
+
*/
67
+
export const CHAT_PROXY_DID: Did =
68
+
process.env.EXPO_PUBLIC_CHAT_PROXY_DID || 'did:web:api.bsky.chat'
69
+
70
+
/**
71
+
* Sentry DSN for telemetry
72
+
*/
73
+
export const SENTRY_DSN: string | undefined = process.env.EXPO_PUBLIC_SENTRY_DSN
74
+
75
+
/**
76
+
* Bitdrift API key. If undefined, Bitdrift should be disabled.
77
+
*/
78
+
export const BITDRIFT_API_KEY: string | undefined =
79
+
process.env.EXPO_PUBLIC_BITDRIFT_API_KEY
+19
src/env/index.ts
+19
src/env/index.ts
···
1
+
import {nativeBuildVersion} from 'expo-application'
2
+
3
+
import {BUNDLE_IDENTIFIER, IS_TESTFLIGHT, RELEASE_VERSION} from '#/env/common'
4
+
5
+
export * from '#/env/common'
6
+
7
+
/**
8
+
* The semver version of the app, specified in our `package.json`.file. On
9
+
* iOs/Android, the native build version is appended to the semver version, so
10
+
* that it can be used to identify a specific build.
11
+
*/
12
+
export const APP_VERSION = `${RELEASE_VERSION}.${nativeBuildVersion}`
13
+
14
+
/**
15
+
* The short commit hash and environment of the current bundle.
16
+
*/
17
+
export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${
18
+
__DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod'
19
+
})`
+15
src/env/index.web.ts
+15
src/env/index.web.ts
···
1
+
import {BUNDLE_IDENTIFIER, RELEASE_VERSION} from '#/env/common'
2
+
3
+
export * from '#/env/common'
4
+
5
+
/**
6
+
* The semver version of the app, specified in our `package.json`.file. On
7
+
* iOs/Android, the native build version is appended to the semver version, so
8
+
* that it can be used to identify a specific build.
9
+
*/
10
+
export const APP_VERSION = RELEASE_VERSION
11
+
12
+
/**
13
+
* The short commit hash and environment of the current bundle.
14
+
*/
15
+
export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${__DEV__ ? 'dev' : 'prod'})`
-18
src/lib/app-info.ts
-18
src/lib/app-info.ts
···
1
-
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
2
-
3
-
export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
4
-
export const IS_INTERNAL = __DEV__ || IS_TESTFLIGHT
5
-
6
-
// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
7
-
// along with the other version info. Useful for debugging/reporting.
8
-
export const BUNDLE_IDENTIFIER = process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? ''
9
-
10
-
// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used
11
-
// for Statsig reporting and shouldn't be used to identify a specific bundle.
12
-
export const BUNDLE_DATE =
13
-
IS_TESTFLIGHT || __DEV__ ? 0 : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
14
-
15
-
export const appVersion = `${nativeApplicationVersion}.${nativeBuildVersion}`
16
-
export const bundleInfo = `${BUNDLE_IDENTIFIER} (${
17
-
__DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod'
18
-
})`
-18
src/lib/app-info.web.ts
-18
src/lib/app-info.web.ts
···
1
-
import packageDotJson from '../../package.json'
2
-
3
-
export const IS_TESTFLIGHT = false
4
-
export const IS_INTERNAL = __DEV__
5
-
6
-
// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
7
-
// along with the other version info. Useful for debugging/reporting.
8
-
export const BUNDLE_IDENTIFIER =
9
-
process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? 'dev'
10
-
11
-
// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used
12
-
// for Statsig reporting and shouldn't be used to identify a specific bundle.
13
-
export const BUNDLE_DATE = __DEV__
14
-
? 0
15
-
: Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
16
-
17
-
export const appVersion = packageDotJson.version
18
-
export const bundleInfo = `${BUNDLE_IDENTIFIER} (${__DEV__ ? 'dev' : 'prod'})`
+1
-1
src/lib/hooks/useOTAUpdates.ts
+1
-1
src/lib/hooks/useOTAUpdates.ts
+9
-11
src/lib/statsig/statsig.tsx
+9
-11
src/lib/statsig/statsig.tsx
···
3
3
import {AppState, type AppStateStatus} from 'react-native'
4
4
import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
5
5
6
-
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
7
6
import {logger} from '#/logger'
8
7
import {type MetricEvents} from '#/logger/metrics'
9
8
import {isWeb} from '#/platform/detection'
10
9
import * as persisted from '#/state/persisted'
11
-
import packageDotJson from '../../../package.json'
10
+
import * as env from '#/env'
12
11
import {useSession} from '../../state/session'
13
12
import {timeout} from '../async/timeout'
14
13
import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback'
···
49
48
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
50
49
return {
51
50
environment: {
52
-
tier:
53
-
process.env.NODE_ENV === 'development'
54
-
? 'development'
55
-
: IS_TESTFLIGHT
56
-
? 'staging'
57
-
: 'production',
51
+
tier: env.IS_DEV
52
+
? 'development'
53
+
: env.IS_TESTFLIGHT
54
+
? 'staging'
55
+
: 'production',
58
56
},
59
57
// Don't block on waiting for network. The fetched config will kick in on next load.
60
58
// This ensures the UI is always consistent and doesn't update mid-session.
···
212
210
refSrc,
213
211
refUrl,
214
212
platform: Platform.OS as 'ios' | 'android' | 'web',
215
-
appVersion: packageDotJson.version,
216
-
bundleIdentifier: BUNDLE_IDENTIFIER,
217
-
bundleDate: BUNDLE_DATE,
213
+
appVersion: env.RELEASE_VERSION,
214
+
bundleIdentifier: env.BUNDLE_IDENTIFIER,
215
+
bundleDate: env.BUNDLE_DATE,
218
216
appLanguage: languagePrefs.appLanguage,
219
217
contentLanguages: languagePrefs.contentLanguages,
220
218
},
+31
-22
src/locale/locales/en/messages.po
+31
-22
src/locale/locales/en/messages.po
···
966
966
967
967
#: src/components/hooks/useFollowMethods.ts:35
968
968
#: src/components/hooks/useFollowMethods.ts:50
969
-
#: src/components/ProfileCard.tsx:457
970
-
#: src/components/ProfileCard.tsx:478
969
+
#: src/components/ProfileCard.tsx:484
970
+
#: src/components/ProfileCard.tsx:505
971
971
#: src/view/com/profile/FollowButton.tsx:38
972
972
#: src/view/com/profile/FollowButton.tsx:48
973
973
msgid "An issue occurred, please try again."
···
1262
1262
msgstr ""
1263
1263
1264
1264
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:757
1265
-
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:319
1265
+
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:320
1266
1266
#: src/view/com/profile/ProfileMenu.tsx:473
1267
1267
msgid "Block"
1268
1268
msgstr ""
···
1423
1423
msgid "Books"
1424
1424
msgstr ""
1425
1425
1426
-
#: src/components/FeedInterstitials.tsx:379
1426
+
#: src/components/FeedInterstitials.tsx:436
1427
1427
msgid "Browse more accounts on the Explore page"
1428
1428
msgstr ""
1429
1429
1430
-
#: src/components/FeedInterstitials.tsx:517
1430
+
#: src/components/FeedInterstitials.tsx:566
1431
1431
msgid "Browse more feeds on the Explore page"
1432
1432
msgstr ""
1433
1433
1434
-
#: src/components/FeedInterstitials.tsx:359
1435
-
#: src/components/FeedInterstitials.tsx:362
1436
-
#: src/components/FeedInterstitials.tsx:498
1437
-
#: src/components/FeedInterstitials.tsx:501
1434
+
#: src/components/FeedInterstitials.tsx:547
1435
+
#: src/components/FeedInterstitials.tsx:550
1438
1436
msgid "Browse more suggestions"
1439
1437
msgstr ""
1440
1438
1441
-
#: src/components/FeedInterstitials.tsx:387
1442
-
#: src/components/FeedInterstitials.tsx:526
1439
+
#: src/components/FeedInterstitials.tsx:575
1443
1440
msgid "Browse more suggestions on the Explore page"
1444
1441
msgstr ""
1445
1442
···
3618
3615
msgstr ""
3619
3616
3620
3617
#. User is not following this account, click to follow
3621
-
#: src/components/ProfileCard.tsx:490
3618
+
#: src/components/ProfileCard.tsx:517
3622
3619
#: src/components/ProfileHoverCard/index.web.tsx:494
3623
3620
#: src/components/ProfileHoverCard/index.web.tsx:505
3624
3621
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:245
···
3696
3693
msgstr ""
3697
3694
3698
3695
#. User is following this account, click to unfollow
3699
-
#: src/components/ProfileCard.tsx:484
3696
+
#: src/components/ProfileCard.tsx:511
3700
3697
#: src/components/ProfileHoverCard/index.web.tsx:493
3701
3698
#: src/components/ProfileHoverCard/index.web.tsx:504
3702
3699
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:241
···
3711
3708
msgid "Following"
3712
3709
msgstr ""
3713
3710
3714
-
#: src/components/ProfileCard.tsx:447
3711
+
#: src/components/ProfileCard.tsx:474
3715
3712
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:89
3716
3713
msgid "Following {0}"
3717
3714
msgstr ""
···
5459
5456
msgid "No likes yet"
5460
5457
msgstr ""
5461
5458
5462
-
#: src/components/ProfileCard.tsx:469
5459
+
#: src/components/ProfileCard.tsx:496
5463
5460
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:110
5464
5461
msgid "No longer following {0}"
5465
5462
msgstr ""
···
7398
7395
msgid "See jobs at Bluesky"
7399
7396
msgstr ""
7400
7397
7398
+
#: src/components/FeedInterstitials.tsx:397
7399
+
msgid "See more"
7400
+
msgstr ""
7401
+
7402
+
#: src/components/FeedInterstitials.tsx:444
7403
+
msgid "See more accounts you might like"
7404
+
msgstr ""
7405
+
7406
+
#: src/components/FeedInterstitials.tsx:395
7407
+
msgid "See more suggested profiles on the Explore page"
7408
+
msgstr ""
7409
+
7401
7410
#: src/view/screens/SavedFeeds.tsx:213
7402
7411
msgid "See this guide"
7403
7412
msgstr ""
···
7975
7984
msgid "Signed in as @{0}"
7976
7985
msgstr ""
7977
7986
7978
-
#: src/components/FeedInterstitials.tsx:343
7987
+
#: src/components/FeedInterstitials.tsx:389
7979
7988
msgid "Similar accounts"
7980
7989
msgstr ""
7981
7990
···
8005
8014
msgid "Some of your verifications are invalid."
8006
8015
msgstr ""
8007
8016
8008
-
#: src/components/FeedInterstitials.tsx:480
8017
+
#: src/components/FeedInterstitials.tsx:529
8009
8018
msgid "Some other feeds you might like"
8010
8019
msgstr ""
8011
8020
···
8237
8246
msgid "Suggested Accounts"
8238
8247
msgstr ""
8239
8248
8240
-
#: src/components/FeedInterstitials.tsx:345
8249
+
#: src/components/FeedInterstitials.tsx:391
8241
8250
msgid "Suggested for you"
8242
8251
msgstr ""
8243
8252
···
8421
8430
msgid "That's everything!"
8422
8431
msgstr ""
8423
8432
8424
-
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:315
8433
+
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:316
8425
8434
#: src/view/com/profile/ProfileMenu.tsx:461
8426
8435
msgid "The account will be able to interact with you after unblocking."
8427
8436
msgstr ""
···
9046
9055
#: src/components/dms/MessagesListBlockedFooter.tsx:112
9047
9056
#: src/components/dms/MessagesListBlockedFooter.tsx:119
9048
9057
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:203
9049
-
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:319
9058
+
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:320
9050
9059
#: src/view/com/profile/ProfileMenu.tsx:473
9051
9060
#: src/view/screens/ProfileList.tsx:723
9052
9061
msgid "Unblock"
···
9064
9073
msgid "Unblock account"
9065
9074
msgstr ""
9066
9075
9067
-
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:313
9076
+
#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:314
9068
9077
#: src/view/com/profile/ProfileMenu.tsx:455
9069
9078
msgid "Unblock Account?"
9070
9079
msgstr ""
···
9571
9580
msgid "View {0}'s avatar"
9572
9581
msgstr ""
9573
9582
9574
-
#: src/components/ProfileCard.tsx:118
9583
+
#: src/components/ProfileCard.tsx:124
9575
9584
#: src/screens/Profile/components/ProfileFeedHeader.tsx:454
9576
9585
#: src/screens/Search/components/SearchProfileCard.tsx:36
9577
9586
#: src/screens/VideoFeed/index.tsx:790
+1
-2
src/logger/bitdrift/setup/index.ts
+1
-2
src/logger/bitdrift/setup/index.ts
+2
-1
src/logger/index.ts
+2
-1
src/logger/index.ts
···
14
14
} from '#/logger/types'
15
15
import {enabledLogLevels} from '#/logger/util'
16
16
import {isNative} from '#/platform/detection'
17
+
import {ENV} from '#/env'
17
18
18
19
const TRANSPORTS: Transport[] = (function configureTransports() {
19
-
switch (process.env.NODE_ENV) {
20
+
switch (ENV) {
20
21
case 'production': {
21
22
return [sentryTransport, isNative && bitdriftTransport].filter(
22
23
Boolean,
+6
-23
src/logger/sentry/setup/index.ts
+6
-23
src/logger/sentry/setup/index.ts
···
1
-
/**
2
-
* Importing these separately from `platform/detection` and `lib/app-info` to
3
-
* avoid future conflicts and/or circular deps
4
-
*/
5
-
6
1
import {init} from '@sentry/react-native'
7
2
8
-
import pkgJson from '#/../package.json'
9
-
10
-
/**
11
-
* Examples:
12
-
* - `dev`
13
-
* - `1.99.0`
14
-
*/
15
-
const release = process.env.SENTRY_RELEASE || pkgJson.version
16
-
17
-
/**
18
-
* The latest deployed commit hash
19
-
*/
20
-
const dist = process.env.SENTRY_DIST || 'dev'
3
+
import * as env from '#/env'
21
4
22
5
init({
23
-
enabled: !__DEV__ && !!process.env.SENTRY_DSN,
6
+
enabled: !env.IS_DEV && !!env.SENTRY_DSN,
24
7
autoSessionTracking: false,
25
-
dsn: process.env.SENTRY_DSN,
8
+
dsn: env.SENTRY_DSN,
26
9
debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production
27
-
environment: process.env.NODE_ENV,
28
-
dist,
29
-
release,
10
+
environment: env.ENV,
11
+
dist: env.BUNDLE_IDENTIFIER,
12
+
release: env.RELEASE_VERSION,
30
13
ignoreErrors: [
31
14
/*
32
15
* Unknown internals errors
+5
-5
src/screens/Settings/AboutSettings.tsx
+5
-5
src/screens/Settings/AboutSettings.tsx
···
9
9
import {useMutation} from '@tanstack/react-query'
10
10
import {Statsig} from 'statsig-react-native-expo'
11
11
12
-
import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info'
13
12
import {STATUS_PAGE_URL} from '#/lib/constants'
14
13
import {type CommonNavigatorParams} from '#/lib/routes/types'
15
14
import {isAndroid, isIOS, isNative} from '#/platform/detection'
···
23
22
import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
24
23
import * as Layout from '#/components/Layout'
25
24
import {Loader} from '#/components/Loader'
25
+
import * as env from '#/env'
26
26
import {useDemoMode} from '#/storage/hooks/demo-mode'
27
27
import {useDevMode} from '#/storage/hooks/dev-mode'
28
28
import {OTAInfo} from './components/OTAInfo'
···
123
123
</SettingsList.PressableItem>
124
124
)}
125
125
<SettingsList.PressableItem
126
-
label={_(msg`Version ${appVersion}`)}
126
+
label={_(msg`Version ${env.APP_VERSION}`)}
127
127
accessibilityHint={_(msg`Copies build version to clipboard`)}
128
128
onLongPress={() => {
129
129
const newDevModeEnabled = !devModeEnabled
···
146
146
}}
147
147
onPress={() => {
148
148
setStringAsync(
149
-
`Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`,
149
+
`Build version: ${env.APP_VERSION}; Bundle info: ${env.APP_METADATA}; Bundle date: ${env.BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`,
150
150
)
151
151
Toast.show(_(msg`Copied build version to clipboard`))
152
152
}}>
153
153
<SettingsList.ItemIcon icon={WrenchIcon} />
154
154
<SettingsList.ItemText>
155
-
<Trans>Version {appVersion}</Trans>
155
+
<Trans>Version {env.APP_VERSION}</Trans>
156
156
</SettingsList.ItemText>
157
-
<SettingsList.BadgeText>{bundleInfo}</SettingsList.BadgeText>
157
+
<SettingsList.BadgeText>{env.APP_METADATA}</SettingsList.BadgeText>
158
158
</SettingsList.PressableItem>
159
159
{devModeEnabled && (
160
160
<>
+4
-4
src/screens/Settings/AppIconSettings/index.tsx
+4
-4
src/screens/Settings/AppIconSettings/index.tsx
···
3
3
import {msg, Trans} from '@lingui/macro'
4
4
import {useLingui} from '@lingui/react'
5
5
import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
6
-
import {NativeStackScreenProps} from '@react-navigation/native-stack'
6
+
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
7
7
8
-
import {IS_INTERNAL} from '#/lib/app-info'
9
8
import {PressableScale} from '#/lib/custom-animations/PressableScale'
10
-
import {CommonNavigatorParams} from '#/lib/routes/types'
9
+
import {type CommonNavigatorParams} from '#/lib/routes/types'
11
10
import {useGate} from '#/lib/statsig/statsig'
12
11
import {isAndroid} from '#/platform/detection'
13
12
import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
14
-
import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
13
+
import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types'
15
14
import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
16
15
import {atoms as a, useTheme} from '#/alf'
17
16
import * as Toggle from '#/components/forms/Toggle'
18
17
import * as Layout from '#/components/Layout'
19
18
import {Text} from '#/components/Typography'
19
+
import {IS_INTERNAL} from '#/env'
20
20
21
21
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
22
22
export function AppIconSettingsScreen({}: Props) {
+1
-1
src/screens/Settings/AppearanceSettings.tsx
+1
-1
src/screens/Settings/AppearanceSettings.tsx
···
8
8
import {msg, Trans} from '@lingui/macro'
9
9
import {useLingui} from '@lingui/react'
10
10
11
-
import {IS_INTERNAL} from '#/lib/app-info'
12
11
import {
13
12
type CommonNavigatorParams,
14
13
type NativeStackScreenProps,
···
26
25
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
27
26
import * as Layout from '#/components/Layout'
28
27
import {Text} from '#/components/Typography'
28
+
import {IS_INTERNAL} from '#/env'
29
29
import * as SettingsList from './components/SettingsList'
30
30
31
31
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
+1
-1
src/screens/Settings/Settings.tsx
+1
-1
src/screens/Settings/Settings.tsx
···
9
9
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
10
10
11
11
import {useActorStatus} from '#/lib/actor-status'
12
-
import {IS_INTERNAL} from '#/lib/app-info'
13
12
import {HELP_DESK_URL} from '#/lib/constants'
14
13
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
15
14
import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
···
66
65
shouldShowVerificationCheckButton,
67
66
VerificationCheckButton,
68
67
} from '#/components/verification/VerificationCheckButton'
68
+
import {IS_INTERNAL} from '#/env'
69
69
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
70
70
71
71
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
+3
-1
src/state/queries/messages/const.ts
+3
-1
src/state/queries/messages/const.ts
+5
-5
src/state/session/logging.ts
+5
-5
src/state/session/logging.ts
···
1
-
import {AtpSessionData, AtpSessionEvent} from '@atproto/api'
1
+
import {type AtpSessionData, type AtpSessionEvent} from '@atproto/api'
2
2
import {sha256} from 'js-sha256'
3
3
import {Statsig} from 'statsig-react-native-expo'
4
4
5
-
import {IS_INTERNAL} from '#/lib/app-info'
6
-
import {Schema} from '../persisted'
7
-
import {Action, State} from './reducer'
8
-
import {SessionAccount} from './types'
5
+
import {IS_INTERNAL} from '#/env'
6
+
import {type Schema} from '../persisted'
7
+
import {type Action, type State} from './reducer'
8
+
import {type SessionAccount} from './types'
9
9
10
10
type Reducer = (state: State, action: Action) => State
11
11
+8
-14
src/view/com/util/Link.tsx
+8
-14
src/view/com/util/Link.tsx
···
101
101
{name: 'activate', label: title},
102
102
]
103
103
104
-
const dataSet = useMemo(() => {
105
-
const ds = {...dataSetProp}
106
-
if (anchorNoUnderline) {
107
-
ds.noUnderline = 1
108
-
}
109
-
return ds
110
-
}, [dataSetProp, anchorNoUnderline])
104
+
const dataSet = anchorNoUnderline
105
+
? {...dataSetProp, noUnderline: 1}
106
+
: dataSetProp
111
107
112
108
if (noFeedback) {
113
109
return (
···
125
121
onAccessibilityAction?.(e)
126
122
}
127
123
}}
124
+
// @ts-ignore web only -sfn
125
+
dataSet={dataSet}
128
126
{...props}
129
127
android_ripple={{
130
128
color: t.atoms.bg_contrast_25.backgroundColor,
···
198
196
console.error('Unable to detect mismatching label')
199
197
}
200
198
201
-
const dataSet = useMemo(() => {
202
-
const ds = {...dataSetProp}
203
-
if (anchorNoUnderline) {
204
-
ds.noUnderline = 1
205
-
}
206
-
return ds
207
-
}, [dataSetProp, anchorNoUnderline])
199
+
const dataSet = anchorNoUnderline
200
+
? {...dataSetProp, noUnderline: 1}
201
+
: dataSetProp
208
202
209
203
const onPress = useCallback(
210
204
(e?: Event) => {
+201
src/view/com/util/Toast.style.tsx
+201
src/view/com/util/Toast.style.tsx
···
1
+
import {select, type Theme} from '#/alf'
2
+
import {Check_Stroke2_Corner0_Rounded as SuccessIcon} from '#/components/icons/Check'
3
+
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
4
+
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
5
+
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
6
+
7
+
export type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info'
8
+
9
+
export type LegacyToastType =
10
+
| 'xmark'
11
+
| 'exclamation-circle'
12
+
| 'check'
13
+
| 'clipboard-check'
14
+
| 'circle-exclamation'
15
+
16
+
export const convertLegacyToastType = (
17
+
type: ToastType | LegacyToastType,
18
+
): ToastType => {
19
+
switch (type) {
20
+
// these ones are fine
21
+
case 'default':
22
+
case 'success':
23
+
case 'error':
24
+
case 'warning':
25
+
case 'info':
26
+
return type
27
+
// legacy ones need conversion
28
+
case 'xmark':
29
+
return 'error'
30
+
case 'exclamation-circle':
31
+
return 'warning'
32
+
case 'check':
33
+
return 'success'
34
+
case 'clipboard-check':
35
+
return 'success'
36
+
case 'circle-exclamation':
37
+
return 'warning'
38
+
default:
39
+
return 'default'
40
+
}
41
+
}
42
+
43
+
export const TOAST_ANIMATION_CONFIG = {
44
+
duration: 300,
45
+
damping: 15,
46
+
stiffness: 150,
47
+
mass: 0.8,
48
+
overshootClamping: false,
49
+
restSpeedThreshold: 0.01,
50
+
restDisplacementThreshold: 0.01,
51
+
}
52
+
53
+
export const TOAST_TYPE_TO_ICON = {
54
+
default: SuccessIcon,
55
+
success: SuccessIcon,
56
+
error: ErrorIcon,
57
+
warning: WarningIcon,
58
+
info: CircleInfo,
59
+
}
60
+
61
+
export const getToastTypeStyles = (t: Theme) => ({
62
+
default: {
63
+
backgroundColor: select(t.name, {
64
+
light: t.atoms.bg_contrast_25.backgroundColor,
65
+
dim: t.atoms.bg_contrast_100.backgroundColor,
66
+
dark: t.atoms.bg_contrast_100.backgroundColor,
67
+
}),
68
+
borderColor: select(t.name, {
69
+
light: t.atoms.border_contrast_low.borderColor,
70
+
dim: t.atoms.border_contrast_high.borderColor,
71
+
dark: t.atoms.border_contrast_high.borderColor,
72
+
}),
73
+
iconColor: select(t.name, {
74
+
light: t.atoms.text_contrast_medium.color,
75
+
dim: t.atoms.text_contrast_medium.color,
76
+
dark: t.atoms.text_contrast_medium.color,
77
+
}),
78
+
textColor: select(t.name, {
79
+
light: t.atoms.text_contrast_medium.color,
80
+
dim: t.atoms.text_contrast_medium.color,
81
+
dark: t.atoms.text_contrast_medium.color,
82
+
}),
83
+
},
84
+
success: {
85
+
backgroundColor: select(t.name, {
86
+
light: t.palette.primary_100,
87
+
dim: t.palette.primary_100,
88
+
dark: t.palette.primary_50,
89
+
}),
90
+
borderColor: select(t.name, {
91
+
light: t.palette.primary_500,
92
+
dim: t.palette.primary_500,
93
+
dark: t.palette.primary_500,
94
+
}),
95
+
iconColor: select(t.name, {
96
+
light: t.palette.primary_500,
97
+
dim: t.palette.primary_600,
98
+
dark: t.palette.primary_600,
99
+
}),
100
+
textColor: select(t.name, {
101
+
light: t.palette.primary_500,
102
+
dim: t.palette.primary_600,
103
+
dark: t.palette.primary_600,
104
+
}),
105
+
},
106
+
error: {
107
+
backgroundColor: select(t.name, {
108
+
light: t.palette.negative_200,
109
+
dim: t.palette.negative_25,
110
+
dark: t.palette.negative_25,
111
+
}),
112
+
borderColor: select(t.name, {
113
+
light: t.palette.negative_300,
114
+
dim: t.palette.negative_300,
115
+
dark: t.palette.negative_300,
116
+
}),
117
+
iconColor: select(t.name, {
118
+
light: t.palette.negative_600,
119
+
dim: t.palette.negative_600,
120
+
dark: t.palette.negative_600,
121
+
}),
122
+
textColor: select(t.name, {
123
+
light: t.palette.negative_600,
124
+
dim: t.palette.negative_600,
125
+
dark: t.palette.negative_600,
126
+
}),
127
+
},
128
+
warning: {
129
+
backgroundColor: select(t.name, {
130
+
light: t.atoms.bg_contrast_25.backgroundColor,
131
+
dim: t.atoms.bg_contrast_100.backgroundColor,
132
+
dark: t.atoms.bg_contrast_100.backgroundColor,
133
+
}),
134
+
borderColor: select(t.name, {
135
+
light: t.atoms.border_contrast_low.borderColor,
136
+
dim: t.atoms.border_contrast_high.borderColor,
137
+
dark: t.atoms.border_contrast_high.borderColor,
138
+
}),
139
+
iconColor: select(t.name, {
140
+
light: t.atoms.text_contrast_medium.color,
141
+
dim: t.atoms.text_contrast_medium.color,
142
+
dark: t.atoms.text_contrast_medium.color,
143
+
}),
144
+
textColor: select(t.name, {
145
+
light: t.atoms.text_contrast_medium.color,
146
+
dim: t.atoms.text_contrast_medium.color,
147
+
dark: t.atoms.text_contrast_medium.color,
148
+
}),
149
+
},
150
+
info: {
151
+
backgroundColor: select(t.name, {
152
+
light: t.atoms.bg_contrast_25.backgroundColor,
153
+
dim: t.atoms.bg_contrast_100.backgroundColor,
154
+
dark: t.atoms.bg_contrast_100.backgroundColor,
155
+
}),
156
+
borderColor: select(t.name, {
157
+
light: t.atoms.border_contrast_low.borderColor,
158
+
dim: t.atoms.border_contrast_high.borderColor,
159
+
dark: t.atoms.border_contrast_high.borderColor,
160
+
}),
161
+
iconColor: select(t.name, {
162
+
light: t.atoms.text_contrast_medium.color,
163
+
dim: t.atoms.text_contrast_medium.color,
164
+
dark: t.atoms.text_contrast_medium.color,
165
+
}),
166
+
textColor: select(t.name, {
167
+
light: t.atoms.text_contrast_medium.color,
168
+
dim: t.atoms.text_contrast_medium.color,
169
+
dark: t.atoms.text_contrast_medium.color,
170
+
}),
171
+
},
172
+
})
173
+
174
+
export const getToastWebAnimationStyles = () => ({
175
+
entering: {
176
+
animation: 'toastFadeIn 0.3s ease-out forwards',
177
+
},
178
+
exiting: {
179
+
animation: 'toastFadeOut 0.2s ease-in forwards',
180
+
},
181
+
})
182
+
183
+
export const TOAST_WEB_KEYFRAMES = `
184
+
@keyframes toastFadeIn {
185
+
from {
186
+
opacity: 0;
187
+
}
188
+
to {
189
+
opacity: 1;
190
+
}
191
+
}
192
+
193
+
@keyframes toastFadeOut {
194
+
from {
195
+
opacity: 1;
196
+
}
197
+
to {
198
+
opacity: 0;
199
+
}
200
+
}
201
+
`
+78
-59
src/view/com/util/Toast.tsx
+78
-59
src/view/com/util/Toast.tsx
···
6
6
GestureHandlerRootView,
7
7
} from 'react-native-gesture-handler'
8
8
import Animated, {
9
-
FadeInUp,
10
-
FadeOutUp,
9
+
FadeIn,
10
+
FadeOut,
11
11
runOnJS,
12
12
useAnimatedReaction,
13
13
useAnimatedStyle,
···
17
17
} from 'react-native-reanimated'
18
18
import RootSiblings from 'react-native-root-siblings'
19
19
import {useSafeAreaInsets} from 'react-native-safe-area-context'
20
-
import {
21
-
FontAwesomeIcon,
22
-
type Props as FontAwesomeProps,
23
-
} from '@fortawesome/react-native-fontawesome'
24
20
25
21
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
22
+
import {
23
+
convertLegacyToastType,
24
+
getToastTypeStyles,
25
+
type LegacyToastType,
26
+
TOAST_ANIMATION_CONFIG,
27
+
TOAST_TYPE_TO_ICON,
28
+
type ToastType,
29
+
} from '#/view/com/util/Toast.style'
26
30
import {atoms as a, useTheme} from '#/alf'
27
31
import {Text} from '#/components/Typography'
28
32
29
33
const TIMEOUT = 2e3
30
34
35
+
// Use type overloading to mark certain types as deprecated -sfn
36
+
// https://stackoverflow.com/a/78325851/13325987
37
+
export function show(message: string, type?: ToastType): void
38
+
/**
39
+
* @deprecated type is deprecated - use one of `'default' | 'success' | 'error' | 'warning' | 'info'`
40
+
*/
41
+
export function show(message: string, type?: LegacyToastType): void
31
42
export function show(
32
43
message: string,
33
-
icon: FontAwesomeProps['icon'] = 'check',
34
-
) {
44
+
type: ToastType | LegacyToastType = 'default',
45
+
): void {
35
46
if (process.env.NODE_ENV === 'test') {
36
47
return
37
48
}
49
+
38
50
AccessibilityInfo.announceForAccessibility(message)
39
51
const item = new RootSiblings(
40
-
<Toast message={message} icon={icon} destroy={() => item.destroy()} />,
52
+
(
53
+
<Toast
54
+
message={message}
55
+
type={convertLegacyToastType(type)}
56
+
destroy={() => item.destroy()}
57
+
/>
58
+
),
41
59
)
42
60
}
43
61
44
62
function Toast({
45
63
message,
46
-
icon,
64
+
type,
47
65
destroy,
48
66
}: {
49
67
message: string
50
-
icon: FontAwesomeProps['icon']
68
+
type: ToastType
51
69
destroy: () => void
52
70
}) {
53
71
const t = useTheme()
···
55
73
const isPanning = useSharedValue(false)
56
74
const dismissSwipeTranslateY = useSharedValue(0)
57
75
const [cardHeight, setCardHeight] = useState(0)
76
+
77
+
const toastStyles = getToastTypeStyles(t)
78
+
const colors = toastStyles[type]
79
+
const IconComponent = TOAST_TYPE_TO_ICON[type]
58
80
59
81
// for the exit animation to work on iOS the animated component
60
82
// must not be the root component
···
159
181
pointerEvents="box-none">
160
182
{alive && (
161
183
<Animated.View
162
-
entering={FadeInUp}
163
-
exiting={FadeOutUp}
164
-
style={[a.flex_1]}>
165
-
<Animated.View
166
-
onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)}
167
-
accessibilityRole="alert"
168
-
accessible={true}
169
-
accessibilityLabel={message}
170
-
accessibilityHint=""
171
-
onAccessibilityEscape={hideAndDestroyImmediately}
172
-
style={[
173
-
a.flex_1,
174
-
t.name === 'dark' ? t.atoms.bg_contrast_25 : t.atoms.bg,
175
-
a.shadow_lg,
176
-
t.atoms.border_contrast_medium,
177
-
a.rounded_sm,
178
-
a.border,
179
-
animatedStyle,
180
-
]}>
181
-
<GestureDetector gesture={panGesture}>
182
-
<View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}>
183
-
<View
184
-
style={[
185
-
a.flex_shrink_0,
186
-
a.rounded_full,
187
-
{width: 32, height: 32},
188
-
a.align_center,
189
-
a.justify_center,
190
-
{
191
-
backgroundColor:
192
-
t.name === 'dark'
193
-
? t.palette.black
194
-
: t.palette.primary_50,
195
-
},
196
-
]}>
197
-
<FontAwesomeIcon
198
-
icon={icon}
199
-
size={16}
200
-
style={t.atoms.text_contrast_medium}
201
-
/>
202
-
</View>
203
-
<View style={[a.h_full, a.justify_center, a.flex_1]}>
204
-
<Text style={a.text_md} emoji>
205
-
{message}
206
-
</Text>
207
-
</View>
184
+
entering={FadeIn.duration(TOAST_ANIMATION_CONFIG.duration)}
185
+
exiting={FadeOut.duration(TOAST_ANIMATION_CONFIG.duration * 0.7)}
186
+
onLayout={evt => setCardHeight(evt.nativeEvent.layout.height)}
187
+
accessibilityRole="alert"
188
+
accessible={true}
189
+
accessibilityLabel={message}
190
+
accessibilityHint=""
191
+
onAccessibilityEscape={hideAndDestroyImmediately}
192
+
style={[
193
+
a.flex_1,
194
+
{backgroundColor: colors.backgroundColor},
195
+
a.shadow_sm,
196
+
{borderColor: colors.borderColor, borderWidth: 1},
197
+
a.rounded_sm,
198
+
animatedStyle,
199
+
]}>
200
+
<GestureDetector gesture={panGesture}>
201
+
<View style={[a.flex_1, a.px_md, a.py_lg, a.flex_row, a.gap_md]}>
202
+
<View
203
+
style={[
204
+
a.flex_shrink_0,
205
+
a.rounded_full,
206
+
{width: 32, height: 32},
207
+
a.align_center,
208
+
a.justify_center,
209
+
{
210
+
backgroundColor: colors.backgroundColor,
211
+
},
212
+
]}>
213
+
<IconComponent fill={colors.iconColor} size="sm" />
208
214
</View>
209
-
</GestureDetector>
210
-
</Animated.View>
215
+
<View
216
+
style={[
217
+
a.h_full,
218
+
a.justify_center,
219
+
a.flex_1,
220
+
a.justify_center,
221
+
]}>
222
+
<Text
223
+
style={[a.text_md, a.font_bold, {color: colors.textColor}]}
224
+
emoji>
225
+
{message}
226
+
</Text>
227
+
</View>
228
+
</View>
229
+
</GestureDetector>
211
230
</Animated.View>
212
231
)}
213
232
</GestureHandlerRootView>
+97
-21
src/view/com/util/Toast.web.tsx
+97
-21
src/view/com/util/Toast.web.tsx
···
4
4
5
5
import {useEffect, useState} from 'react'
6
6
import {Pressable, StyleSheet, Text, View} from 'react-native'
7
+
7
8
import {
8
-
FontAwesomeIcon,
9
-
type FontAwesomeIconStyle,
10
-
type Props as FontAwesomeProps,
11
-
} from '@fortawesome/react-native-fontawesome'
9
+
convertLegacyToastType,
10
+
getToastTypeStyles,
11
+
getToastWebAnimationStyles,
12
+
type LegacyToastType,
13
+
TOAST_TYPE_TO_ICON,
14
+
TOAST_WEB_KEYFRAMES,
15
+
type ToastType,
16
+
} from '#/view/com/util/Toast.style'
17
+
import {atoms as a, useTheme} from '#/alf'
12
18
13
19
const DURATION = 3500
14
20
15
21
interface ActiveToast {
16
22
text: string
17
-
icon: FontAwesomeProps['icon']
23
+
type: ToastType
18
24
}
19
25
type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
20
26
···
28
34
type ToastContainerProps = {}
29
35
export const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
30
36
const [activeToast, setActiveToast] = useState<ActiveToast | undefined>()
37
+
const [isExiting, setIsExiting] = useState(false)
38
+
31
39
useEffect(() => {
32
40
globalSetActiveToast = (t: ActiveToast | undefined) => {
33
-
setActiveToast(t)
41
+
if (!t && activeToast) {
42
+
setIsExiting(true)
43
+
setTimeout(() => {
44
+
setActiveToast(t)
45
+
setIsExiting(false)
46
+
}, 200)
47
+
} else {
48
+
setActiveToast(t)
49
+
setIsExiting(false)
50
+
}
51
+
}
52
+
}, [activeToast])
53
+
54
+
useEffect(() => {
55
+
const styleId = 'toast-animations'
56
+
if (!document.getElementById(styleId)) {
57
+
const style = document.createElement('style')
58
+
style.id = styleId
59
+
style.textContent = TOAST_WEB_KEYFRAMES
60
+
document.head.appendChild(style)
34
61
}
35
-
})
62
+
}, [])
63
+
64
+
const t = useTheme()
65
+
66
+
const toastTypeStyles = getToastTypeStyles(t)
67
+
const toastStyles = activeToast
68
+
? toastTypeStyles[activeToast.type]
69
+
: toastTypeStyles.default
70
+
71
+
const IconComponent = activeToast
72
+
? TOAST_TYPE_TO_ICON[activeToast.type]
73
+
: TOAST_TYPE_TO_ICON.default
74
+
75
+
const animationStyles = getToastWebAnimationStyles()
76
+
36
77
return (
37
78
<>
38
79
{activeToast && (
39
-
<View style={styles.container}>
40
-
<FontAwesomeIcon
41
-
icon={activeToast.icon}
42
-
size={20}
43
-
style={styles.icon as FontAwesomeIconStyle}
44
-
/>
45
-
<Text style={styles.text}>{activeToast.text}</Text>
80
+
<View
81
+
style={[
82
+
styles.container,
83
+
{
84
+
backgroundColor: toastStyles.backgroundColor,
85
+
borderColor: toastStyles.borderColor,
86
+
...(isExiting
87
+
? animationStyles.exiting
88
+
: animationStyles.entering),
89
+
},
90
+
]}>
91
+
<View
92
+
style={[
93
+
styles.iconContainer,
94
+
{
95
+
backgroundColor: 'transparent',
96
+
},
97
+
]}>
98
+
<IconComponent
99
+
fill={toastStyles.iconColor}
100
+
size="sm"
101
+
style={styles.icon}
102
+
/>
103
+
</View>
104
+
<Text
105
+
style={[
106
+
styles.text,
107
+
a.text_sm,
108
+
a.font_bold,
109
+
{color: toastStyles.textColor},
110
+
]}>
111
+
{activeToast.text}
112
+
</Text>
46
113
<Pressable
47
114
style={styles.dismissBackdrop}
48
115
accessibilityLabel="Dismiss"
···
60
127
// methods
61
128
// =
62
129
63
-
export function show(text: string, icon: FontAwesomeProps['icon'] = 'check') {
130
+
export function show(
131
+
text: string,
132
+
type: ToastType | LegacyToastType = 'default',
133
+
) {
64
134
if (toastTimeout) {
65
135
clearTimeout(toastTimeout)
66
136
}
67
-
globalSetActiveToast?.({text, icon})
137
+
138
+
globalSetActiveToast?.({text, type: convertLegacyToastType(type)})
68
139
toastTimeout = setTimeout(() => {
69
140
globalSetActiveToast?.(undefined)
70
141
}, DURATION)
···
78
149
bottom: 20,
79
150
// @ts-ignore web only
80
151
width: 'calc(100% - 40px)',
81
-
maxWidth: 350,
152
+
maxWidth: 380,
82
153
padding: 20,
83
154
flexDirection: 'row',
84
155
alignItems: 'center',
85
-
backgroundColor: '#000c',
86
156
borderRadius: 10,
157
+
borderWidth: 1,
87
158
},
88
159
dismissBackdrop: {
89
160
position: 'absolute',
···
92
163
bottom: 0,
93
164
right: 0,
94
165
},
166
+
iconContainer: {
167
+
width: 32,
168
+
height: 32,
169
+
borderRadius: 16,
170
+
alignItems: 'center',
171
+
justifyContent: 'center',
172
+
flexShrink: 0,
173
+
},
95
174
icon: {
96
-
color: '#fff',
97
175
flexShrink: 0,
98
176
},
99
177
text: {
100
-
color: '#fff',
101
-
fontSize: 18,
102
178
marginLeft: 10,
103
179
},
104
180
})
+102
src/view/screens/Storybook/Toasts.tsx
+102
src/view/screens/Storybook/Toasts.tsx
···
1
+
import {Pressable, View} from 'react-native'
2
+
3
+
import * as Toast from '#/view/com/util/Toast'
4
+
import {
5
+
getToastTypeStyles,
6
+
TOAST_TYPE_TO_ICON,
7
+
type ToastType,
8
+
} from '#/view/com/util/Toast.style'
9
+
import {atoms as a, useTheme} from '#/alf'
10
+
import {H1, Text} from '#/components/Typography'
11
+
12
+
function ToastPreview({message, type}: {message: string; type: ToastType}) {
13
+
const t = useTheme()
14
+
const toastStyles = getToastTypeStyles(t)
15
+
const colors = toastStyles[type as keyof typeof toastStyles]
16
+
const IconComponent =
17
+
TOAST_TYPE_TO_ICON[type as keyof typeof TOAST_TYPE_TO_ICON]
18
+
19
+
return (
20
+
<Pressable
21
+
accessibilityRole="button"
22
+
onPress={() => Toast.show(message, type)}
23
+
style={[
24
+
{backgroundColor: colors.backgroundColor},
25
+
a.shadow_sm,
26
+
{borderColor: colors.borderColor},
27
+
a.rounded_sm,
28
+
a.border,
29
+
a.px_sm,
30
+
a.py_sm,
31
+
a.flex_row,
32
+
a.gap_sm,
33
+
a.align_center,
34
+
]}>
35
+
<View
36
+
style={[
37
+
a.flex_shrink_0,
38
+
a.rounded_full,
39
+
{width: 24, height: 24},
40
+
a.align_center,
41
+
a.justify_center,
42
+
{
43
+
backgroundColor: colors.backgroundColor,
44
+
},
45
+
]}>
46
+
<IconComponent fill={colors.iconColor} size="xs" />
47
+
</View>
48
+
<View style={[a.flex_1]}>
49
+
<Text
50
+
style={[
51
+
a.text_sm,
52
+
a.font_bold,
53
+
a.leading_snug,
54
+
{color: colors.textColor},
55
+
]}
56
+
emoji>
57
+
{message}
58
+
</Text>
59
+
</View>
60
+
</Pressable>
61
+
)
62
+
}
63
+
64
+
export function Toasts() {
65
+
return (
66
+
<View style={[a.gap_md]}>
67
+
<H1>Toast Examples</H1>
68
+
69
+
<View style={[a.gap_md]}>
70
+
<View style={[a.gap_xs]}>
71
+
<ToastPreview message="Default Toast" type="default" />
72
+
</View>
73
+
74
+
<View style={[a.gap_xs]}>
75
+
<ToastPreview
76
+
message="Operation completed successfully!"
77
+
type="success"
78
+
/>
79
+
</View>
80
+
81
+
<View style={[a.gap_xs]}>
82
+
<ToastPreview message="Something went wrong!" type="error" />
83
+
</View>
84
+
85
+
<View style={[a.gap_xs]}>
86
+
<ToastPreview message="Please check your input" type="warning" />
87
+
</View>
88
+
89
+
<View style={[a.gap_xs]}>
90
+
<ToastPreview message="Here's some helpful information" type="info" />
91
+
</View>
92
+
93
+
<View style={[a.gap_xs]}>
94
+
<ToastPreview
95
+
message="This is a longer message to test how the toast handles multiple lines of text content."
96
+
type="info"
97
+
/>
98
+
</View>
99
+
</View>
100
+
</View>
101
+
)
102
+
}
+2
src/view/screens/Storybook/index.tsx
+2
src/view/screens/Storybook/index.tsx
···
20
20
import {Shadows} from './Shadows'
21
21
import {Spacing} from './Spacing'
22
22
import {Theming} from './Theming'
23
+
import {Toasts} from './Toasts'
23
24
import {Typography} from './Typography'
24
25
25
26
export function Storybook() {
···
122
123
<Breakpoints />
123
124
<Dialogs />
124
125
<Admonitions />
126
+
<Toasts />
125
127
<Settings />
126
128
127
129
<Button
+1
-1
webpack.config.js
+1
-1
webpack.config.js