Skip to content
Merged
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
5 changes: 3 additions & 2 deletions src/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,7 @@ export interface DuplicatePublishView {

export enum ViewIconType {
Emoji = 0,
URL = 1,
Icon = 2,
}

Expand Down Expand Up @@ -967,11 +968,11 @@ export type Subscriptions = Subscription[];

export interface UpdatePagePayload {
name: string;
icon: {
icon?: {
ty: ViewIconType,
value: string,
};
extra: Partial<ViewExtra>;
extra?: Partial<ViewExtra>;
}

export interface ViewMetaCover {
Expand Down
17 changes: 11 additions & 6 deletions src/components/_shared/image-upload/EmbedLink.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { processUrl } from '@/utils/url';
import React, { useCallback, useState } from 'react';
import TextField from '@mui/material/TextField';
import { useTranslation } from 'react-i18next';
import Button from '@mui/material/Button';
import isURL from 'validator/lib/isURL';

export function EmbedLink ({
export function EmbedLink({
onDone,
onEscape,
defaultLink,
placeholder,
validator,
}: {
defaultLink?: string;
onDone?: (value: string) => void;
onEscape?: () => void;
placeholder?: string;
validator?: (url: string) => boolean;
}) {
const { t } = useTranslation();

Expand All @@ -25,20 +27,23 @@ export function EmbedLink ({
const value = e.target.value;

setValue(value);
setError(!isURL(value, { require_protocol: true }));
const urlValid = !!processUrl(value);
const customValid = validator ? validator(value) : true;

setError(!urlValid || !customValid);
},
[setValue, setError],
[setValue, setError, validator],
);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !error && value) {
if(e.key === 'Enter' && !error && value) {
e.preventDefault();
e.stopPropagation();
onDone?.(value);
}

if (e.key === 'Escape') {
if(e.key === 'Escape') {
onEscape?.();
}
},
Expand Down
16 changes: 16 additions & 0 deletions src/components/_shared/view-icon/PageIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ function PageIcon({
return null;
}, [view]);

const img = useMemo(() => {
if(view.icon && view.icon.ty === ViewIconType.URL && view.icon.value) {
return <img
className={className}
src={view.icon.value}
alt="icon"
/>;
}

return null;
}, [className, view.icon]);

const isFlag = useMemo(() => {
return emoji ? isFlagEmoji(emoji) : false;
}, [emoji]);
Expand Down Expand Up @@ -77,6 +89,10 @@ function PageIcon({
</>;
}

if(img) {
return img;
}

if(icon) {
return icon;
}
Expand Down
19 changes: 13 additions & 6 deletions src/components/editor/components/blocks/video/VideoBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { notify } from '@/components/_shared/notify';
import { usePopoverContext } from '@/components/editor/components/block-popover/BlockPopoverContext';
import VideoEmpty from '@/components/editor/components/blocks/video/VideoEmpty';
import { EditorElementProps, VideoBlockNode } from '@/components/editor/editor.type';
import React, { forwardRef, memo, useCallback, useMemo, useRef, lazy } from 'react';
import React, { forwardRef, memo, useCallback, useMemo, useRef } from 'react';
import { Element } from 'slate';
import { useReadOnly, useSlateStatic } from 'slate-react';

const VideoRender = lazy(() => import('@/components/editor/components/blocks/video/VideoRender'));
import VideoRender from './VideoRender';

export const VideoBlock = memo(forwardRef<HTMLDivElement, EditorElementProps<VideoBlockNode>>(({
node,
Expand All @@ -20,6 +20,7 @@ export const VideoBlock = memo(forwardRef<HTMLDivElement, EditorElementProps<Vid
const { blockId, data } = node;
const { url, align } = data || {};
const editor = useSlateStatic() as YjsEditor;
const [error, setError] = React.useState<string | undefined>(undefined);

const readOnly = useReadOnly() || editor.isElementReadOnly(node as unknown as Element);

Expand All @@ -44,7 +45,7 @@ export const VideoBlock = memo(forwardRef<HTMLDivElement, EditorElementProps<Vid

const handleClick = useCallback(async() => {
try {
if(!url) {
if(!url || error) {
if(!readOnly && containerRef.current) {
openPopover(blockId, BlockType.VideoBlock, containerRef.current);
}
Expand All @@ -56,7 +57,7 @@ export const VideoBlock = memo(forwardRef<HTMLDivElement, EditorElementProps<Vid
} catch(e: any) {
notify.error(e.message);
}
}, [url, readOnly, openPopover, blockId]);
}, [url, error, readOnly, openPopover, blockId]);

return (
<div
Expand All @@ -67,9 +68,15 @@ export const VideoBlock = memo(forwardRef<HTMLDivElement, EditorElementProps<Vid
>
<div
contentEditable={false}
className={`embed-block relative ${alignCss} ${url ? '!bg-transparent !border-none !rounded-none' : 'p-4'}`}
className={`embed-block relative ${alignCss} ${url && !error ? '!bg-transparent !border-none !rounded-none' : 'p-4'}`}
>
{url ? <VideoRender node={node} /> : <VideoEmpty node={node} />}
{url && !error ? <VideoRender
node={node}
onError={setError}
/> : <VideoEmpty
error={error}
node={node}
/>}
</div>
<div
ref={ref}
Expand Down
8 changes: 4 additions & 4 deletions src/components/editor/components/blocks/video/VideoEmpty.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { YjsEditor } from '@/application/slate-yjs';
import { VideoBlockNode } from '@/components/editor/editor.type';
import React from 'react';
import { ReactComponent as ImageIcon } from '@/assets/image.svg';
import { ReactComponent as ImageIcon } from '@/assets/video.svg';
import { useTranslation } from 'react-i18next';
import { Element } from 'slate';
import { useReadOnly, useSlateStatic } from 'slate-react';

function VideoEmpty({ node }: { node: VideoBlockNode }) {
function VideoEmpty({ node, error }: { node: VideoBlockNode; error?: string }) {
const { t } = useTranslation();
const editor = useSlateStatic() as YjsEditor;

Expand All @@ -16,11 +16,11 @@ function VideoEmpty({ node }: { node: VideoBlockNode }) {
<>
<div
className={
`flex w-full select-none items-center gap-4 text-text-caption ${readOnly ? 'cursor-not-allowed' : 'cursor-pointer'}`
`flex w-full select-none items-center gap-4 ${readOnly ? 'cursor-not-allowed' : 'cursor-pointer'} ${error ? 'text-function-error' : 'text-text-caption'}`
}
>
<ImageIcon className={'w-6 h-6'} />
{t('embedAVideo')}
{error || t('embedAVideo')}
</div>
</>
);
Expand Down
10 changes: 8 additions & 2 deletions src/components/editor/components/blocks/video/VideoRender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import ReactPlayer from 'react-player';

function VideoRender({
node,
onError,
}: {
node: VideoBlockNode
node: VideoBlockNode;
onError?: (e: string) => void;
}) {
const editor = useSlateStatic() as YjsEditor;
const readOnly = useReadOnly() || editor.isElementReadOnly(node as unknown as Element);
Expand Down Expand Up @@ -53,6 +55,7 @@ function VideoRender({
style={{
width: node.data.width ? `${node.data.width}px` : '100%',
}}
contentEditable={false}
onMouseEnter={() => setShowToolbar(true)}
onMouseLeave={() => setShowToolbar(false)}
className={`image-render w-full h-full relative min-h-[100px]`}
Expand All @@ -68,7 +71,10 @@ function VideoRender({
ref={ref}
className={'w-full absolute left-0 top-0 h-full'}
>
<ReactPlayer {...playerProps} />
<ReactPlayer {...playerProps} onError={() => {
if(onError) onError('The video embed couldn\'t be loaded');
}}
/>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function useHoverControls({ disabled }: { disabled: boolean; }) {

const slateEditorDom = ReactEditor.toDOMNode(editor, editor);

if (!ref.current) return;
if(!ref.current) return;

ref.current.style.top = `${top + slateEditorDom.offsetTop}px`;
ref.current.style.left = `${left + slateEditorDom.offsetLeft - 64}px`;
Expand All @@ -30,7 +30,7 @@ export function useHoverControls({ disabled }: { disabled: boolean; }) {
const close = useCallback(() => {
const el = ref.current;

if (!el) return;
if(!el) return;

el.style.opacity = '0';
el.style.pointerEvents = 'none';
Expand All @@ -40,17 +40,17 @@ export function useHoverControls({ disabled }: { disabled: boolean; }) {

useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (disabled) return;
if(disabled) return;
const el = ref.current;

if (!el) return;
if(!el) return;

let range: Range | null = null;
let node: Element | null = null;

try {
range = ReactEditor.findEventRange(editor, e);
if (!range) {
if(!range) {
throw new Error('No range found');
}
} catch {
Expand All @@ -60,7 +60,7 @@ export function useHoverControls({ disabled }: { disabled: boolean; }) {
const isOverRightBoundary = e.clientX > rect.right - 96 && e.clientX < rect.right;
let newX = e.clientX;

if (isOverLeftBoundary || isOverRightBoundary) {
if(isOverLeftBoundary || isOverRightBoundary) {
newX = rect.left + editorDom.clientWidth / 2;
}

Expand All @@ -71,36 +71,36 @@ export function useHoverControls({ disabled }: { disabled: boolean; }) {

}

if (!range && !node) {
if(!range && !node) {
console.warn('No range and node found');
return;
} else if (range) {
} else if(range) {
const match = editor.above({
match: (n) => {
return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined;
},
at: range,
});

if (!match) {
if(!match) {
close();
return;
}

node = match[0];
}

if (!node) {
if(!node) {
close();
return;
}

const blockElement = ReactEditor.toDOMNode(editor, node);

if (!blockElement) return;
if(!blockElement) return;
const shouldSkipTypes = [BlockType.TableBlock, BlockType.GridBlock, BlockType.CalendarBlock, BlockType.BoardBlock, BlockType.SimpleTableBlock];

if (shouldSkipTypes.some((type) => blockElement.closest(`[data-block-type="${type}"]`))) {
if(shouldSkipTypes.some((type) => blockElement.closest(`[data-block-type="${type}"]`))) {
close();
return;

Expand All @@ -117,7 +117,7 @@ export function useHoverControls({ disabled }: { disabled: boolean; }) {

const dom = ReactEditor.toDOMNode(editor, editor);

if (!disabled) {
if(!disabled) {
dom.addEventListener('mousemove', handleMouseMove);
dom.parentElement?.addEventListener('mouseleave', close);
getScrollParent(dom)?.addEventListener('scroll', close);
Expand All @@ -133,16 +133,23 @@ export function useHoverControls({ disabled }: { disabled: boolean; }) {
useEffect(() => {
let observer: MutationObserver | null = null;

if (hoveredBlockId) {
const [node] = findSlateEntryByBlockId(editor, hoveredBlockId);
const dom = ReactEditor.toDOMNode(editor, node);
if(hoveredBlockId) {
try {
const [node] = findSlateEntryByBlockId(editor, hoveredBlockId);

if (dom.parentElement) {
observer = new MutationObserver(close);
if(!node) return;

observer.observe(dom.parentElement, {
childList: true,
});
const dom = ReactEditor.toDOMNode(editor, node);

if(dom.parentElement) {
observer = new MutationObserver(close);

observer.observe(dom.parentElement, {
childList: true,
});
}
} catch(e) {
console.error(e);
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/components/editor/plugins/withPasted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ export const withPasted = (editor: ReactEditor) => {
insertFragment(editor, [{
type: BlockType.VideoBlock,
data: { url: text } as VideoBlockData,
children: [{ text: '' }],
children: [{
text: '',
}],
}]);
return true;
}
Expand Down
Loading
Loading