FileSaver fails in Capacitor WebView, so save via MediaStore; clear tray notifications when sync reports rooms as read on another device, and improve collapsed notification avatars with conversation shortcuts.
85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
import { Badge, Box, IconButton, Spinner, Text, as, toRem } from 'folds';
|
|
import { Icon, Icons } from '../icons';
|
|
import React, { ReactNode, useCallback } from 'react';
|
|
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
|
|
import { mimeTypeToExt } from '../../utils/mimeTypes';
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
|
import {
|
|
decryptFile,
|
|
downloadEncryptedMedia,
|
|
downloadMedia,
|
|
mxcUrlToHttp,
|
|
} from '../../utils/matrix';
|
|
import { getCurrentAccessToken } from '../../utils/auth';
|
|
import { saveMediaBlob } from '../../utils/saveMedia';
|
|
|
|
const badgeStyles = { maxWidth: toRem(100) };
|
|
|
|
type FileDownloadButtonProps = {
|
|
filename: string;
|
|
url: string;
|
|
mimeType: string;
|
|
encInfo?: EncryptedAttachmentInfo;
|
|
};
|
|
export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDownloadButtonProps) {
|
|
const mx = useMatrixClient();
|
|
const useAuthentication = useMediaAuthentication();
|
|
|
|
const [downloadState, download] = useAsyncCallback(
|
|
useCallback(async () => {
|
|
const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
|
|
// Always use current session's token to avoid stale tokens during account switches
|
|
const accessToken = getCurrentAccessToken();
|
|
const fileContent = encInfo
|
|
? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo), accessToken)
|
|
: await downloadMedia(mediaUrl, accessToken);
|
|
|
|
await saveMediaBlob(fileContent, filename);
|
|
return true;
|
|
}, [mx, url, useAuthentication, mimeType, encInfo, filename])
|
|
);
|
|
|
|
const downloading = downloadState.status === AsyncStatus.Loading;
|
|
const hasError = downloadState.status === AsyncStatus.Error;
|
|
return (
|
|
<IconButton
|
|
disabled={downloading}
|
|
onClick={download}
|
|
variant={hasError ? 'Critical' : 'SurfaceVariant'}
|
|
size="300"
|
|
radii="300"
|
|
>
|
|
{downloading ? (
|
|
<Spinner size="100" variant={hasError ? 'Critical' : 'Secondary'} />
|
|
) : (
|
|
<Icon size="100" src={Icons.Download} />
|
|
)}
|
|
</IconButton>
|
|
);
|
|
}
|
|
|
|
export type FileHeaderProps = {
|
|
body: string;
|
|
mimeType: string;
|
|
after?: ReactNode;
|
|
};
|
|
export const FileHeader = as<'div', FileHeaderProps>(({ body, mimeType, after, ...props }, ref) => (
|
|
<Box alignItems="Center" gap="200" grow="Yes" {...props} ref={ref}>
|
|
<Box shrink="No">
|
|
<Badge style={badgeStyles} variant="Secondary" radii="Pill">
|
|
<Text size="O400" truncate>
|
|
{mimeTypeToExt(mimeType)}
|
|
</Text>
|
|
</Badge>
|
|
</Box>
|
|
<Box grow="Yes">
|
|
<Text size="T300" truncate>
|
|
{body}
|
|
</Text>
|
|
</Box>
|
|
{after}
|
|
</Box>
|
|
));
|