diff --git a/android/app/src/main/java/chat/vojo/app/LaunchSplashPlugin.java b/android/app/src/main/java/chat/vojo/app/LaunchSplashPlugin.java new file mode 100644 index 00000000..f4fb7394 --- /dev/null +++ b/android/app/src/main/java/chat/vojo/app/LaunchSplashPlugin.java @@ -0,0 +1,16 @@ +package chat.vojo.app; + +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; + +@CapacitorPlugin(name = "LaunchSplash") +public class LaunchSplashPlugin extends Plugin { + + @PluginMethod + public void ready(PluginCall call) { + MainActivity.releaseLaunchSplash(); + call.resolve(); + } +} diff --git a/android/app/src/main/java/chat/vojo/app/MainActivity.java b/android/app/src/main/java/chat/vojo/app/MainActivity.java index 2a463ae9..f3734bb9 100644 --- a/android/app/src/main/java/chat/vojo/app/MainActivity.java +++ b/android/app/src/main/java/chat/vojo/app/MainActivity.java @@ -4,12 +4,26 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import androidx.activity.EdgeToEdge; +import androidx.core.splashscreen.SplashScreen; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsControllerCompat; import com.getcapacitor.BridgeActivity; public class MainActivity extends BridgeActivity { public static volatile boolean isInForeground = false; + private static volatile boolean launchSplashReady = false; + + // Safety net for setKeepOnScreenCondition: if JS never calls + // launchSplash.ready() (boot crash, exception during config load before + // AuthMascot mounts, network hang in useClientConfig, deep-link straight + // into AuthLayout where the centered AuthMascot variant doesn't render, + // …) the splash would otherwise hang indefinitely and the user can't + // interact with anything. 5s covers normal cold boots on mid-range + // Android (config + bundle parse + first paint typically lands inside + // 1-2s) with comfortable headroom; past it we drop the splash and let + // whatever the web side has rendered take over — including blank + // AuthLayout, which is at least recoverable. + private static final long SPLASH_SAFETY_TIMEOUT_MS = 5000L; // Short debounce on the onPause→renderRegistry edge so an in-flight JS // removeIncomingRing bridge call (e.g. user accepted/declined, then @@ -33,13 +47,33 @@ public class MainActivity extends BridgeActivity { private final Runnable cancelRunnable = () -> VojoFirebaseMessagingService.cancelRenderedIncomingRings(this); + public static void releaseLaunchSplash() { + launchSplashReady = true; + } + @Override protected void onCreate(Bundle savedInstanceState) { + if (savedInstanceState == null) { + launchSplashReady = false; + } + // Custom plugins must be registered before super.onCreate so BridgeActivity // can wire them into the WebView bridge on load. Registering after // super.onCreate would make the plugin invisible to JS until the next relaunch. registerPlugin(FullScreenIntentPlugin.class); registerPlugin(CallForegroundPlugin.class); + registerPlugin(LaunchSplashPlugin.class); + + // AndroidX SplashScreen must be installed before super.onCreate(). + // Keep it until the web splash confirms its first visible frame is + // ready, OR the safety timeout elapses (see SPLASH_SAFETY_TIMEOUT_MS). + final long splashStartMs = System.currentTimeMillis(); + SplashScreen splashScreen = SplashScreen.installSplashScreen(this); + splashScreen.setKeepOnScreenCondition(() -> { + if (launchSplashReady) return false; + return System.currentTimeMillis() - splashStartMs < SPLASH_SAFETY_TIMEOUT_MS; + }); + EdgeToEdge.enable(this); super.onCreate(savedInstanceState); // Force light icons on both system bars: our CSS is permanently dark diff --git a/android/app/src/main/res/drawable-land-hdpi/splash.png b/android/app/src/main/res/drawable-land-hdpi/splash.png deleted file mode 100644 index e31573b4..00000000 Binary files a/android/app/src/main/res/drawable-land-hdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-mdpi/splash.png b/android/app/src/main/res/drawable-land-mdpi/splash.png deleted file mode 100644 index f7a64923..00000000 Binary files a/android/app/src/main/res/drawable-land-mdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-xhdpi/splash.png b/android/app/src/main/res/drawable-land-xhdpi/splash.png deleted file mode 100644 index 80772550..00000000 Binary files a/android/app/src/main/res/drawable-land-xhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxhdpi/splash.png deleted file mode 100644 index 14c6c8fe..00000000 Binary files a/android/app/src/main/res/drawable-land-xxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/android/app/src/main/res/drawable-land-xxxhdpi/splash.png deleted file mode 100644 index 244ca250..00000000 Binary files a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-hdpi/splash.png b/android/app/src/main/res/drawable-port-hdpi/splash.png deleted file mode 100644 index 74faaa58..00000000 Binary files a/android/app/src/main/res/drawable-port-hdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-mdpi/splash.png b/android/app/src/main/res/drawable-port-mdpi/splash.png deleted file mode 100644 index e944f4ad..00000000 Binary files a/android/app/src/main/res/drawable-port-mdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-xhdpi/splash.png b/android/app/src/main/res/drawable-port-xhdpi/splash.png deleted file mode 100644 index 564a82ff..00000000 Binary files a/android/app/src/main/res/drawable-port-xhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxhdpi/splash.png deleted file mode 100644 index bfabe687..00000000 Binary files a/android/app/src/main/res/drawable-port-xxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/android/app/src/main/res/drawable-port-xxxhdpi/splash.png deleted file mode 100644 index 69290712..00000000 Binary files a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable/splash.png b/android/app/src/main/res/drawable/splash.png deleted file mode 100644 index f7a64923..00000000 Binary files a/android/app/src/main/res/drawable/splash.png and /dev/null differ diff --git a/android/app/src/main/res/drawable/vojo_mascot_splash.png b/android/app/src/main/res/drawable/vojo_mascot_splash.png new file mode 100644 index 00000000..84392931 Binary files /dev/null and b/android/app/src/main/res/drawable/vojo_mascot_splash.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..39a2e6e1 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + + #0d0e11 + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index daf94474..c60a33fd 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -2,10 +2,12 @@ - - diff --git a/capacitor.config.ts b/capacitor.config.ts index 8a677246..7c5f1942 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -17,6 +17,13 @@ const config: CapacitorConfig = { android: { // Keep default: resolveServiceWorkerRequests = true // SW is critical for authenticated Matrix media (MSC3916 / spec v1.11+) + // + // WebView bg color before first body paint. Without this the WebView + // paints transparent/black during bundle hydration, which combined with + // the post-splash window theme produced a visible black flash between + // the Android 12+ system splash and the in-app AuthSplashScreen mascot. + // Matches the native splash + AuthLayout + body backgrounds. + backgroundColor: '#0d0e11', }, plugins: { PushNotifications: { diff --git a/src/app/pages/auth/AuthMascot.tsx b/src/app/pages/auth/AuthMascot.tsx index 766871d4..a51abf99 100644 --- a/src/app/pages/auth/AuthMascot.tsx +++ b/src/app/pages/auth/AuthMascot.tsx @@ -1,27 +1,82 @@ -import React, { Ref } from 'react'; +import React, { Ref, useCallback, useEffect, useLayoutEffect, useRef } from 'react'; -import mascotPoster from '../../../../public/res/img/mascot.png'; -import mascotWebm from '../../../../public/res/img/mascot.webm'; +import { launchSplash } from '../../plugins/launchSplash'; +import { + attachMascotVideo, + detachMascotVideo, + mascotVideoReady, +} from './mascotSingleton'; import * as css from './styles.css'; type AuthMascotProps = { mascotRef?: Ref; + // True for the splash-gate variant (cold start, sync wait): mascot is + // centered both axes to mirror the Android 12+ system splash icon, so the + // native → WebView → web-splash handoff reads as one continuous splash. + // False (default) keeps the auth-page positioning (top-anchored) used by + // login / register / reset-password. + centered?: boolean; }; -export function AuthMascot({ mascotRef }: AuthMascotProps) { +export function AuthMascot({ mascotRef, centered }: AuthMascotProps) { + const containerRef = useRef(null); + const didSignalLaunchReadyRef = useRef(false); + + // Singleton