Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

# You can now chose to generate bundled link previews for your message (MSC4095). They get downloaded and attached to your message and dont rely on your server supporting server-side embeds.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@
"vite-plugin-svgr": "4.5.0",
"vite-plugin-top-level-await": "^1.6.0",
"vitest": "^4.1.9",
"wrangler": "^4.106.0"
"wrangler": "^4.106.0",
"htmlparser2": "^12.0.0"
},
"devEngines": {
"runtime": {
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"clipboard-manager:allow-write-image",
{
"identifier": "http:default",
"allow": [{ "url": "https://*" }]
"allow": [{ "url": "http://*:*" }, { "url": "https://*" }]
},
{
"identifier": "opener:allow-open-url",
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/RenderMessageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ function RenderMessageContentInternal({
),
[urlPreview]
);
const messageUrlsPreview = urlPreview || themeChatSableWidgets ? renderUrlsPreview : undefined;
const messageUrlsPreview = urlPreview ? renderUrlsPreview : undefined;
const messageBundlePreview = bundledPreview ? renderBundledPreviews : undefined;

const renderCaption = () => {
Expand Down
2 changes: 2 additions & 0 deletions src/app/components/icons/phosphor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ import {
VideoCameraSlashIcon,
WarningIcon,
XIcon,
CircleNotchIcon,
} from '@phosphor-icons/react';
import type { ComponentType, ReactNode } from 'react';
import { useLayoutEffect } from 'react';
Expand Down Expand Up @@ -436,4 +437,5 @@ export {
VideoCameraSlashIcon as VideoCameraSlash,
WarningIcon as Warning,
XIcon as X,
CircleNotchIcon as CircleNotch,
};
2 changes: 1 addition & 1 deletion src/app/components/overlay-stack/OverlayStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function OverlayStackProvider({ children }: { children: ReactNode }) {
setClaims((prev) =>
prev.some((claim) => claim.id === id)
? prev
: [...prev, { id, seq }].sort((a, b) => a.seq - b.seq)
: [...prev, { id, seq }].toSorted((a, b) => a.seq - b.seq)
),
release: (id) => setClaims((prev) => prev.filter((claim) => claim.id !== id)),
}),
Expand Down
146 changes: 146 additions & 0 deletions src/app/components/upload-card/EmbedCardRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { useMatrixClient } from '$hooks/useMatrixClient.ts';
import { roomEmbedAtomFamily } from '$state/room/roomInputDrafts.ts';
import { useSetting } from '$state/hooks/settings.ts';
import { settingsAtom } from '$state/settings.ts';
import { UploadCard } from '$components/upload-card/UploadCard.tsx';
import { Box, color, Text, toRem } from 'folds';
import { Check, CircleNotch, File as FileIcon, sizedIcon, X } from '$components/icons/phosphor.tsx';
import {
EmbedErrorReason,
EmbedPreviewType,
EmbedStatus,
type FixedPreviewUrlResponse,
useBindEmbedAtom,
} from '$state/bundle.ts';
import { Image as MediaImage } from '$components/media';
import { useObjectURL } from '$hooks/useObjectURL.ts';
import { isImageMimeType } from '$utils/mimeTypes.ts';

type EmbedCardRendererProps = {
url: string;
successCallback: (result: FixedPreviewUrlResponse) => void;
encrypt: boolean;
};

function prettyError(error: EmbedErrorReason) {
switch (error) {
//TODO localize
case EmbedErrorReason.UploadFailed:
return 'Failed to upload';
case EmbedErrorReason.EncryptFailed:
return 'Failed to encrypt';
case EmbedErrorReason.RequestFailed:
return 'Failed to fetch url';
case EmbedErrorReason.NoOgData:
return 'Url doesnt provide previews';
}
}

interface MediaProps {
data: Blob;
type: string;
}

function Media({ data, type }: Readonly<MediaProps>) {
const fileUrl = useObjectURL(data);
if (isImageMimeType(type)) {
return (
<MediaImage
style={{
objectFit: 'contain',
width: '100%',
height: toRem(128),
}}
src={fileUrl}
/>
);
}

return <FileIcon size={'fill'} />;
}

export function EmbedCardRenderer({
url,
successCallback,
encrypt,
}: Readonly<EmbedCardRendererProps>) {
const mx = useMatrixClient();
const [bundleUseHomeserver] = useSetting(settingsAtom, 'useHomeserverForBundles');
const embedAtom = roomEmbedAtomFamily(url);
const { embed, startEmbed } = useBindEmbedAtom(mx, embedAtom, encrypt, bundleUseHomeserver);

if (embed.status === EmbedStatus.Idle) {
startEmbed(successCallback).then(() => {});
}

return (
<UploadCard
radii="300"
compact
style={{ maxWidth: toRem(400), flexShrink: 0 }}
before={<></>}
bottom={
<>
{embed.status == EmbedStatus.Idle && <>{'Preview here'}</>}
{(embed.status == EmbedStatus.Loading || embed.status == EmbedStatus.Success) && (
<>
<Box direction={'Row'} gap={'200'}>
{embed.preview?.type == EmbedPreviewType.TitleDescriptionMedia && (
<Box style={{ flex: 2 }}>
{embed.preview?.media && (
<Media data={embed.preview.media} type={embed.preview.mediaType ?? ''} />
)}
{!embed.preview?.media && (
<CircleNotch
size={'fill'}
style={{
animation: 'spin 1s infinite linear',
}}
/>
)}
</Box>
)}
<Box direction={'Column'} style={{ flex: 5 }}>
<Text size={'H4'}>{embed.preview?.title}</Text>
<Text size={'T400'}>{embed.preview?.description || 'No Description'}</Text>
</Box>
</Box>
</>
)}
{embed.status == EmbedStatus.Error && (
<Text style={{ color: color.Critical.Main }}>{prettyError(embed.reason)}</Text>
)}
</>
}
>
<Text size="H6" truncate align={'Left'} style={{ minWidth: 0, flexGrow: 1 }}>
{url}
</Text>
{embed.status == EmbedStatus.Success &&
sizedIcon(Check, '100', { style: { color: color.Success.Main } })}

{embed.status == EmbedStatus.Error &&
sizedIcon(X, '100', { style: { color: color.Critical.Main } })}
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>
{embed.status == EmbedStatus.Loading && (
<Text align={'Right'} size="H6" truncate style={{ minWidth: 0, flexGrow: 1 }}>
{embed.progress}
</Text>
)}
{(embed.status == EmbedStatus.Loading || embed.status == EmbedStatus.Idle) &&
sizedIcon(CircleNotch, '100', {
style: {
color: color.Primary.Main,
animation: 'spin 1s infinite linear',
},
})}
</UploadCard>
);
}
1 change: 1 addition & 0 deletions src/app/components/upload-card/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './UploadCard';
export * from './UploadCardRenderer';
export * from './CompactUploadCardRenderer';
export * from './EmbedCardRenderer';
2 changes: 2 additions & 0 deletions src/app/features/room/RoomInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ vi.mock('$state/room/roomInputDrafts', async () => {
const { atom } = await import('jotai');
const msgDraftAtom = atom([]);
const replyDraftAtom = atom(undefined);
const embedItemsAtom = atom([]);
const uploadItemsAtom = atom<unknown[], [unknown], void>([], (get, set, action) => {
const update = action as any;
if (Array.isArray(update)) {
Expand Down Expand Up @@ -115,6 +116,7 @@ vi.mock('$state/room/roomInputDrafts', async () => {
roomIdToMsgDraftAtomFamily: () => msgDraftAtom,
roomIdToReplyDraftAtomFamily: () => replyDraftAtom,
roomIdToUploadItemsAtomFamily: () => uploadItemsAtom,
roomIdToEmbeddItemsAtomFamily: () => embedItemsAtom,
roomUploadAtomFamily: Object.assign(() => atom(undefined), { remove: vi.fn() }),
roomIdToScheduledTimeAtomFamily: () => scheduledTimeAtom,
roomIdToEditingScheduledDelayIdAtomFamily: () => editingScheduledDelayIdAtom,
Expand Down
Loading
Loading