about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-11-27 14:41:18 +0000
committerGitHub <noreply@github.com>2024-11-27 14:41:18 +0000
commit6a29296ae0a1207a54323cc20fad1acdec8cba03 (patch)
treeccfbbe623158ebf57f1d45ccba684a4ca2c12402
parentc7d57b63744db040e6790625de7029afdd7387eb (diff)
downloadvoidsky-6a29296ae0a1207a54323cc20fad1acdec8cba03.tar.zst
[Subs] Custom app icons (#6758)
* custom icons

* rm default

* clouds.jpg

* use cross-platform fork

* minor fixes for android

* update dynamic icon lib

* gate app icon settings behind discover debug dids

* rename clouds

* Bop it

* Update default ios icon as well

* Remove old icon

* Update logo placement

* update to latest expo-dynamic-app-icon

* fix android icon sizes

---------

Co-authored-by: Eric Bailey <git@esb.lol>
-rw-r--r--app.config.js71
-rw-r--r--assets/app-icons/android_icon_core_aurora.pngbin0 -> 801482 bytes
-rw-r--r--assets/app-icons/android_icon_core_bonfire.pngbin0 -> 730829 bytes
-rw-r--r--assets/app-icons/android_icon_core_classic.pngbin0 -> 969534 bytes
-rw-r--r--assets/app-icons/android_icon_core_flat_black.pngbin0 -> 16745 bytes
-rw-r--r--assets/app-icons/android_icon_core_flat_blue.pngbin0 -> 16622 bytes
-rw-r--r--assets/app-icons/android_icon_core_flat_white.pngbin0 -> 16561 bytes
-rw-r--r--assets/app-icons/android_icon_core_midnight.pngbin0 -> 479078 bytes
-rw-r--r--assets/app-icons/android_icon_core_sunrise.pngbin0 -> 576891 bytes
-rw-r--r--assets/app-icons/android_icon_core_sunset.pngbin0 -> 687062 bytes
-rw-r--r--assets/app-icons/android_icon_default_dark.pngbin0 -> 478572 bytes
-rw-r--r--assets/app-icons/android_icon_default_light.pngbin0 -> 442330 bytes
-rw-r--r--assets/app-icons/ios_icon_core_aurora.pngbin0 -> 522881 bytes
-rw-r--r--assets/app-icons/ios_icon_core_bonfire.pngbin0 -> 506845 bytes
-rw-r--r--assets/app-icons/ios_icon_core_classic.pngbin0 -> 969534 bytes
-rw-r--r--assets/app-icons/ios_icon_core_flat_black.pngbin0 -> 18912 bytes
-rw-r--r--assets/app-icons/ios_icon_core_flat_blue.pngbin0 -> 18585 bytes
-rw-r--r--assets/app-icons/ios_icon_core_flat_white.pngbin0 -> 18432 bytes
-rw-r--r--assets/app-icons/ios_icon_core_midnight.pngbin0 -> 388446 bytes
-rw-r--r--assets/app-icons/ios_icon_core_sunrise.pngbin0 -> 357692 bytes
-rw-r--r--assets/app-icons/ios_icon_core_sunset.pngbin0 -> 416566 bytes
-rw-r--r--assets/app-icons/ios_icon_default_dark.pngbin0 -> 397927 bytes
-rw-r--r--assets/app-icons/ios_icon_default_light.pngbin0 -> 328839 bytes
-rw-r--r--assets/icon.pngbin462407 -> 0 bytes
-rw-r--r--bskyweb/cmd/bskyweb/server.go1
-rw-r--r--package.json1
-rw-r--r--src/Navigation.tsx9
-rw-r--r--src/alf/atoms.ts8
-rw-r--r--src/lib/constants.ts1
-rw-r--r--src/lib/routes/types.ts1
-rw-r--r--src/routes.ts1
-rw-r--r--src/screens/Settings/AppIconSettings.tsx252
-rw-r--r--src/screens/Settings/AppearanceSettings.tsx24
-rw-r--r--yarn.lock18
34 files changed, 382 insertions, 5 deletions
diff --git a/app.config.js b/app.config.js
index 149b949e6..fd754a50f 100644
--- a/app.config.js
+++ b/app.config.js
@@ -71,7 +71,7 @@ module.exports = function (config) {
         policy: 'appVersion',
       },
       orientation: 'portrait',
-      icon: './assets/icon.png',
+      icon: './assets/app-icons/ios_icon_default_light.png',
       userInterfaceStyle: 'automatic',
       splash: SPLASH_CONFIG,
       // hsl(211, 99%, 53%), same as palette.default.brandText
@@ -162,7 +162,7 @@ module.exports = function (config) {
         backgroundColor: DARK_SPLASH_CONFIG_ANDROID.backgroundColor,
       },
       android: {
-        icon: './assets/icon.png',
+        icon: './assets/app-icons/android_icon_default_light.png',
         adaptiveIcon: {
           foregroundImage: './assets/icon-android-foreground.png',
           monochromeImage: './assets/icon-android-foreground.png',
@@ -273,6 +273,73 @@ module.exports = function (config) {
             ],
           },
         ],
+        [
+          '@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: {
diff --git a/assets/app-icons/android_icon_core_aurora.png b/assets/app-icons/android_icon_core_aurora.png
new file mode 100644
index 000000000..52ce4045b
--- /dev/null
+++ b/assets/app-icons/android_icon_core_aurora.png
Binary files differdiff --git a/assets/app-icons/android_icon_core_bonfire.png b/assets/app-icons/android_icon_core_bonfire.png
new file mode 100644
index 000000000..18ed29112
--- /dev/null
+++ b/assets/app-icons/android_icon_core_bonfire.png
Binary files differdiff --git a/assets/app-icons/android_icon_core_classic.png b/assets/app-icons/android_icon_core_classic.png
new file mode 100644
index 000000000..56fed7f16
--- /dev/null
+++ b/assets/app-icons/android_icon_core_classic.png
Binary files differdiff --git a/assets/app-icons/android_icon_core_flat_black.png b/assets/app-icons/android_icon_core_flat_black.png
new file mode 100644
index 000000000..f203145e7
--- /dev/null
+++ b/assets/app-icons/android_icon_core_flat_black.png
Binary files differdiff --git a/assets/app-icons/android_icon_core_flat_blue.png b/assets/app-icons/android_icon_core_flat_blue.png
new file mode 100644
index 000000000..4a4d157a9
--- /dev/null
+++ b/assets/app-icons/android_icon_core_flat_blue.png
Binary files differdiff --git a/assets/app-icons/android_icon_core_flat_white.png b/assets/app-icons/android_icon_core_flat_white.png
new file mode 100644
index 000000000..a6dc31d55
--- /dev/null
+++ b/assets/app-icons/android_icon_core_flat_white.png
Binary files differdiff --git a/assets/app-icons/android_icon_core_midnight.png b/assets/app-icons/android_icon_core_midnight.png
new file mode 100644
index 000000000..a5fdd1e70
--- /dev/null
+++ b/assets/app-icons/android_icon_core_midnight.png
Binary files differdiff --git a/assets/app-icons/android_icon_core_sunrise.png b/assets/app-icons/android_icon_core_sunrise.png
new file mode 100644
index 000000000..2fbded7e7
--- /dev/null
+++ b/assets/app-icons/android_icon_core_sunrise.png
Binary files differdiff --git a/assets/app-icons/android_icon_core_sunset.png b/assets/app-icons/android_icon_core_sunset.png
new file mode 100644
index 000000000..d632a2322
--- /dev/null
+++ b/assets/app-icons/android_icon_core_sunset.png
Binary files differdiff --git a/assets/app-icons/android_icon_default_dark.png b/assets/app-icons/android_icon_default_dark.png
new file mode 100644
index 000000000..b968147e5
--- /dev/null
+++ b/assets/app-icons/android_icon_default_dark.png
Binary files differdiff --git a/assets/app-icons/android_icon_default_light.png b/assets/app-icons/android_icon_default_light.png
new file mode 100644
index 000000000..4ceaec604
--- /dev/null
+++ b/assets/app-icons/android_icon_default_light.png
Binary files differdiff --git a/assets/app-icons/ios_icon_core_aurora.png b/assets/app-icons/ios_icon_core_aurora.png
new file mode 100644
index 000000000..3be24585d
--- /dev/null
+++ b/assets/app-icons/ios_icon_core_aurora.png
Binary files differdiff --git a/assets/app-icons/ios_icon_core_bonfire.png b/assets/app-icons/ios_icon_core_bonfire.png
new file mode 100644
index 000000000..0928f5eb7
--- /dev/null
+++ b/assets/app-icons/ios_icon_core_bonfire.png
Binary files differdiff --git a/assets/app-icons/ios_icon_core_classic.png b/assets/app-icons/ios_icon_core_classic.png
new file mode 100644
index 000000000..56fed7f16
--- /dev/null
+++ b/assets/app-icons/ios_icon_core_classic.png
Binary files differdiff --git a/assets/app-icons/ios_icon_core_flat_black.png b/assets/app-icons/ios_icon_core_flat_black.png
new file mode 100644
index 000000000..69e7ccfeb
--- /dev/null
+++ b/assets/app-icons/ios_icon_core_flat_black.png
Binary files differdiff --git a/assets/app-icons/ios_icon_core_flat_blue.png b/assets/app-icons/ios_icon_core_flat_blue.png
new file mode 100644
index 000000000..71a380265
--- /dev/null
+++ b/assets/app-icons/ios_icon_core_flat_blue.png
Binary files differdiff --git a/assets/app-icons/ios_icon_core_flat_white.png b/assets/app-icons/ios_icon_core_flat_white.png
new file mode 100644
index 000000000..2f9f61536
--- /dev/null
+++ b/assets/app-icons/ios_icon_core_flat_white.png
Binary files differdiff --git a/assets/app-icons/ios_icon_core_midnight.png b/assets/app-icons/ios_icon_core_midnight.png
new file mode 100644
index 000000000..65a882ad3
--- /dev/null
+++ b/assets/app-icons/ios_icon_core_midnight.png
Binary files differdiff --git a/assets/app-icons/ios_icon_core_sunrise.png b/assets/app-icons/ios_icon_core_sunrise.png
new file mode 100644
index 000000000..9405c7ab3
--- /dev/null
+++ b/assets/app-icons/ios_icon_core_sunrise.png
Binary files differdiff --git a/assets/app-icons/ios_icon_core_sunset.png b/assets/app-icons/ios_icon_core_sunset.png
new file mode 100644
index 000000000..ac57f6b01
--- /dev/null
+++ b/assets/app-icons/ios_icon_core_sunset.png
Binary files differdiff --git a/assets/app-icons/ios_icon_default_dark.png b/assets/app-icons/ios_icon_default_dark.png
new file mode 100644
index 000000000..b9accd048
--- /dev/null
+++ b/assets/app-icons/ios_icon_default_dark.png
Binary files differdiff --git a/assets/app-icons/ios_icon_default_light.png b/assets/app-icons/ios_icon_default_light.png
new file mode 100644
index 000000000..200641fae
--- /dev/null
+++ b/assets/app-icons/ios_icon_default_light.png
Binary files differdiff --git a/assets/icon.png b/assets/icon.png
deleted file mode 100644
index 75866fc82..000000000
--- a/assets/icon.png
+++ /dev/null
Binary files differdiff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go
index d0d6b4c49..19252f765 100644
--- a/bskyweb/cmd/bskyweb/server.go
+++ b/bskyweb/cmd/bskyweb/server.go
@@ -257,6 +257,7 @@ func serve(cctx *cli.Context) error {
 	e.GET("/settings/privacy-and-security", server.WebGeneric)
 	e.GET("/settings/content-and-media", server.WebGeneric)
 	e.GET("/settings/about", server.WebGeneric)
+	e.GET("/settings/app-icon", server.WebGeneric)
 	e.GET("/sys/debug", server.WebGeneric)
 	e.GET("/sys/debug-mod", server.WebGeneric)
 	e.GET("/sys/log", server.WebGeneric)
diff --git a/package.json b/package.json
index 6e7fcd9e8..ddb90c4f6 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,7 @@
     "@lingui/react": "^4.5.0",
     "@mattermost/react-native-paste-input": "^0.7.1",
     "@miblanchard/react-native-slider": "^2.3.1",
+    "@mozzius/expo-dynamic-app-icon": "^1.4.1",
     "@radix-ui/react-dismissable-layer": "^1.1.1",
     "@radix-ui/react-dropdown-menu": "2.0.1",
     "@radix-ui/react-focus-guards": "^1.1.1",
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index cc815ef70..0b162d363 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -79,6 +79,7 @@ import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
 import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
 import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
 import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
+import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
 import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
 import {
   StarterPackScreen,
@@ -363,6 +364,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
         }}
       />
       <Stack.Screen
+        name="AppIconSettings"
+        getComponent={() => AppIconSettingsScreen}
+        options={{
+          title: title(msg`App Icon`),
+          requireAuth: true,
+        }}
+      />
+      <Stack.Screen
         name="Hashtag"
         getComponent={() => HashtagScreen}
         options={{title: title(msg`Hashtag`)}}
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index fe8cf9a78..1f08eb7e1 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -1,7 +1,7 @@
 import {Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native'
 
 import * as tokens from '#/alf/tokens'
-import {native, web} from '#/alf/util/platform'
+import {ios, native, web} from '#/alf/util/platform'
 
 export const atoms = {
   debug: {
@@ -312,6 +312,12 @@ export const atoms = {
   border_r: {
     borderRightWidth: StyleSheet.hairlineWidth,
   },
+  curve_circular: ios({
+    borderCurve: 'circular',
+  }),
+  curve_continuous: ios({
+    borderCurve: 'continuous',
+  }),
 
   /*
    * Shadow
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index a44a9e518..ebf4b1ee1 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -29,6 +29,7 @@ export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
   'did:plc:p2cp5gopk7mgjegy6wadk3ep': true, // samuel.bsky.team
   'did:plc:ragtjsm2j2vknwkz3zp4oxrd': true, // pfrazee.com
   'did:plc:vpkhqolt662uhesyj6nxm7ys': true, // why.bsky.team
+  'did:plc:3jpt2mvvsumj2r7eqk4gzzjz': true, // esb.lol
 }
 
 const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 41a47b4bc..9e3407261 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -44,6 +44,7 @@ export type CommonNavigatorParams = {
   PrivacyAndSecuritySettings: undefined
   ContentAndMediaSettings: undefined
   AboutSettings: undefined
+  AppIconSettings: undefined
   Search: {q?: string}
   Hashtag: {tag: string; author?: string}
   MessagesConversation: {conversation: string; embed?: string}
diff --git a/src/routes.ts b/src/routes.ts
index 388e8c521..188665d84 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -44,6 +44,7 @@ export const router = new Router({
   PrivacyAndSecuritySettings: '/settings/privacy-and-security',
   ContentAndMediaSettings: '/settings/content-and-media',
   AboutSettings: '/settings/about',
+  AppIconSettings: '/settings/app-icon',
   // support
   Support: '/support',
   PrivacyPolicy: '/support/privacy',
diff --git a/src/screens/Settings/AppIconSettings.tsx b/src/screens/Settings/AppIconSettings.tsx
new file mode 100644
index 000000000..1dd87d45f
--- /dev/null
+++ b/src/screens/Settings/AppIconSettings.tsx
@@ -0,0 +1,252 @@
+import React from 'react'
+import {Alert, View} from 'react-native'
+import {Image} from 'expo-image'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import * as AppIcon from '@mozzius/expo-dynamic-app-icon'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+
+import {PressableScale} from '#/lib/custom-animations/PressableScale'
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {isAndroid} from '#/platform/detection'
+import {atoms as a, platform} from '#/alf'
+import * as Layout from '#/components/Layout'
+import {Text} from '#/components/Typography'
+
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
+export function AppIconSettingsScreen({}: Props) {
+  const {_} = useLingui()
+  const sets = useAppIconSets()
+
+  return (
+    <Layout.Screen>
+      <Layout.Header title={_('App Icon')} />
+      <Layout.Content
+        contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}>
+        <Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
+        <View style={[a.flex_row, a.flex_wrap]}>
+          {sets.defaults.map(icon => (
+            <View
+              style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
+              key={icon.id}>
+              <PressableScale
+                accessibilityLabel={icon.name}
+                accessibilityHint={_(msg`Tap to change app icon`)}
+                targetScale={0.95}
+                onPress={() => AppIcon.setAppIcon(icon.id)}>
+                <Image
+                  source={platform({
+                    ios: icon.iosImage(),
+                    android: icon.androidImage(),
+                  })}
+                  style={[
+                    {width: 100, height: 100},
+                    platform({
+                      ios: {borderRadius: 20},
+                      android: a.rounded_full,
+                    }),
+                    a.curve_continuous,
+                  ]}
+                  accessibilityIgnoresInvertColors
+                />
+              </PressableScale>
+              <Text style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}>
+                {icon.name}
+              </Text>
+            </View>
+          ))}
+        </View>
+
+        <Text style={[a.text_lg, a.font_heavy]}>Bluesky+</Text>
+        <View style={[a.flex_row, a.flex_wrap]}>
+          {sets.core.map(icon => (
+            <View
+              style={[{width: '50%'}, a.py_lg, a.px_xs, a.align_center]}
+              key={icon.id}>
+              <PressableScale
+                accessibilityLabel={icon.name}
+                accessibilityHint={_(msg`Tap to change app icon`)}
+                targetScale={0.95}
+                onPress={() => {
+                  if (isAndroid) {
+                    Alert.alert(
+                      _(msg`Change app icon to "${icon.name}"`),
+                      _(msg`The app will be restarted`),
+                      [
+                        {
+                          text: _(msg`Cancel`),
+                          style: 'cancel',
+                        },
+                        {
+                          text: _(msg`OK`),
+                          onPress: () => {
+                            AppIcon.setAppIcon(icon.id)
+                          },
+                          style: 'default',
+                        },
+                      ],
+                    )
+                  } else {
+                    AppIcon.setAppIcon(icon.id)
+                  }
+                }}>
+                <Image
+                  source={platform({
+                    ios: icon.iosImage(),
+                    android: icon.androidImage(),
+                  })}
+                  style={[
+                    {width: 100, height: 100},
+                    platform({
+                      ios: {borderRadius: 20},
+                      android: a.rounded_full,
+                    }),
+                    a.curve_continuous,
+                    a.shadow_lg,
+                  ]}
+                  accessibilityIgnoresInvertColors
+                />
+              </PressableScale>
+              <Text
+                style={[a.text_center, a.font_bold, a.text_md, a.mt_md]}
+                // for Classicâ„¢
+                emoji>
+                {icon.name}
+              </Text>
+            </View>
+          ))}
+        </View>
+      </Layout.Content>
+    </Layout.Screen>
+  )
+}
+
+function useAppIconSets() {
+  const {_} = useLingui()
+
+  return React.useMemo(() => {
+    const defaults = [
+      {
+        id: 'default_light',
+        name: _('Light'),
+        iosImage: () => {
+          return require(`../../../assets/app-icons/ios_icon_default_light.png`)
+        },
+        androidImage: () => {
+          return require(`../../../assets/app-icons/android_icon_default_light.png`)
+        },
+      },
+      {
+        id: 'default_dark',
+        name: _('Dark'),
+        iosImage: () => {
+          return require(`../../../assets/app-icons/ios_icon_default_dark.png`)
+        },
+        androidImage: () => {
+          return require(`../../../assets/app-icons/android_icon_default_dark.png`)
+        },
+      },
+    ]
+
+    /**
+     * Bluesky+
+     */
+    const core = [
+      {
+        id: 'core_aurora',
+        name: _('Aurora'),
+        iosImage: () => {
+          return require(`../../../assets/app-icons/ios_icon_core_aurora.png`)
+        },
+        androidImage: () => {
+          return require(`../../../assets/app-icons/android_icon_core_aurora.png`)
+        },
+      },
+      // {
+      //   id: 'core_bonfire',
+      //   name: _('Bonfire'),
+      //   iosImage: () => {
+      //     return require(`../../../assets/app-icons/ios_icon_core_bonfire.png`)
+      //   },
+      //   androidImage: () => {
+      //     return require(`../../../assets/app-icons/android_icon_core_bonfire.png`)
+      //   },
+      // },
+      {
+        id: 'core_sunrise',
+        name: _('Sunrise'),
+        iosImage: () => {
+          return require(`../../../assets/app-icons/ios_icon_core_sunrise.png`)
+        },
+        androidImage: () => {
+          return require(`../../../assets/app-icons/android_icon_core_sunrise.png`)
+        },
+      },
+      {
+        id: 'core_sunset',
+        name: _('Sunset'),
+        iosImage: () => {
+          return require(`../../../assets/app-icons/ios_icon_core_sunset.png`)
+        },
+        androidImage: () => {
+          return require(`../../../assets/app-icons/android_icon_core_sunset.png`)
+        },
+      },
+      {
+        id: 'core_midnight',
+        name: _('Midnight'),
+        iosImage: () => {
+          return require(`../../../assets/app-icons/ios_icon_core_midnight.png`)
+        },
+        androidImage: () => {
+          return require(`../../../assets/app-icons/android_icon_core_midnight.png`)
+        },
+      },
+      {
+        id: 'core_flat_blue',
+        name: _('Flat Blue'),
+        iosImage: () => {
+          return require(`../../../assets/app-icons/ios_icon_core_flat_blue.png`)
+        },
+        androidImage: () => {
+          return require(`../../../assets/app-icons/android_icon_core_flat_blue.png`)
+        },
+      },
+      {
+        id: 'core_flat_white',
+        name: _('Flat White'),
+        iosImage: () => {
+          return require(`../../../assets/app-icons/ios_icon_core_flat_white.png`)
+        },
+        androidImage: () => {
+          return require(`../../../assets/app-icons/android_icon_core_flat_white.png`)
+        },
+      },
+      {
+        id: 'core_flat_black',
+        name: _('Flat Black'),
+        iosImage: () => {
+          return require(`../../../assets/app-icons/ios_icon_core_flat_black.png`)
+        },
+        androidImage: () => {
+          return require(`../../../assets/app-icons/android_icon_core_flat_black.png`)
+        },
+      },
+      {
+        id: 'core_classic',
+        name: _('Bluesky Classicâ„¢'),
+        iosImage: () => {
+          return require(`../../../assets/app-icons/ios_icon_core_classic.png`)
+        },
+        androidImage: () => {
+          return require(`../../../assets/app-icons/android_icon_core_classic.png`)
+        },
+      },
+    ]
+
+    return {
+      defaults,
+      core,
+    }
+  }, [_])
+}
diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx
index d0beb7d50..662b3093f 100644
--- a/src/screens/Settings/AppearanceSettings.tsx
+++ b/src/screens/Settings/AppearanceSettings.tsx
@@ -5,11 +5,14 @@ import Animated, {
   LayoutAnimationConfig,
   LinearTransition,
 } from 'react-native-reanimated'
-import {msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
+import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
 import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {useSession} from '#/state/session'
 import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
+import {Logo} from '#/view/icons/Logo'
 import {atoms as a, native, useAlf, useTheme} from '#/alf'
 import * as ToggleButton from '#/components/forms/ToggleButton'
 import {Props as SVGIconProps} from '#/components/icons/common'
@@ -70,6 +73,8 @@ export function AppearanceSettingsScreen({}: Props) {
     [fonts],
   )
 
+  const {currentAccount} = useSession()
+
   return (
     <LayoutAnimationConfig skipExiting skipEntering>
       <Layout.Screen testID="preferencesThreadsScreen">
@@ -121,6 +126,8 @@ export function AppearanceSettingsScreen({}: Props) {
             )}
 
             <Animated.View layout={native(LinearTransition)}>
+              <SettingsList.Divider />
+
               <AppearanceToggleButtonGroup
                 title={_(msg`Font`)}
                 description={_(
@@ -161,6 +168,21 @@ export function AppearanceSettingsScreen({}: Props) {
                 values={[fonts.scale]}
                 onChange={onChangeFontScale}
               />
+
+              {DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
+                <>
+                  <SettingsList.Divider />
+
+                  <SettingsList.LinkItem
+                    to="/settings/app-icon"
+                    label={_(msg`App Icon`)}>
+                    <SettingsList.ItemIcon icon={Logo} />
+                    <SettingsList.ItemText>
+                      <Trans>App Icon</Trans>
+                    </SettingsList.ItemText>
+                  </SettingsList.LinkItem>
+                </>
+              )}
             </Animated.View>
           </SettingsList.Container>
         </Layout.Content>
diff --git a/yarn.lock b/yarn.lock
index 16dbba397..f70489e17 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3736,7 +3736,7 @@
   resolved "https://registry.yarnpkg.com/@expo/html-elements/-/html-elements-0.4.3.tgz#32b4ca05dd13582164ed1be34ae87e22adfd1d5b"
   integrity sha512-UwEEdnpyhUEIDe/AkFSBUmCuwcknjAuu73fd5L9Rm/BbHczYXCrtyZmzCNVBsAiHhwUjmhNWzFlr9cAkp/sxIA==
 
-"@expo/image-utils@0.3.23":
+"@expo/image-utils@0.3.23", "@expo/image-utils@^0.3.23":
   version "0.3.23"
   resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.23.tgz#f14fd7e1f5ff6f8e4911a41e27dd274470665c3f"
   integrity sha512-nhUVvW0TrRE4jtWzHQl8TR4ox7kcmrc2I0itaeJGjxF5A54uk7avgA0wRt7jP1rdvqQo1Ke1lXyLYREdhN9tPw==
@@ -4937,6 +4937,15 @@
   resolved "https://registry.yarnpkg.com/@miblanchard/react-native-slider/-/react-native-slider-2.3.1.tgz#79e0f1f9b1ce43ef25ee51ee9256c012e5dfa412"
   integrity sha512-J/hZDBWmXq8fJeOnTVHqIUVDHshqMSpJVxJ4WqwuCBKl5Rke9OBYXIdkSlgi75OgtScAr8FKK5KNkDKHUf6JIg==
 
+"@mozzius/expo-dynamic-app-icon@^1.4.1":
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/@mozzius/expo-dynamic-app-icon/-/expo-dynamic-app-icon-1.4.1.tgz#245e54c31347e3ec2a1ce10f0df8cf07a0c1be6e"
+  integrity sha512-IiL6OiuW4kP5Jz/vrZ6U1t0m4gK1rW5VZAQzszdVcZy1cadX3EdR2/uA6jMU0qSwuesk028RhO6S0uBI9ckxBw==
+  dependencies:
+    "@expo/image-utils" "^0.3.23"
+    expo-modules-core "^1.0.3"
+    xcode "^3.0.1"
+
 "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
   version "5.1.1-v1"
   resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
@@ -10663,6 +10672,13 @@ expo-modules-core@1.12.11:
   dependencies:
     invariant "^2.2.4"
 
+expo-modules-core@^1.0.3:
+  version "1.12.26"
+  resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.12.26.tgz#86c4087dc6246abfc4d7f5e61097dc8cc4b22262"
+  integrity sha512-y8yDWjOi+rQRdO+HY+LnUlz8qzHerUaw/LUjKPU/mX8PRXP4UUPEEp5fjAwBU44xjNmYSHWZDwet4IBBE+yQUA==
+  dependencies:
+    invariant "^2.2.4"
+
 expo-navigation-bar@~3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/expo-navigation-bar/-/expo-navigation-bar-3.0.4.tgz#c122194f720f0fb03430fa8a34c15980fc0a169f"