mirror of https://github.com/logseq/logseq
[wip] feat (whiteboards): tweet shape
parent
9820db95e2
commit
5b491ef54f
|
@ -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})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -19,6 +19,9 @@ export interface LogseqContextValue {
|
|||
levelLimit?: number
|
||||
endSeparator?: boolean
|
||||
}>
|
||||
Tweet: React.FC<{
|
||||
tweetId: string
|
||||
}>
|
||||
PageName: React.FC<{
|
||||
pageName: string
|
||||
}>
|
||||
|
|
|
@ -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({})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue