Skip to content

Commit

Permalink
Merge pull request #1150 from tloncorp/dd/image-upload-buttons
Browse files Browse the repository at this point in the history
groups: s3 image upload buttons
  • Loading branch information
jamesacklin authored Nov 15, 2022
2 parents 83edc56 + 8c4287c commit 11f16fd
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 67 deletions.
2 changes: 1 addition & 1 deletion ui/src/components/CoverImageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default function CoverImageInput({
{...register('image')}
defaultValue={url}
placeholder="Insert URL Here..."
className="input-inner w-full"
className="input-inner w-full p-0"
/>
{loaded && hasCredentials ? (
<button
Expand Down
26 changes: 11 additions & 15 deletions ui/src/components/ImageOrColorField.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import cn from 'classnames';
import React, { useEffect, useState } from 'react';
import { isValidUrl } from '@/logic/utils';
import { FieldPath, FieldValues, useFormContext } from 'react-hook-form';
import { ColorPickerField } from './ColorPicker';
import ImageURLUploadField from './ImageURLUploadField';
import XIcon from './icons/XIcon';
import LinkIcon from './icons/LinkIcon';

export type ImageOrColorFieldState = 'image' | 'color';

Expand All @@ -23,12 +22,14 @@ export default function ImageOrColorField<FormType extends FieldValues>({
}: ImageOrColorFieldProps<FormType>) {
const {
register,
watch,
setValue,
formState: { errors },
} = useFormContext<FormType>();
const [type, setType] = useState<ImageOrColorFieldState>('color');
const status = state || type;
const setStatus = setState || setType;
const watchValue = watch(fieldName);

useEffect(() => {
setStatus('color');
Expand All @@ -54,25 +55,20 @@ export default function ImageOrColorField<FormType extends FieldValues>({
<>
<div className="flex w-full items-center space-x-2">
{status === 'image' ? (
<div className="input flex w-full items-center">
<div className="flex items-center justify-center">
<LinkIcon className="h-4 w-4 text-gray-600" />
</div>
<input
className={cn('input-inner grow rounded-none py-0', {})}
placeholder="Paste Image URL Here"
{...register(fieldName, {
required: true,
validate: (value) => isValidUrl(value),
})}
/>
<>
<button
className="flex items-center justify-center"
onClick={handleColorIconType}
>
<XIcon className="h-4 w-4" />
</button>
</div>
<ImageURLUploadField
formRegister={register}
formSetValue={setValue}
formWatchURL={watchValue}
formValue={fieldName}
/>
</>
) : null}
{status === 'color' ? (
<div className="input flex w-full items-center rounded-lg px-1 py-0.5">
Expand Down
96 changes: 96 additions & 0 deletions ui/src/components/ImageURLUploadField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useState, useRef, useEffect } from 'react';
import { findLast } from 'lodash';
import cn from 'classnames';
import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
import { isValidUrl } from '@/logic/utils';
import LinkIcon from '@/components/icons/LinkIcon';
import useFileUpload from '@/logic/useFileUpload';
import { useFileStore } from '@/state/storage';
import LoadingSpinner from '@/components/LoadingSpinner/LoadingSpinner';
import { UploadErrorPopover } from '@/chat/ChatInput/ChatInput';

interface ImageURLUploadFieldProps {
formRegister: UseFormRegister<any>;
formWatchURL: string | null | undefined;
formSetValue: UseFormSetValue<any>;
formValue: string;
}

export default function ImageURLUploadField({
formRegister,
formWatchURL,
formSetValue,
formValue,
}: ImageURLUploadFieldProps) {
const [isFocused, setIsFocused] = useState(false);
const urlHasLength = formWatchURL?.length;

const [uploadError, setUploadError] = useState<string | null>(null);
const { loaded, hasCredentials, promptUpload } = useFileUpload();
const fileId = useRef(`chat-input-${Math.floor(Math.random() * 1000000)}`);
const mostRecentFile = useFileStore((state) =>
findLast(state.files, ['for', fileId.current])
);

useEffect(() => {
if (
mostRecentFile &&
mostRecentFile.status === 'error' &&
mostRecentFile.errorMessage
) {
setUploadError(mostRecentFile.errorMessage);
}

if (mostRecentFile && mostRecentFile.status === 'success') {
formSetValue(formValue, mostRecentFile.url, {
shouldDirty: true,
shouldTouch: true,
});
}
}, [mostRecentFile, formSetValue, formValue]);

return (
<div className="input relative flex w-full items-center justify-end">
<input
className={cn('input-inner grow p-0')}
onFocus={() => setIsFocused(true)}
{...formRegister(formValue, {
onBlur: () => setIsFocused(false),
validate: (value: string) =>
value && value.length ? isValidUrl(value) : true,
})}
/>
{!isFocused && !urlHasLength ? (
<div className="pointer-events-none absolute left-[0.5625rem] flex cursor-pointer items-center">
<LinkIcon className="mr-1 inline h-4 w-4 fill-gray-100" />
<span className="pointer-events-none">Paste an image URL</span>
</div>
) : null}
{loaded && hasCredentials ? (
<button
title={'Upload an image'}
className="small-button whitespace-nowrap"
aria-label="Add attachment"
onClick={(e) => {
e.preventDefault();
promptUpload(fileId.current);
}}
>
{mostRecentFile && mostRecentFile.status === 'loading' ? (
<LoadingSpinner secondary="black" className="h-4 w-4" />
) : (
'Upload Image'
)}
</button>
) : null}
{uploadError ? (
<div className="absolute mr-2">
<UploadErrorPopover
errorMessage={uploadError}
setUploadError={setUploadError}
/>
</div>
) : null}
</div>
);
}
57 changes: 55 additions & 2 deletions ui/src/diary/DiaryImageNode.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import React, { useState, useEffect, useRef } from 'react';
import { useCalm } from '@/state/settings';
import { findLast } from 'lodash';
import LinkIcon from '@/components/icons/LinkIcon';
import AsteriskIcon from '@/components/icons/Asterisk16Icon';
import { Node, NodeViewProps } from '@tiptap/core';
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
import cn from 'classnames';
import React, { useState, useEffect, useRef } from 'react';
import useFileUpload from '@/logic/useFileUpload';
import { useFileStore } from '@/state/storage';
import LoadingSpinner from '@/components/LoadingSpinner/LoadingSpinner';
import { UploadErrorPopover } from '@/chat/ChatInput/ChatInput';
import useDiaryNode from './useDiaryNode';

function DiaryImageComponent(props: NodeViewProps) {
Expand All @@ -15,13 +20,36 @@ function DiaryImageComponent(props: NodeViewProps) {
const [src, setSrc] = useState(null as string | null);
const image = useRef<HTMLImageElement>(null);
const calm = useCalm();
const [uploadError, setUploadError] = useState<string | null>(null);
const { loaded, hasCredentials, promptUpload } = useFileUpload();
const fileId = useRef(`chat-input-${Math.floor(Math.random() * 1000000)}`);
const mostRecentFile = useFileStore((state) =>
findLast(state.files, ['for', fileId.current])
);
const onError = () => {
setError(true);
};
useEffect(() => {
setError(false);
}, [src]);

useEffect(() => {
if (
mostRecentFile &&
mostRecentFile.status === 'error' &&
mostRecentFile.errorMessage
) {
setUploadError(mostRecentFile.errorMessage);
}

if (mostRecentFile && mostRecentFile.status === 'success') {
setSrc(mostRecentFile.url);
if (bind.ref.current) {
bind.ref.current.value = mostRecentFile.url;
}
}
}, [mostRecentFile, bind]);

useEffect(() => {
if (!selected) {
setSrc(bind.value);
Expand Down Expand Up @@ -62,7 +90,7 @@ function DiaryImageComponent(props: NodeViewProps) {
className
)}
>
<div className="absolute inset-x-4 bottom-4 flex h-8 items-center space-x-2 rounded-lg border border-gray-100 bg-white px-2">
<div className="input absolute inset-x-4 bottom-4 flex h-8 items-center space-x-2 rounded-lg border border-gray-100 bg-white px-2">
<LinkIcon className="h-4 w-4" />
<input
className="input-transparent grow"
Expand All @@ -71,6 +99,31 @@ function DiaryImageComponent(props: NodeViewProps) {
onKeyDown={onKeyDown}
placeholder="Enter an image/embed/web URL"
/>
{loaded && hasCredentials ? (
<button
title={'Upload an image'}
className="small-button whitespace-nowrap"
aria-label="Add attachment"
onClick={(e) => {
e.preventDefault();
promptUpload(fileId.current);
}}
>
{mostRecentFile && mostRecentFile.status === 'loading' ? (
<LoadingSpinner secondary="black" className="h-4 w-4" />
) : (
'Upload Image'
)}
</button>
) : null}
{uploadError ? (
<div className="absolute mr-2">
<UploadErrorPopover
errorMessage={uploadError}
setUploadError={setUploadError}
/>
</div>
) : null}
{error ? (
<div className="flex space-x-2">
<AsteriskIcon className="h-4 w-4" />
Expand Down
68 changes: 61 additions & 7 deletions ui/src/heap/NewCurioForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState, useRef } from 'react';
import cn from 'classnames';
import { intersection } from 'lodash';
import { intersection, findLast } from 'lodash';
import { useForm } from 'react-hook-form';
import LinkIcon from '@/components/icons/LinkIcon';
import { useHeapPerms, useHeapState } from '@/state/heap/heap';
Expand All @@ -18,7 +18,11 @@ import {
NewCurioFormSchema,
TEXT,
} from '@/types/heap';
import LoadingSpinner from '@/components/LoadingSpinner/LoadingSpinner';
import { UploadErrorPopover } from '@/chat/ChatInput/ChatInput';
import { useHeapDisplayMode } from '@/state/settings';
import { useFileStore } from '@/state/storage';
import useFileUpload from '@/logic/useFileUpload';
import HeapTextInput from './HeapTextInput';

export default function NewCurioForm() {
Expand All @@ -39,11 +43,36 @@ export default function NewCurioForm() {
perms.writers.length === 0 ||
intersection(perms.writers, vessel.sects).length !== 0;

const { register, handleSubmit, reset, watch } = useForm<NewCurioFormSchema>({
defaultValues: {
content: '',
},
});
const [uploadError, setUploadError] = useState<string | null>(null);
const { loaded, hasCredentials, promptUpload } = useFileUpload();
const fileId = useRef(`chat-input-${Math.floor(Math.random() * 1000000)}`);
const mostRecentFile = useFileStore((state) =>
findLast(state.files, ['for', fileId.current])
);
const { register, handleSubmit, reset, watch, setValue } =
useForm<NewCurioFormSchema>({
defaultValues: {
content: '',
},
});

useEffect(() => {
if (
mostRecentFile &&
mostRecentFile.status === 'error' &&
mostRecentFile.errorMessage
) {
setUploadError(mostRecentFile.errorMessage);
}

if (mostRecentFile && mostRecentFile.status === 'success') {
setValue('content', mostRecentFile.url, {
shouldDirty: true,
shouldTouch: true,
});
}
}, [mostRecentFile, setValue]);

const { isPending, setPending, setReady } = useRequestState();
const onSubmit = useCallback(
async ({ content }: NewCurioFormSchema) => {
Expand Down Expand Up @@ -147,6 +176,31 @@ export default function NewCurioForm() {
onKeyDown={onKeyDown}
defaultValue={draftLink}
/>
{loaded && hasCredentials ? (
<button
title={'Upload an image'}
className="button absolute bottom-3 left-3 whitespace-nowrap rounded-md px-2 py-1"
aria-label="Add attachment"
onClick={(e) => {
e.preventDefault();
promptUpload(fileId.current);
}}
>
{mostRecentFile && mostRecentFile.status === 'loading' ? (
<LoadingSpinner secondary="black" className="h-4 w-4" />
) : (
'Upload Image'
)}
</button>
) : null}
{uploadError ? (
<div className="absolute mr-2">
<UploadErrorPopover
errorMessage={uploadError}
setUploadError={setUploadError}
/>
</div>
) : null}
<input
value={isPending ? 'Posting...' : 'Post'}
type="submit"
Expand Down
Loading

3 comments on commit 11f16fd

@vercel
Copy link

@vercel vercel bot commented on 11f16fd Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

homestead-storybook – ./ui

homestead-storybook-tlon.vercel.app
homestead-storybook.vercel.app
homestead-storybook-git-master-tlon.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 11f16fd Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

chatstead – ./ui

chatstead-git-master-tlon.vercel.app
chatstead.vercel.app
chatstead-tlon.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 11f16fd Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

homestead – ./ui

homestead-git-master-tlon.vercel.app
homestead-tlon.vercel.app
tlon-homestead.vercel.app

Please sign in to comment.