Skip to content

Commit

Permalink
feat: take link screenshot as thumbnail (#1060)
Browse files Browse the repository at this point in the history
  • Loading branch information
ascariandrea authored Nov 21, 2023
1 parent 2ff0a9e commit 9b5f915
Show file tree
Hide file tree
Showing 39 changed files with 542 additions and 220 deletions.
4 changes: 3 additions & 1 deletion packages/@liexp/backend/src/providers/puppeteer.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type * as error from "@liexp/shared/lib/io/http/Error";
import * as E from "fp-ts/Either";
import * as TE from "fp-ts/TaskEither";
import { pipe } from "fp-ts/function";
import type * as puppeteer from "puppeteer-core";
import * as puppeteer from "puppeteer-core";
import { addExtra, type VanillaPuppeteer } from "puppeteer-extra";
import puppeteerStealth from "puppeteer-extra-plugin-stealth";

Expand Down Expand Up @@ -107,6 +107,7 @@ type BrowserLaunchOpts = puppeteer.LaunchOptions &
puppeteer.BrowserLaunchArgumentOptions;

export interface PuppeteerProvider {
devices: typeof puppeteer.KnownDevices;
getBrowser: (
opts: BrowserLaunchOpts,
) => TE.TaskEither<PuppeteerError, puppeteer.Browser>;
Expand Down Expand Up @@ -257,6 +258,7 @@ export const GetPuppeteerProvider = (
};

return {
devices: puppeteer.KnownDevices,
getBrowser,
goToPage,
download,
Expand Down
4 changes: 4 additions & 0 deletions packages/@liexp/backend/src/providers/space/s3.provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { S3Client, type S3ClientConfig } from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { type Reader } from "fp-ts/Reader";
import { MakeSpaceProvider, type SpaceProvider } from "./space.provider";

Expand All @@ -7,6 +9,8 @@ type GetS3ProviderConfig = S3ClientConfig;
const GetS3Provider: Reader<GetS3ProviderConfig, SpaceProvider> = (config) =>
MakeSpaceProvider({
client: new S3Client(config),
getSignedUrl,
classes: { Upload }
});

export { GetS3Provider };
16 changes: 11 additions & 5 deletions packages/@liexp/backend/src/providers/space/space.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
type ListObjectsCommandInput,
type ListObjectsCommandOutput,
} from "@aws-sdk/client-s3";
import { Upload } from "@aws-sdk/lib-storage";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { type Upload } from "@aws-sdk/lib-storage";
import { type getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { type Endpoint } from "@aws-sdk/types";
import * as logger from "@liexp/core/lib/logger";
import * as TE from "fp-ts/TaskEither";
Expand Down Expand Up @@ -90,10 +90,16 @@ export type SpaceProviderImpl = S3Client;

export interface MakeSpaceProviderConfig {
client: SpaceProviderImpl;
getSignedUrl: typeof getSignedUrl,
classes: {
Upload: typeof Upload,
}
}

export const MakeSpaceProvider = ({
client,
getSignedUrl,
classes
}: MakeSpaceProviderConfig): SpaceProvider => {
return {
getEndpoint: (bucket, path) => {
Expand Down Expand Up @@ -145,15 +151,15 @@ export const MakeSpaceProvider = ({
upload(input) {
return pipe(
TE.tryCatch(async () => {
const parallelUploads3 = new Upload({
const parallelUploads3 = new classes.Upload({
client,
params: { ...input },
queueSize: 4, // optional concurrency configuration
partSize: 1024 * 1024 * 5, // optional size of each part, in bytes, at least 5MB
leavePartsOnError: false, // optional manually handle dropped parts
});

return await parallelUploads3.done();
const result = await parallelUploads3.done();
return result;
}, toError),
TE.filterOrElse(
(
Expand Down
11 changes: 11 additions & 0 deletions packages/@liexp/shared/src/endpoints/link.endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ export const UpdateMetadata = Endpoint({
Output: OneLinkOutput,
});

export const TakeLinkScreenshot = Endpoint({
Method: "POST",
getPath: ({ id }) => `/links/${id}/screenshot`,
Input: {
Params: t.type({ id: UUID }),
Body: Link.EditLink,
},
Output: OneLinkOutput,
});

export const links = ResourceEndpoints({
Get,
List,
Expand All @@ -91,5 +101,6 @@ export const links = ResourceEndpoints({
Custom: {
CreateMany,
Submit,
TakeLinkScreenshot
},
});
3 changes: 2 additions & 1 deletion packages/@liexp/shared/src/io/http/Media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export type MEDIA = t.TypeOf<typeof MEDIA>;

const JpgType = t.literal("image/jpg");
const JpegType = t.literal("image/jpeg");
const PngType = t.literal("image/png");
export const PngType = t.literal("image/png");
export type PngType = t.TypeOf<typeof PngType>

/** audio types */
export const MP3Type = t.literal("audio/mp3");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const MediaArb: tests.fc.Arbitrary<http.Media.Media> = tests
keywords: [],
featuredIn: [],
areas: [],
type: "image/png",
type: http.Media.PngType.value,
creator: undefined,
extra: undefined,
socialPosts: undefined,
Expand Down
157 changes: 1 addition & 156 deletions packages/@liexp/ui/src/components/admin/links/AdminLinks.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,24 @@
import { ImageType } from "@liexp/shared/lib/io/http/Media";
import { checkIsAdmin } from "@liexp/shared/lib/utils/user.utils";
import * as React from "react";
import {
BooleanInput,
Button,
Create,
CreateButton,
Datagrid,
DateInput,
FormTab,
List,
LoadingPage,
ReferenceManyField,
SimpleForm,
TabbedForm,
TextField,
TextInput,
useDataProvider,
useGetIdentity,
usePermissions,
useRecordContext,
useRefresh,
type ListProps,
type RaRecord,
} from "react-admin";
import { Box, Grid, Toolbar } from "../../mui";
import { SocialPostFormTabContent } from '../SocialPost/SocialPostFormTabContent';
import { DangerZoneField } from "../common/DangerZoneField";
import { EditForm } from "../common/EditForm";
import { Box } from "../../mui";
import URLMetadataInput from "../common/URLMetadataInput";
import { CreateEventFromLinkButton } from "../events/CreateEventFromLinkButton";
import ReferenceArrayEventInput from "../events/ReferenceArrayEventInput";
import ReferenceManyEventField from "../events/ReferenceManyEventField";
import ReferenceGroupInput from "../groups/ReferenceGroupInput";
import ReferenceArrayKeywordInput from "../keywords/ReferenceArrayKeywordInput";
import { SearchLinksButton } from "../links/SearchLinksButton";
import { MediaField } from "../media/MediaField";
import ReferenceMediaInput from "../media/input/ReferenceMediaInput";
import LinkPreview from "../previews/LinkPreview";
import ReferenceUserInput from "../user/ReferenceUserInput";
import { LinkDataGrid } from './LinkDataGrid';
import { LinkTGPostButton } from "./button/LinkTGPostButton";

const RESOURCE = "links";

Expand Down Expand Up @@ -93,139 +71,6 @@ export const LinkList: React.FC<ListProps> = (props) => {
);
};

const transformLink = ({ newEvents, ...r }: RaRecord): RaRecord => {
return {
...r,
image: r.image.id ? r.image.id : undefined,
provider: r.provider === "" ? undefined : r.provider,
events: (r.events ?? []).concat(newEvents ?? []),
};
};

const EditTitle: React.FC = () => {
const record = useRecordContext();
return <span>Link {record?.title}</span>;
};

const OverrideThumbnail: React.FC = () => {
const refresh = useRefresh();
const record = useRecordContext();
const dataProvider = useDataProvider();
return (
<Button
label="resources.links.actions.override_thumbnail"
variant="contained"
onClick={() => {
void dataProvider
.put(
`/links/${record?.id}`,
transformLink({
...record,
overrideThumbnail: true,
}),
)
.then(() => {
refresh();
});
}}
/>
);
};

const UpdateMetadataButton: React.FC = () => {
const refresh = useRefresh();
const record = useRecordContext();
const dataProvider = useDataProvider();
return (
<Button
label="resources.links.actions.update_metadata"
variant="contained"
onClick={() => {
void dataProvider.put(`/links/${record?.id}/metadata`).then(() => {
refresh();
});
}}
/>
);
};

export const LinkEdit: React.FC = () => {
const record = useRecordContext();
const { permissions, isLoading: isLoadingPermissions } = usePermissions();
if (isLoadingPermissions) {
return <LoadingPage />;
}

const isAdmin = checkIsAdmin(permissions);

return (
<EditForm
redirect={false}
title={<EditTitle />}
actions={
<Toolbar>
<UpdateMetadataButton />
<LinkTGPostButton />
</Toolbar>
}
preview={<LinkPreview record={record} />}
transform={transformLink}
>
<TabbedForm>
<FormTab label="General">
<Grid container spacing={2}>
<Grid item md={6}>
<TextInput source="title" fullWidth />
<URLMetadataInput source="url" type="Link" />
<DateInput source="publishDate" />
<MediaField
source="image.thumbnail"
sourceType="image/jpeg"
controls={false}
/>
<ReferenceMediaInput
source="image.id"
allowedTypes={ImageType.types.map((t) => t.value)}
/>
<OverrideThumbnail />
<TextInput source="description" fullWidth multiline />
<ReferenceGroupInput source="provider" />
{isAdmin && <DangerZoneField />}
</Grid>
<Grid item md={6}>
<ReferenceArrayKeywordInput source="keywords" showAdd={true} />
{isAdmin && <ReferenceUserInput source="creator" />}
</Grid>
</Grid>
</FormTab>
<FormTab label="Events">
<CreateEventFromLinkButton />
<ReferenceArrayEventInput source="newEvents" defaultValue={[]} />
<ReferenceManyEventField
target="links[]"
filter={{ withDrafts: true }}
/>
</FormTab>
<FormTab label="Event Suggestions">
<ReferenceManyField
reference="events/suggestions"
filter={{ links: record?.id ? [record.id] : [] }}
target="links[]"
>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="payload.event.payload.title" />
</Datagrid>
</ReferenceManyField>
</FormTab>
<FormTab label="Social Posts">
<SocialPostFormTabContent type='links' source="id" />
</FormTab>
</TabbedForm>
</EditForm>
);
};

export const LinkCreate: React.FC = () => {
return (
<Create title="Create a Link">
Expand Down
8 changes: 8 additions & 0 deletions packages/@liexp/ui/src/components/admin/links/EditTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as React from "react";
import { useRecordContext } from "react-admin";


export const EditTitle: React.FC = () => {
const record = useRecordContext();
return <span>Link {record?.title}</span>;
};
Loading

0 comments on commit 9b5f915

Please sign in to comment.