[wip] feat (whiteboards): tweet shape

pull/8279/head
Konstantinos Kaloutas 2023-01-05 17:25:26 +02:00
parent 9820db95e2
commit 5b491ef54f
8 changed files with 171 additions and 10 deletions

View File

@ -37,6 +37,10 @@
{:end-separator? (gobj/get props "endSeparator")
:level-limit (gobj/get props "levelLimit" 3)}))
(rum/defc tweet
[props]
(ui/tweet-embed (gobj/get props "tweetId")))
(rum/defc block-reference
[props]
(block/block-reference {} (gobj/get props "blockId") nil))
@ -71,6 +75,7 @@
(def tldraw-renderers {:Page page-cp
:Block block-cp
:Breadcrumb breadcrumb
:Tweet tweet
:PageName page-name-link
:BacklinksCount references-count
:BlockReference block-reference})

View File

@ -14,6 +14,7 @@ import type {
Shape,
TextShape,
YouTubeShape,
TweetShape,
} from '../../lib'
import { Button } from '../Button'
import { TablerIcon } from '../icons'
@ -39,6 +40,7 @@ export const contextBarActionTypes = [
'ScaleLevel',
'TextStyle',
'YoutubeLink',
'TwitterLink',
'IFrameSource',
'LogseqPortalViewMode',
'ArrowMode',
@ -46,7 +48,7 @@ export const contextBarActionTypes = [
] as const
type ContextBarActionType = typeof contextBarActionTypes[number]
const singleShapeActions: ContextBarActionType[] = ['Edit', 'YoutubeLink', 'IFrameSource', 'Links']
const singleShapeActions: ContextBarActionType[] = ['Edit', 'YoutubeLink', 'TwitterLink', 'IFrameSource', 'Links']
const contextBarActionMapping = new Map<ContextBarActionType, React.FC>()
@ -62,6 +64,7 @@ export const shapeMapping: Record<ShapeType, ContextBarActionType[]> = {
'Links',
],
youtube: ['YoutubeLink', 'Links'],
tweet: ['TwitterLink', 'Links'],
iframe: ['IFrameSource', 'Links'],
box: ['Edit', 'TextStyle', 'Swatch', 'ScaleLevel', 'NoFill', 'StrokeType', 'Links'],
ellipse: ['Edit', 'TextStyle', 'Swatch', 'ScaleLevel', 'NoFill', 'StrokeType', 'Links'],
@ -296,6 +299,33 @@ const YoutubeLinkAction = observer(() => {
)
})
const TwitterLinkAction = observer(() => {
const app = useApp<Shape>()
const shape = filterShapeByAction<TweetShape>(app.selectedShapesArray, 'TwitterLink')[0]
const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
shape.onTwitterLinkChange(e.target.value)
app.persist()
}, [])
return (
<span className="flex gap-3">
<TextInput
title="Twitter Link"
className="tl-twitter-link"
value={`${shape.props.url}`}
onChange={handleChange}
/>
<Button
tooltip="Open Twitter Link"
type="button"
onClick={() => window.logseq?.api?.open_external_link?.(shape.props.url)}
>
<TablerIcon name="external-link" />
</Button>
</span>
)
})
const NoFillAction = observer(() => {
const app = useApp<Shape>()
const shapes = filterShapeByAction<BoxShape | PolygonShape | EllipseShape>(
@ -511,6 +541,7 @@ contextBarActionMapping.set('AutoResizing', AutoResizingAction)
contextBarActionMapping.set('LogseqPortalViewMode', LogseqPortalViewModeAction)
contextBarActionMapping.set('ScaleLevel', ScaleLevelAction)
contextBarActionMapping.set('YoutubeLink', YoutubeLinkAction)
contextBarActionMapping.set('TwitterLink', TwitterLinkAction)
contextBarActionMapping.set('IFrameSource', IFrameSourceAction)
contextBarActionMapping.set('NoFill', NoFillAction)
contextBarActionMapping.set('Swatch', SwatchAction)

View File

@ -20,6 +20,9 @@ import {
LogseqPortalShape,
VideoShape,
YouTubeShape,
YOUTUBE_REGEX,
TweetShape,
TWITTER_REGEX,
type Shape,
} from '../lib'
import { LogseqContext, LogseqContextValue } from '../lib/logseq-context'
@ -269,12 +272,7 @@ const handleCreatingShapes = async (
async function tryCreateShapeFromURL(rawText: string) {
if (isValidURL(rawText) && !(shiftKey || fromDrop)) {
const isYoutubeUrl = (url: string) => {
const youtubeRegex =
/^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
return youtubeRegex.test(url)
}
if (isYoutubeUrl(rawText)) {
if (YOUTUBE_REGEX.test(rawText)) {
return [
{
...YouTubeShape.defaultProps,
@ -284,6 +282,16 @@ const handleCreatingShapes = async (
]
}
if (TWITTER_REGEX.test(rawText)) {
return [
{
...TweetShape.defaultProps,
url: rawText,
point: [point[0], point[1]],
},
]
}
return [
{
...IFrameShape.defaultProps,

View File

@ -19,6 +19,9 @@ export interface LogseqContextValue {
levelLimit?: number
endSeparator?: boolean
}>
Tweet: React.FC<{
tweetId: string
}>
PageName: React.FC<{
pageName: string
}>

View File

@ -0,0 +1,102 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
import { HTMLContainer, TLComponentProps } from '@tldraw/react'
import { action, computed } from 'mobx'
import { observer } from 'mobx-react-lite'
import { withClampedStyles } from './style-props'
import { LogseqContext } from '../logseq-context'
import * as React from 'react'
export const TWITTER_REGEX = /https?:\/\/twitter.com\/[0-9a-zA-Z_]{1,20}\/status\/([0-9]*)/
export interface TweetShapeProps extends TLBoxShapeProps {
type: 'tweet'
url: string
}
export class TweetShape extends TLBoxShape<TweetShapeProps> {
static id = 'tweet'
static defaultProps: TweetShapeProps = {
id: 'tweet',
type: 'tweet',
parentId: 'page',
point: [0, 0],
size: [331, 290],
url: '',
}
canFlip = false
canEdit = true
@computed get embedId() {
const url = this.props.url
const match = url.match(TWITTER_REGEX)
const embedId = match?.[1] ?? url ?? ''
return embedId
}
@action onTwitterLinkChange = (url: string) => {
this.update({ url, size: TweetShape.defaultProps.size })
}
ReactComponent = observer(({ events, isErasing, isEditing, isSelected }: TLComponentProps) => {
const {
renderers: { Tweet },
} = React.useContext(LogseqContext)
return (
<HTMLContainer
style={{
overflow: 'hidden',
pointerEvents: 'all',
opacity: isErasing ? 0.2 : 1,
}}
{...events}
>
<div
className="rounded-lg w-full h-full relative overflow-hidden shadow-xl"
style={{
pointerEvents: isEditing ? 'all' : 'none',
userSelect: 'none',
}}
>
{this.embedId ? (
<Tweet tweetId={this.embedId}/>
) : (null)}
</div>
</HTMLContainer>
)
})
ReactIndicator = observer(() => {
const {
props: {
size: [w, h],
},
} = this
return <rect width={w} height={h} fill="transparent" rx={8} ry={8} />
})
validateProps = (props: Partial<TweetShapeProps>) => {
if (props.size !== undefined) {
props.size[0] = Math.max(props.size[0], 1)
props.size[1] = Math.max(props.size[1], 1)
}
return withClampedStyles(this, props)
}
getShapeSVGJsx() {
// Do not need to consider the original point here
const bounds = this.getBounds()
const embedId = this.embedId
if (embedId) {
return (
<g></g>
)
}
return super.getShapeSVGJsx({})
}
}

View File

@ -5,6 +5,8 @@ import { action, computed } from 'mobx'
import { observer } from 'mobx-react-lite'
import { withClampedStyles } from './style-props'
export const YOUTUBE_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
export interface YouTubeShapeProps extends TLBoxShapeProps {
type: 'youtube'
url: string
@ -32,9 +34,7 @@ export class YouTubeShape extends TLBoxShape<YouTubeShapeProps> {
@computed get embedId() {
const url = this.props.url
const match = url.match(
/^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
)
const match = url.match(YOUTUBE_REGEX)
const embedId = match?.[1] ?? url ?? ''
return embedId
}

View File

@ -13,6 +13,7 @@ import { PolygonShape } from './PolygonShape'
import { TextShape } from './TextShape'
import { VideoShape } from './VideoShape'
import { YouTubeShape } from './YouTubeShape'
import { TweetShape } from './TweetShape'
export type Shape =
// | PenShape
@ -27,6 +28,7 @@ export type Shape =
| PolygonShape
| TextShape
| YouTubeShape
| TweetShape
| IFrameShape
| HTMLShape
| LogseqPortalShape
@ -46,6 +48,7 @@ export * from './PolygonShape'
export * from './TextShape'
export * from './VideoShape'
export * from './YouTubeShape'
export * from './TweetShape'
export const shapes: TLReactShapeConstructor<Shape>[] = [
// DotShape,
@ -59,6 +62,7 @@ export const shapes: TLReactShapeConstructor<Shape>[] = [
PolygonShape,
TextShape,
YouTubeShape,
TweetShape,
IFrameShape,
HTMLShape,
LogseqPortalShape,

View File

@ -901,12 +901,20 @@ html[data-theme='dark'] {
}
.tl-youtube-link,
.tl-twitter-link,
.tl-iframe-src {
@apply rounded-lg px-2 py-1;
color: var(--ls-primary-text-color);
box-shadow: 0 0 0 1px var(--ls-secondary-border-color);
}
.logseq-tldraw {
.twitter-tweet,
iframe {
margin: 0 !important;
}
}
.tl-hitarea-stroke {
fill: none;
stroke: transparent;