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,160 @@
import React, { useCallback } from 'react';
import { Box, Text, Switch, Button, color, Spinner } from 'folds';
import { IPusherRequest } from 'matrix-js-sdk';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { getNotificationState, usePermissionState } from '../../../hooks/usePermission';
import { useEmailNotifications } from '../../../hooks/useEmailNotifications';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { isCapacitorNative, requestSystemNotificationPermission } from '../../../utils/tauri';
function EmailNotification() {
const mx = useMatrixClient();
const [result, refreshResult] = useEmailNotifications();
const [setState, setEnable] = useAsyncCallback(
useCallback(
async (email: string, enable: boolean) => {
if (enable) {
await mx.setPusher({
kind: 'email',
app_id: 'm.email',
pushkey: email,
app_display_name: 'Email Notifications',
device_display_name: email,
lang: 'en',
data: {
brand: 'Paarrot',
},
append: true,
});
return;
}
await mx.setPusher({
pushkey: email,
app_id: 'm.email',
kind: null,
} as unknown as IPusherRequest);
},
[mx]
)
);
const handleChange = (value: boolean) => {
if (result && result.email) {
setEnable(result.email, value).then(() => {
refreshResult();
});
}
};
return (
<SettingTile
title="Email Notification"
description={
<>
{result && !result.email && (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
Your account does not have any email attached.
</Text>
)}
{result && result.email && <>Send notification to your email. {`("${result.email}")`}</>}
{result === null && (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
Unexpected Error!
</Text>
)}
{result === undefined && 'Send notification to your email.'}
</>
}
after={
<>
{setState.status !== AsyncStatus.Loading &&
typeof result === 'object' &&
result?.email && <Switch value={result.enabled} onChange={handleChange} />}
{(setState.status === AsyncStatus.Loading || result === undefined) && (
<Spinner variant="Secondary" />
)}
</>
}
/>
);
}
export function SystemNotification() {
const notifPermission = usePermissionState('notifications', getNotificationState());
const capacitorNative = isCapacitorNative();
const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications');
const [isNotificationSounds, setIsNotificationSounds] = useSetting(
settingsAtom,
'isNotificationSounds'
);
const requestNotificationPermission = async () => {
await requestSystemNotificationPermission();
};
return (
<Box direction="Column" gap="100">
<Text size="L400">System</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Desktop Notifications"
description={
notifPermission === 'denied' ? (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
{'Notification' in window
? 'Notification permission is blocked. Please allow notification permission from browser address bar.'
: 'Notifications are not supported by the system.'}
</Text>
) : (
<span>Show desktop notifications when message arrive.</span>
)
}
after={
notifPermission === 'prompt' ? (
<Button size="300" radii="300" onClick={requestNotificationPermission}>
<Text size="B300">Enable</Text>
</Button>
) : (
<Switch
disabled={!capacitorNative && notifPermission !== 'granted'}
value={showNotifications}
onChange={setShowNotifications}
/>
)
}
/>
</SequenceCard>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile
title="Notification Sound"
description="Play sound when new message arrive."
after={<Switch value={isNotificationSounds} onChange={setIsNotificationSounds} />}
/>
</SequenceCard>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<EmailNotification />
</SequenceCard>
</Box>
);
}