From 86cdae3682b334a9f65a80425c045c86b7bd69a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 27 Nov 2025 15:16:09 +0100 Subject: [PATCH] pl-fe: allow posting from drive on iceshrimp.net MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/compose.ts | 5 +- .../compose/components/compose-form.tsx | 2 + .../compose/components/drive-button.tsx | 62 +++++++++++++++++++ packages/pl-fe/src/locales/en.json | 1 + .../src/modals/select-drive-file-modal.tsx | 5 +- packages/pl-fe/src/pages/drive/drive.tsx | 16 +++-- packages/pl-fe/src/pages/fun/circle.tsx | 2 +- packages/pl-fe/src/styles/new/drive.scss | 17 +---- 8 files changed, 83 insertions(+), 27 deletions(-) create mode 100644 packages/pl-fe/src/features/compose/components/drive-button.tsx diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index 1d4c57cb4..6251a208b 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -519,7 +519,7 @@ const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) => dispatch(uploadFile( f, intl, - (data) => dispatch(uploadComposeSuccess(composeId, data, f)), + (data) => dispatch(uploadComposeSuccess(composeId, data)), (error) => dispatch(uploadComposeFail(composeId, error)), ({ loaded }) => { progress[i] = loaded; @@ -543,11 +543,10 @@ const uploadComposeProgress = (composeId: string, loaded: number, total: number) total, }); -const uploadComposeSuccess = (composeId: string, media: MediaAttachment, file: File) => ({ +const uploadComposeSuccess = (composeId: string, media: MediaAttachment) => ({ type: COMPOSE_UPLOAD_SUCCESS, composeId, media, - file, }); const uploadComposeFail = (composeId: string, error: unknown) => ({ diff --git a/packages/pl-fe/src/features/compose/components/compose-form.tsx b/packages/pl-fe/src/features/compose/components/compose-form.tsx index 31097cfaa..02486f9c1 100644 --- a/packages/pl-fe/src/features/compose/components/compose-form.tsx +++ b/packages/pl-fe/src/features/compose/components/compose-form.tsx @@ -42,6 +42,7 @@ import { countableText } from '../util/counter'; import ClearLinkSuggestion from './clear-link-suggestion'; import ContentTypeButton from './content-type-button'; +import DriveButton from './drive-button'; import HashtagCasingSuggestion from './hashtag-casing-suggestion'; import InteractionPolicyButton from './interaction-policy-button'; import LanguageDropdown from './language-dropdown'; @@ -281,6 +282,7 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab const renderButtons = useCallback(() => (
+ {features.drive && } {features.polls && } {features.scheduledStatuses && } diff --git a/packages/pl-fe/src/features/compose/components/drive-button.tsx b/packages/pl-fe/src/features/compose/components/drive-button.tsx new file mode 100644 index 000000000..7a1e37e7a --- /dev/null +++ b/packages/pl-fe/src/features/compose/components/drive-button.tsx @@ -0,0 +1,62 @@ +import { mediaAttachmentSchema } from 'pl-api'; +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import * as v from 'valibot'; + +import { uploadComposeSuccess } from 'pl-fe/actions/compose'; +import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +import { useInstance } from 'pl-fe/hooks/use-instance'; +import { useModalsActions } from 'pl-fe/stores/modals'; + +import ComposeFormButton from './compose-form-button'; + +const messages = defineMessages({ + button: { id: 'compose_form.drive_button', defaultMessage: 'Select from drive' }, +}); + +interface IDriveButton { + composeId: string; +} + +const DriveButton: React.FC = ({ composeId }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const { configuration } = useInstance(); + const { openModal } = useModalsActions(); + + const attachmentTypes = configuration.media_attachments.supported_mime_types; + + const onClick = () => openModal('SELECT_DRIVE_FILE', { + title: intl.formatMessage(messages.button), + type: 'file', + accepted: (attachmentTypes?.length === 0 && attachmentTypes[0] === 'application/octet-stream') ? undefined : attachmentTypes, + onSelect: (file) => { + let type = file.content_type.split('/')[0] as 'image' | 'video' | 'audio' | 'unknown'; + if (!['image', 'video', 'audio', 'unknown'].includes(type)) { + type = 'unknown'; + } + + const mediaAttachment = v.parse(mediaAttachmentSchema, { + id: file.id, + url: file.url, + preview_url: file.thumbnail_url, + remote_url: file.url, + description: file.description || '', + type, + mime_type: file.content_type, + }); + + dispatch(uploadComposeSuccess(composeId, mediaAttachment)); + }, + }); + + return ( + + ); +}; + +export { DriveButton as default }; diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 001d46aff..11b5db393 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -542,6 +542,7 @@ "compose_form.approval_required": "The reply needs to be approved by the post author.", "compose_form.content_type.change": "Change content type", "compose_form.direct_message_warning": "This post will only be sent to the mentioned users.", + "compose_form.drive_button": "Select from drive", "compose_form.event_placeholder": "Post to this event", "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", "compose_form.interaction_policy.label": "Manage interaction policy", diff --git a/packages/pl-fe/src/modals/select-drive-file-modal.tsx b/packages/pl-fe/src/modals/select-drive-file-modal.tsx index bd8a72ed5..eedbaaf79 100644 --- a/packages/pl-fe/src/modals/select-drive-file-modal.tsx +++ b/packages/pl-fe/src/modals/select-drive-file-modal.tsx @@ -19,6 +19,7 @@ type SelectDriveFileModalProps = { } & ({ type: 'file'; onSelect: (file: DriveFile) => void; + accepted?: Array; } | { type: 'folder'; onSelect: (folder: DriveFolder) => void; @@ -89,7 +90,7 @@ const File: React.FC = ({ file, active, disabled, onSelect }) => { ); }; -const SelectDriveFileModal: React.FC = ({ onClose, onSelect, type, disabled, title }) => { +const SelectDriveFileModal: React.FC = ({ onClose, onSelect, type, disabled, title, ...props }) => { const onClickClose = () => { onClose('SELECT_DRIVE_FILE'); }; @@ -149,7 +150,7 @@ const SelectDriveFileModal: React.FC key={file.id} file={file} active={selectedFile === file.id} - disabled={type === 'folder' || disabled?.includes(file.id)} + disabled={type === 'folder' || disabled?.includes(file.id) || ('accepted' in props && props.accepted && !props.accepted.includes(file.content_type))} onSelect={({ id }) => { if (type === 'file') { setSelectedFile(id); diff --git a/packages/pl-fe/src/pages/drive/drive.tsx b/packages/pl-fe/src/pages/drive/drive.tsx index 5c77fab7d..4566b08fd 100644 --- a/packages/pl-fe/src/pages/drive/drive.tsx +++ b/packages/pl-fe/src/pages/drive/drive.tsx @@ -1,8 +1,10 @@ import defaultIcon from '@phosphor-icons/core/regular/paperclip.svg'; import { clsx } from 'clsx'; +import { mediaAttachmentSchema, type DriveFile, type DriveFolder } from 'pl-api'; import React, { useMemo } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Link, useHistory } from 'react-router-dom'; +import * as v from 'valibot'; import DropdownMenu, { Menu } from 'pl-fe/components/dropdown-menu'; import { EmptyMessage } from 'pl-fe/components/empty-message'; @@ -17,8 +19,6 @@ import { useModalsActions } from 'pl-fe/stores/modals'; import toast from 'pl-fe/toast'; import { download } from 'pl-fe/utils/download'; -import type { DriveFile, DriveFolder, MediaAttachment } from 'pl-api'; - const messages = defineMessages({ heading: { id: 'column.drive', defaultMessage: 'Drive' }, folderDropdown: { id: 'drive.folder.dropdown', defaultMessage: 'Folder menu' }, @@ -165,16 +165,20 @@ const File: React.FC = ({ file }) => { return; } - const mediaAttachment = { + let type = file.content_type.split('/')[0] as 'image' | 'video' | 'audio' | 'unknown'; + if (!['image', 'video', 'audio', 'unknown'].includes(type)) { + type = 'unknown'; + } + + const mediaAttachment = v.parse(mediaAttachmentSchema, { id: file.id, url: file.url, preview_url: file.thumbnail_url, remote_url: file.url, description: file.description || '', - type: file.content_type.split('/')[0] as 'image' | 'video' | 'audio' | 'unknown', + type, mime_type: file.content_type, - blurhash: null, - } as MediaAttachment; + }); openModal('MEDIA', { media: [mediaAttachment], diff --git a/packages/pl-fe/src/pages/fun/circle.tsx b/packages/pl-fe/src/pages/fun/circle.tsx index fca439fdf..9627f281c 100644 --- a/packages/pl-fe/src/pages/fun/circle.tsx +++ b/packages/pl-fe/src/pages/fun/circle.tsx @@ -75,7 +75,7 @@ const CirclePage: React.FC = () => { const file = new File([blob!], 'interactions_circle.png', { type: 'image/png' }); dispatch(uploadFile(file, intl, (data) => { - dispatch(uploadComposeSuccess('compose-modal', data, file)); + dispatch(uploadComposeSuccess('compose-modal', data)); openModal('COMPOSE'); })); }, 'image/png'); diff --git a/packages/pl-fe/src/styles/new/drive.scss b/packages/pl-fe/src/styles/new/drive.scss index ba45ef453..e3fdcbf16 100644 --- a/packages/pl-fe/src/styles/new/drive.scss +++ b/packages/pl-fe/src/styles/new/drive.scss @@ -142,21 +142,8 @@ } &__label { - // margin-top: auto; - // overflow: hidden; - // display: inline; - // display: -webkit-box; - // -webkit-box-orient: vertical; - // -webkit-line-clamp: 3; - // max-width: 100%; - // text-overflow: ellipsis; - // border-radius: 0.25rem; - // background-color: rgb(var(--color-gray-900)); - // padding: 0.25rem 0.5rem; - // font-size: 0.75rem; - // line-height: 1rem; - // font-weight: 500; - // color: #fff; + overflow: hidden; + text-overflow: ellipsis; } &--active {