feat: add Android/Capacitor platform support via overlay system

- Add Android project (Capacitor-generated, app ID com.paarrot.app)
- Add capacitor.config.json at repo root (webDir: cinny/dist)
- Add overlay/ directory with mobile-specific source patches:
  - tauri.ts: Capacitor native detection, LocalNotifications, permission APIs
  - matrix.ts / useAuthenticatedMediaUrl.ts: legacy /_matrix/media/ URL support
  - SystemNotification.tsx: Capacitor permission request flow
  - ClientNonUIFeatures.tsx: Capacitor notification routing
  - tsconfig.json: moduleResolution bundler, rootDir set
  - package-additions.json: @capacitor/* deps to merge at build time
- Add scripts/apply-overlay.mjs: applies overlay onto clean cinny submodule before build
- Update android:prepare script to run apply-overlay before build + cap sync
This commit is contained in:
2026-04-07 15:11:59 +10:00
parent 7b604b5caf
commit adce77b73b
64 changed files with 2975 additions and 1 deletions

View File

@@ -0,0 +1,142 @@
import { useCallback, useEffect, useState } from 'react';
import { useMatrixClient } from './useMatrixClient';
import { getCurrentAccessToken } from '../utils/auth';
/**
* Fetches media with authentication and returns a blob URL.
* This is needed because service workers may not work reliably in Tauri/WebKit environments,
* and cross-origin image requests cannot include Authorization headers.
*/
export const useAuthenticatedMediaUrl = (
src: string | undefined,
useAuthentication: boolean
): string | undefined => {
const mx = useMatrixClient();
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
useEffect(() => {
if (!src) {
setBlobUrl(undefined);
return;
}
// If not using authentication, just return the original URL
if (!useAuthentication) {
setBlobUrl(src);
return;
}
// Check if this is an authenticated media URL
const isAuthenticatedMediaUrl =
src.includes('/_matrix/client/v1/media/download') ||
src.includes('/_matrix/client/v1/media/thumbnail') ||
(src.includes('/_matrix/media/') &&
(src.includes('/download/') || src.includes('/thumbnail/')));
if (!isAuthenticatedMediaUrl) {
setBlobUrl(src);
return;
}
let cancelled = false;
let objectUrl: string | undefined;
const fetchMedia = async () => {
try {
// Always use current session's token to avoid stale tokens during account switches
const accessToken = getCurrentAccessToken();
let response = await fetch(src, {
method: 'GET',
headers: accessToken
? { Authorization: `Bearer ${accessToken}` }
: undefined,
});
// If we got a 401 and we tried with auth, fallback to unauthenticated request
if (!response.ok && response.status === 401 && accessToken) {
console.warn('[useAuthenticatedMediaUrl] Auth failed (401), attempting unauthenticated fallback for:', src);
response = await fetch(src, { method: 'GET' });
}
if (!response.ok) {
console.warn(`Failed to fetch authenticated media: ${response.status}`);
// Fall back to original URL in case server doesn't require auth
if (!cancelled) setBlobUrl(src);
return;
}
const blob = await response.blob();
if (!cancelled) {
objectUrl = URL.createObjectURL(blob);
setBlobUrl(objectUrl);
}
} catch (error) {
console.warn('Error fetching authenticated media:', error);
// Fall back to original URL
if (!cancelled) setBlobUrl(src);
}
};
fetchMedia();
return () => {
cancelled = true;
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
};
}, [src, useAuthentication, mx]);
return blobUrl;
};
/**
* Creates an authenticated fetch function for media URLs.
* Useful for components that need to load multiple images.
*/
export const useAuthenticatedMediaFetch = () => {
const mx = useMatrixClient();
return useCallback(
async (src: string): Promise<string> => {
const isAuthenticatedMediaUrl =
src.includes('/_matrix/client/v1/media/download') ||
src.includes('/_matrix/client/v1/media/thumbnail') ||
(src.includes('/_matrix/media/') &&
(src.includes('/download/') || src.includes('/thumbnail/')));
if (!isAuthenticatedMediaUrl) {
return src;
}
try {
// Always use current session's token to avoid stale tokens during account switches
const accessToken = getCurrentAccessToken();
let response = await fetch(src, {
method: 'GET',
headers: accessToken
? { Authorization: `Bearer ${accessToken}` }
: undefined,
});
// If we got a 401 and we tried with auth, fallback to unauthenticated request
if (!response.ok && response.status === 401 && accessToken) {
console.warn('[useAuthenticatedMediaFetch] Auth failed (401), attempting unauthenticated fallback');
response = await fetch(src, { method: 'GET' });
}
if (!response.ok) {
console.warn(`Failed to fetch authenticated media: ${response.status}`);
return src;
}
const blob = await response.blob();
return URL.createObjectURL(blob);
} catch (error) {
console.warn('Error fetching authenticated media:', error);
return src;
}
},
[mx]
);
};