diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index bafccd40..b2c2c021 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -36,6 +36,17 @@
android:grantUriPermissions="true">
+
+
+
+
diff --git a/android/app/src/main/java/social/flotilla/MainActivity.java b/android/app/src/main/java/social/flotilla/MainActivity.java
index e86ef463..e1fca734 100644
--- a/android/app/src/main/java/social/flotilla/MainActivity.java
+++ b/android/app/src/main/java/social/flotilla/MainActivity.java
@@ -1,5 +1,8 @@
package social.flotilla;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.os.Build;
import android.os.Bundle;
import com.getcapacitor.BridgeActivity;
@@ -10,6 +13,28 @@ public class MainActivity extends BridgeActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
registerPlugin(AndroidPushFallbackPlugin.class);
+ createPushNotificationChannel();
super.onCreate(savedInstanceState);
}
+
+ // Ensures the channel named by the FCM default_notification_channel_id metadata
+ // exists. FCM does not create it for us; if it's missing when a notification
+ // arrives, Android falls back to a generic "Miscellaneous" channel. Channels
+ // persist once created, so doing this on launch is sufficient.
+ private void createPushNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationManager manager = getSystemService(NotificationManager.class);
+ String id = getString(R.string.default_notification_channel_id);
+
+ if (manager != null && manager.getNotificationChannel(id) == null) {
+ NotificationChannel channel = new NotificationChannel(
+ id,
+ "Notifications",
+ NotificationManager.IMPORTANCE_HIGH
+ );
+ channel.setDescription("New messages and activity");
+ manager.createNotificationChannel(channel);
+ }
+ }
+ }
}
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 4cb42b63..088e07bf 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -4,4 +4,5 @@
Flotilla
social.flotilla
social.flotilla
+ flotilla_push
diff --git a/build.sh b/build.sh
index b9229dc3..3a8b6d5e 100755
--- a/build.sh
+++ b/build.sh
@@ -40,3 +40,6 @@ npx @capacitor/assets generate \
--iconBackgroundColorDark '#222222' \
--splashBackgroundColor '#ffffff' \
--splashBackgroundColorDark '#191E24'
+
+# @capacitor/assets doesn't generate Android notification icons
+node scripts/generate-notification-icon.mjs
diff --git a/package.json b/package.json
index 78956a9f..1768be24 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"postcss": "^8.5.15",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
+ "sharp": "0.35.0-rc.0",
"svelte": "^5.55.9",
"svelte-check": "^4.3.5",
"tailwindcss": "^4.2.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5e09364f..53fb07dd 100644
Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ
diff --git a/scripts/generate-notification-icon.mjs b/scripts/generate-notification-icon.mjs
new file mode 100644
index 00000000..789cc3f7
--- /dev/null
+++ b/scripts/generate-notification-icon.mjs
@@ -0,0 +1,49 @@
+#!/usr/bin/env node
+// Generates the Android notification (status-bar) small icon from the app's
+// source logo. @capacitor/assets does NOT produce notification icons, so the
+// build pipeline calls this after `@capacitor/assets generate` to keep the
+// notification icon in sync with a custom platform logo (VITE_PLATFORM_LOGO).
+//
+// A notification small icon must be a monochrome silhouette: Android renders
+// only its alpha channel and tints it. We take the logo's alpha channel as the
+// shape and fill it white, with a little transparent padding so it isn't
+// edge-to-edge in the status bar.
+import sharp from "sharp"
+import {mkdir} from "node:fs/promises"
+import {dirname, join} from "node:path"
+
+const source = process.argv[2] || "assets/logo.png"
+const resDir = process.argv[3] || "android/app/src/main/res"
+
+// density bucket -> small-icon size in px
+const densities = {mdpi: 24, hdpi: 36, xhdpi: 48, xxhdpi: 72, xxxhdpi: 96}
+
+const generate = async (size, out) => {
+ const pad = Math.round(size * 0.14)
+ const inner = size - pad * 2
+
+ // The logo's alpha channel is the shape we want to render.
+ const alpha = await sharp(source)
+ .resize(inner, inner, {fit: "contain", background: {r: 0, g: 0, b: 0, alpha: 0}})
+ .ensureAlpha()
+ .extractChannel(3)
+ .toColourspace("b-w")
+ .raw()
+ .toBuffer()
+
+ await sharp({
+ create: {width: inner, height: inner, channels: 3, background: {r: 255, g: 255, b: 255}},
+ })
+ .joinChannel(alpha, {raw: {width: inner, height: inner, channels: 1}})
+ .extend({top: pad, bottom: pad, left: pad, right: pad, background: {r: 0, g: 0, b: 0, alpha: 0}})
+ .png()
+ .toFile(out)
+
+ console.log(`wrote ${out} (${size}px)`)
+}
+
+for (const [density, size] of Object.entries(densities)) {
+ const out = join(resDir, `drawable-${density}`, "ic_stat_notify.png")
+ await mkdir(dirname(out), {recursive: true})
+ await generate(size, out)
+}