(1/2) Cursor Chat - Presence (#1487)
This PR adds support for seeing **another user**'s chat messages. It's part 1 of two PRs relating to Cursor Chat. And it's needed for the much bigger part 2: https://github.com/tldraw/brivate/pull/1981 # Presence You can see another person's chat messages! ![2023-06-02 at 17 42 33 - Blush Capybara](https://github.com/tldraw/tldraw/assets/15892272/8f3efb5f-9c05-459c-aa7e-24842be75e58) If they have a name, it gets popped on top. ![2023-06-02 at 17 45 34 - Sapphire Meerkat](https://github.com/tldraw/tldraw/assets/15892272/749bd924-c1f5-419b-a028-1fafe1b61292) That's it! With this PR, there's no way of actually *typing* your chat messages. That comes with the [next one](https://github.com/tldraw/brivate/pull/1981)! # Admin ### To-do - [x] Store chat message - [x] Allow overflowing chat - [x] Presence for chat message - [x] Display chat message to others ### Change Type - [x] `minor` — New Feature ### Test Plan To test this, I recommend checking out both `lu/cursor-chat` branches, and opening two browser sessions in the same shared project. 1. In one session, type some cursor chat by pressing the Enter key while on the canvas (and typing). 2. On the other session, check that you can see the chat message appear. 3. Repeat this while being both named, and unnamed. I recommend just focusing on the visible presense in this PR. The [other PR](https://github.com/tldraw/brivate/pull/1981) is where we can focus about how we _input_ the cursor chat. ### Release Notes - [dev] Added support for cursor chat presence. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
21377c0f22
commit
3bbb34eba8
15 changed files with 238 additions and 39 deletions
|
@ -1,15 +1,16 @@
|
||||||
|
/* eslint-disable no-inner-declarations */
|
||||||
import { InstancePresenceRecordType, Tldraw } from '@tldraw/tldraw'
|
import { InstancePresenceRecordType, Tldraw } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/editor.css'
|
import '@tldraw/tldraw/editor.css'
|
||||||
import '@tldraw/tldraw/ui.css'
|
import '@tldraw/tldraw/ui.css'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
|
|
||||||
const SHOW_MOVING_CURSOR = true
|
const USER_NAME = 'huppy da arrow'
|
||||||
const CURSOR_SPEED = 0.5
|
const MOVING_CURSOR_SPEED = 0.25 // 0 is stopped, 1 is full send
|
||||||
const CIRCLE_RADIUS = 100
|
const MOVING_CURSOR_RADIUS = 100
|
||||||
const UPDATE_FPS = 60
|
const CURSOR_CHAT_MESSAGE = 'Hey, I think this is just great.'
|
||||||
|
|
||||||
export default function UserPresenceExample() {
|
export default function UserPresenceExample() {
|
||||||
const rTimeout = useRef<any>(-1)
|
const rRaf = useRef<any>(-1)
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<Tldraw
|
<Tldraw
|
||||||
|
@ -22,39 +23,62 @@ export default function UserPresenceExample() {
|
||||||
id: InstancePresenceRecordType.createId(editor.store.id),
|
id: InstancePresenceRecordType.createId(editor.store.id),
|
||||||
currentPageId: editor.currentPageId,
|
currentPageId: editor.currentPageId,
|
||||||
userId: 'peer-1',
|
userId: 'peer-1',
|
||||||
userName: 'Peer 1',
|
userName: USER_NAME,
|
||||||
cursor: { x: 0, y: 0, type: 'default', rotation: 0 },
|
cursor: { x: 0, y: 0, type: 'default', rotation: 0 },
|
||||||
|
chatMessage: CURSOR_CHAT_MESSAGE,
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.store.put([peerPresence])
|
editor.store.put([peerPresence])
|
||||||
|
|
||||||
// Make the fake user's cursor rotate in a circle
|
// Make the fake user's cursor rotate in a circle
|
||||||
if (rTimeout.current) {
|
const raf = rRaf.current
|
||||||
clearTimeout(rTimeout.current)
|
cancelAnimationFrame(raf)
|
||||||
}
|
|
||||||
|
if (MOVING_CURSOR_SPEED > 0 || CURSOR_CHAT_MESSAGE) {
|
||||||
|
function loop() {
|
||||||
|
let cursor = peerPresence.cursor
|
||||||
|
let chatMessage = peerPresence.chatMessage
|
||||||
|
|
||||||
if (SHOW_MOVING_CURSOR) {
|
|
||||||
rTimeout.current = setInterval(() => {
|
|
||||||
const k = 1000 / CURSOR_SPEED
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const t = (now % k) / k
|
|
||||||
// rotate in a circle
|
if (MOVING_CURSOR_SPEED > 0) {
|
||||||
|
const k = 1000 / MOVING_CURSOR_SPEED
|
||||||
|
const t = (now % k) / k
|
||||||
|
|
||||||
|
cursor = {
|
||||||
|
...peerPresence.cursor,
|
||||||
|
x: 150 + Math.cos(t * Math.PI * 2) * MOVING_CURSOR_RADIUS,
|
||||||
|
y: 150 + Math.sin(t * Math.PI * 2) * MOVING_CURSOR_RADIUS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CURSOR_CHAT_MESSAGE) {
|
||||||
|
const k = 1000
|
||||||
|
const t = (now % (k * 3)) / k
|
||||||
|
chatMessage =
|
||||||
|
t < 1
|
||||||
|
? ''
|
||||||
|
: t > 2
|
||||||
|
? CURSOR_CHAT_MESSAGE
|
||||||
|
: CURSOR_CHAT_MESSAGE.slice(0, Math.ceil((t - 1) * CURSOR_CHAT_MESSAGE.length))
|
||||||
|
}
|
||||||
|
|
||||||
editor.store.put([
|
editor.store.put([
|
||||||
{
|
{
|
||||||
...peerPresence,
|
...peerPresence,
|
||||||
cursor: {
|
cursor,
|
||||||
...peerPresence.cursor,
|
chatMessage,
|
||||||
x: 150 + Math.cos(t * Math.PI * 2) * CIRCLE_RADIUS,
|
|
||||||
y: 150 + Math.sin(t * Math.PI * 2) * CIRCLE_RADIUS,
|
|
||||||
},
|
|
||||||
lastActivityTimestamp: now,
|
lastActivityTimestamp: now,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}, 1000 / UPDATE_FPS)
|
|
||||||
|
rRaf.current = requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
rRaf.current = requestAnimationFrame(loop)
|
||||||
} else {
|
} else {
|
||||||
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||||
|
rRaf.current = setInterval(() => {
|
||||||
rTimeout.current = setInterval(() => {
|
|
||||||
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
"action.leave-shared-project": "Leave shared project",
|
"action.leave-shared-project": "Leave shared project",
|
||||||
"action.new-project": "New project",
|
"action.new-project": "New project",
|
||||||
"action.new-shared-project": "New shared project",
|
"action.new-shared-project": "New shared project",
|
||||||
|
"action.open-cursor-chat": "Cursor chat",
|
||||||
"action.open-file": "Open file",
|
"action.open-file": "Open file",
|
||||||
"action.pack": "Pack",
|
"action.pack": "Pack",
|
||||||
"action.paste": "Paste",
|
"action.paste": "Paste",
|
||||||
|
@ -278,6 +279,7 @@
|
||||||
"shortcuts-dialog.tools": "Tools",
|
"shortcuts-dialog.tools": "Tools",
|
||||||
"shortcuts-dialog.transform": "Transform",
|
"shortcuts-dialog.transform": "Transform",
|
||||||
"shortcuts-dialog.view": "View",
|
"shortcuts-dialog.view": "View",
|
||||||
|
"shortcuts-dialog.collaboration": "Collaboration",
|
||||||
"home-project-dialog.title": "Home project",
|
"home-project-dialog.title": "Home project",
|
||||||
"home-project-dialog.description": "This is your local home project. It's just for you!",
|
"home-project-dialog.description": "This is your local home project. It's just for you!",
|
||||||
"rename-project-dialog.title": "Rename project",
|
"rename-project-dialog.title": "Rename project",
|
||||||
|
@ -339,5 +341,6 @@
|
||||||
"vscode.file-open.backup": "Backup",
|
"vscode.file-open.backup": "Backup",
|
||||||
"vscode.file-open.backup-saved": "Backup saved",
|
"vscode.file-open.backup-saved": "Backup saved",
|
||||||
"vscode.file-open.backup-failed": "Backup failed: this is not a .tldr file.",
|
"vscode.file-open.backup-failed": "Backup failed: this is not a .tldr file.",
|
||||||
"vscode.file-open.dont-show-again": "Don't ask again"
|
"vscode.file-open.dont-show-again": "Don't ask again",
|
||||||
|
"cursor-chat.type-to-chat": "Type to chat..."
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,27 @@ https://alex.dytry.ch/toys/palette/?palette=%7B%22families%22:%5B%22black%22,%22
|
||||||
/* These cursor values get programmatically overridden */
|
/* These cursor values get programmatically overridden */
|
||||||
/* They're just here to help your editor autocomplete */
|
/* They're just here to help your editor autocomplete */
|
||||||
--tl-cursor: var(--tl-default-svg);
|
--tl-cursor: var(--tl-default-svg);
|
||||||
|
--tl-cursor-none: none;
|
||||||
|
--tl-cursor-default: default;
|
||||||
|
--tl-cursor-pointer: pointer;
|
||||||
|
--tl-cursor-cross: crosshair;
|
||||||
|
--tl-cursor-move: move;
|
||||||
|
--tl-cursor-grab: grab;
|
||||||
|
--tl-cursor-grabbing: grabbing;
|
||||||
|
--tl-cursor-text: text;
|
||||||
|
--tl-cursor-resize-edge: ew-resize;
|
||||||
|
--tl-cursor-resize-corner: nesw-resize;
|
||||||
|
--tl-cursor-ew-resize: ew-resize;
|
||||||
|
--tl-cursor-ns-resize: ns-resize;
|
||||||
|
--tl-cursor-nesw-resize: nesw-resize;
|
||||||
|
--tl-cursor-nwse-resize: nwse-resize;
|
||||||
|
--tl-cursor-rotate: pointer;
|
||||||
|
--tl-cursor-nwse-rotate: pointer;
|
||||||
|
--tl-cursor-nesw-rotate: pointer;
|
||||||
|
--tl-cursor-senw-rotate: pointer;
|
||||||
|
--tl-cursor-swne-rotate: pointer;
|
||||||
|
--tl-cursor-zoom-in: zoom-in;
|
||||||
|
--tl-cursor-zoom-out: zoom-out;
|
||||||
--tl-scale: calc(1 / var(--tl-zoom));
|
--tl-scale: calc(1 / var(--tl-zoom));
|
||||||
--tl-font-draw: 'tldraw_draw', sans-serif;
|
--tl-font-draw: 'tldraw_draw', sans-serif;
|
||||||
--tl-font-sans: 'tldraw_sans', sans-serif;
|
--tl-font-sans: 'tldraw_sans', sans-serif;
|
||||||
|
@ -746,7 +767,6 @@ input,
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rounded corners */
|
|
||||||
.tl-nametag {
|
.tl-nametag {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 16px;
|
top: 16px;
|
||||||
|
@ -754,15 +774,84 @@ input,
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
color: var(--color-selected-contrast);
|
padding: 3px 6px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
border-radius: 10px;
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
color: var(--color-selected-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-nametag-title {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: 13px;
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
padding: 0px 6px;
|
||||||
|
max-width: 120px;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
text-shadow: var(--tl-text-outline);
|
||||||
|
color: var(--color-selected-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-nametag-chat {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 13px;
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
color: var(--color-selected-contrast);
|
||||||
|
white-space: nowrap;
|
||||||
|
position: absolute;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
opacity: 1;
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-cursor-chat {
|
||||||
|
position: absolute;
|
||||||
|
color: var(--color-selected-contrast);
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-family);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: var(--layer-cursor);
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-left: 13px;
|
||||||
|
opacity: 1;
|
||||||
|
border: none;
|
||||||
|
user-select: text;
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-cursor-chat::selection {
|
||||||
|
background: var(--color-selected);
|
||||||
|
color: var(--color-selected-contrast);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-cursor-chat-fade {
|
||||||
|
/* Setting to zero causes it to immediately disappear */
|
||||||
|
/* Setting to near-zero causes it to fade out gradually */
|
||||||
|
opacity: 0.0001;
|
||||||
|
transition: opacity 5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-cursor-chat::placeholder {
|
||||||
|
color: var(--color-selected-contrast);
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
|
|
@ -10,23 +10,37 @@ export type TLCursorComponent = (props: {
|
||||||
zoom: number
|
zoom: number
|
||||||
color?: string
|
color?: string
|
||||||
name: string | null
|
name: string | null
|
||||||
|
chatMessage: string
|
||||||
}) => any | null
|
}) => any | null
|
||||||
|
|
||||||
const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name }) => {
|
const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name, chatMessage }) => {
|
||||||
const rDiv = useRef<HTMLDivElement>(null)
|
const rCursor = useRef<HTMLDivElement>(null)
|
||||||
useTransform(rDiv, point?.x, point?.y, 1 / zoom)
|
useTransform(rCursor, point?.x, point?.y, 1 / zoom)
|
||||||
|
|
||||||
if (!point) return null
|
if (!point) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={rDiv} className={classNames('tl-overlays__item', className)}>
|
<div ref={rCursor} className={classNames('tl-overlays__item', className)}>
|
||||||
<svg className="tl-cursor">
|
<svg className="tl-cursor">
|
||||||
<use href="#cursor" color={color} />
|
<use href="#cursor" color={color} />
|
||||||
</svg>
|
</svg>
|
||||||
{name !== null && name !== '' && (
|
{chatMessage ? (
|
||||||
<div className="tl-nametag" style={{ backgroundColor: color }}>
|
<>
|
||||||
{name}
|
{name && (
|
||||||
</div>
|
<div className="tl-nametag-title" style={{ color }}>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="tl-nametag-chat" style={{ backgroundColor: color }}>
|
||||||
|
{chatMessage}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
name && (
|
||||||
|
<div className="tl-nametag" style={{ backgroundColor: color }}>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -33,7 +33,7 @@ const Collaborator = track(function Collaborator({ userId }: { userId: string })
|
||||||
// if the collaborator is on another page, ignore them
|
// if the collaborator is on another page, ignore them
|
||||||
if (latestPresence.currentPageId !== editor.currentPageId) return null
|
if (latestPresence.currentPageId !== editor.currentPageId) return null
|
||||||
|
|
||||||
const { brush, scribble, selectedIds, userName, cursor, color } = latestPresence
|
const { brush, scribble, selectedIds, userName, cursor, color, chatMessage } = latestPresence
|
||||||
|
|
||||||
// Add a little padding to the top-left of the viewport
|
// Add a little padding to the top-left of the viewport
|
||||||
// so that the cursor doesn't get cut off
|
// so that the cursor doesn't get cut off
|
||||||
|
@ -63,6 +63,7 @@ const Collaborator = track(function Collaborator({ userId }: { userId: string })
|
||||||
color={color}
|
color={color}
|
||||||
zoom={zoomLevel}
|
zoom={zoomLevel}
|
||||||
name={userName !== 'New User' ? userName : null}
|
name={userName !== 'New User' ? userName : null}
|
||||||
|
chatMessage={chatMessage}
|
||||||
/>
|
/>
|
||||||
) : CollaboratorHint ? (
|
) : CollaboratorHint ? (
|
||||||
<CollaboratorHint
|
<CollaboratorHint
|
||||||
|
|
|
@ -941,6 +941,8 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
brush: Box2dModel | null;
|
brush: Box2dModel | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
chatMessage: string;
|
||||||
|
// (undocumented)
|
||||||
currentPageId: TLPageId;
|
currentPageId: TLPageId;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
cursor: TLCursor;
|
cursor: TLCursor;
|
||||||
|
@ -949,6 +951,8 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
followingUserId: null | string;
|
followingUserId: null | string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
isChatting: boolean;
|
||||||
|
// (undocumented)
|
||||||
isDebugMode: boolean;
|
isDebugMode: boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isFocusMode: boolean;
|
isFocusMode: boolean;
|
||||||
|
@ -1007,6 +1011,8 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
|
||||||
z: number;
|
z: number;
|
||||||
};
|
};
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
chatMessage: string;
|
||||||
|
// (undocumented)
|
||||||
color: string;
|
color: string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
currentPageId: TLPageId;
|
currentPageId: TLPageId;
|
||||||
|
|
|
@ -43,6 +43,7 @@ export const createPresenceStateDerivation =
|
||||||
},
|
},
|
||||||
lastActivityTimestamp: pointer.lastActivityTimestamp,
|
lastActivityTimestamp: pointer.lastActivityTimestamp,
|
||||||
screenBounds: instance.screenBounds,
|
screenBounds: instance.screenBounds,
|
||||||
|
chatMessage: instance.chatMessage,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1110,6 +1110,30 @@ describe('hoist opacity', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Adds chat message to presence', () => {
|
||||||
|
const { up, down } = instancePresenceMigrations.migrators[3]
|
||||||
|
|
||||||
|
test('up adds the chatMessage property', () => {
|
||||||
|
expect(up({})).toEqual({ chatMessage: '' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('down removes the chatMessage property', () => {
|
||||||
|
expect(down({ chatMessage: '' })).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Adds chat properties to instance', () => {
|
||||||
|
const { up, down } = instanceMigrations.migrators[14]
|
||||||
|
|
||||||
|
test('up adds the chatMessage property', () => {
|
||||||
|
expect(up({})).toEqual({ chatMessage: '', isChatting: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('down removes the chatMessage property', () => {
|
||||||
|
expect(down({ chatMessage: '', isChatting: true })).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('Removes does resize from embed', () => {
|
describe('Removes does resize from embed', () => {
|
||||||
const { up, down } = embedShapeMigrations.migrators[2]
|
const { up, down } = embedShapeMigrations.migrators[2]
|
||||||
test('up works as expected', () => {
|
test('up works as expected', () => {
|
||||||
|
|
|
@ -44,6 +44,10 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
|
||||||
exportBackground: boolean
|
exportBackground: boolean
|
||||||
screenBounds: Box2dModel
|
screenBounds: Box2dModel
|
||||||
zoomBrush: Box2dModel | null
|
zoomBrush: Box2dModel | null
|
||||||
|
|
||||||
|
chatMessage: string
|
||||||
|
isChatting: boolean
|
||||||
|
|
||||||
isPenMode: boolean
|
isPenMode: boolean
|
||||||
isGridMode: boolean
|
isGridMode: boolean
|
||||||
}
|
}
|
||||||
|
@ -87,6 +91,8 @@ export const instanceTypeValidator: T.Validator<TLInstance> = T.model(
|
||||||
exportBackground: T.boolean,
|
exportBackground: T.boolean,
|
||||||
screenBounds: T.boxModel,
|
screenBounds: T.boxModel,
|
||||||
zoomBrush: T.boxModel.nullable(),
|
zoomBrush: T.boxModel.nullable(),
|
||||||
|
chatMessage: T.string,
|
||||||
|
isChatting: T.boolean,
|
||||||
isPenMode: T.boolean,
|
isPenMode: T.boolean,
|
||||||
isGridMode: T.boolean,
|
isGridMode: T.boolean,
|
||||||
})
|
})
|
||||||
|
@ -106,13 +112,14 @@ const Versions = {
|
||||||
RemoveUserId: 11,
|
RemoveUserId: 11,
|
||||||
AddIsPenModeAndIsGridMode: 12,
|
AddIsPenModeAndIsGridMode: 12,
|
||||||
HoistOpacity: 13,
|
HoistOpacity: 13,
|
||||||
|
AddChat: 14,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export { Versions as instanceTypeVersions }
|
export { Versions as instanceTypeVersions }
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const instanceMigrations = defineMigrations({
|
export const instanceMigrations = defineMigrations({
|
||||||
currentVersion: Versions.HoistOpacity,
|
currentVersion: Versions.AddChat,
|
||||||
migrators: {
|
migrators: {
|
||||||
[Versions.AddTransparentExportBgs]: {
|
[Versions.AddTransparentExportBgs]: {
|
||||||
up: (instance: TLInstance) => {
|
up: (instance: TLInstance) => {
|
||||||
|
@ -281,6 +288,14 @@ export const instanceMigrations = defineMigrations({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[Versions.AddChat]: {
|
||||||
|
up: (instance: TLInstance) => {
|
||||||
|
return { ...instance, chatMessage: '', isChatting: false }
|
||||||
|
},
|
||||||
|
down: ({ chatMessage: _, isChatting: __, ...instance }: TLInstance) => {
|
||||||
|
return instance
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -321,6 +336,8 @@ export const InstanceRecordType = createRecordType<TLInstance>('instance', {
|
||||||
isToolLocked: false,
|
isToolLocked: false,
|
||||||
screenBounds: { x: 0, y: 0, w: 1080, h: 720 },
|
screenBounds: { x: 0, y: 0, w: 1080, h: 720 },
|
||||||
zoomBrush: null,
|
zoomBrush: null,
|
||||||
|
chatMessage: '',
|
||||||
|
isChatting: false,
|
||||||
isGridMode: false,
|
isGridMode: false,
|
||||||
isPenMode: false,
|
isPenMode: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -27,6 +27,7 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
|
||||||
type: TLCursor['type']
|
type: TLCursor['type']
|
||||||
rotation: number
|
rotation: number
|
||||||
}
|
}
|
||||||
|
chatMessage: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -59,18 +60,20 @@ export const instancePresenceValidator: T.Validator<TLInstancePresence> = T.mode
|
||||||
currentPageId: idValidator<TLPageId>('page'),
|
currentPageId: idValidator<TLPageId>('page'),
|
||||||
brush: T.boxModel.nullable(),
|
brush: T.boxModel.nullable(),
|
||||||
scribble: scribbleValidator.nullable(),
|
scribble: scribbleValidator.nullable(),
|
||||||
|
chatMessage: T.string,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const Versions = {
|
const Versions = {
|
||||||
AddScribbleDelay: 1,
|
AddScribbleDelay: 1,
|
||||||
RemoveInstanceId: 2,
|
RemoveInstanceId: 2,
|
||||||
|
AddChatMessage: 3,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export { Versions as instancePresenceVersions }
|
export { Versions as instancePresenceVersions }
|
||||||
|
|
||||||
export const instancePresenceMigrations = defineMigrations({
|
export const instancePresenceMigrations = defineMigrations({
|
||||||
currentVersion: Versions.RemoveInstanceId,
|
currentVersion: Versions.AddChatMessage,
|
||||||
migrators: {
|
migrators: {
|
||||||
[Versions.AddScribbleDelay]: {
|
[Versions.AddScribbleDelay]: {
|
||||||
up: (instance) => {
|
up: (instance) => {
|
||||||
|
@ -95,6 +98,14 @@ export const instancePresenceMigrations = defineMigrations({
|
||||||
return { ...instance, instanceId: TLINSTANCE_ID }
|
return { ...instance, instanceId: TLINSTANCE_ID }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[Versions.AddChatMessage]: {
|
||||||
|
up: (instance) => {
|
||||||
|
return { ...instance, chatMessage: '' }
|
||||||
|
},
|
||||||
|
down: ({ chatMessage: _, ...instance }) => {
|
||||||
|
return instance
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -130,4 +141,5 @@ export const InstancePresenceRecordType = createRecordType<TLInstancePresence>(
|
||||||
selectedIds: [],
|
selectedIds: [],
|
||||||
brush: null,
|
brush: null,
|
||||||
scribble: null,
|
scribble: null,
|
||||||
|
chatMessage: '',
|
||||||
}))
|
}))
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -81,6 +81,7 @@ export interface TLUiEventMap {
|
||||||
'toggle-reduce-motion': null
|
'toggle-reduce-motion': null
|
||||||
'exit-pen-mode': null
|
'exit-pen-mode': null
|
||||||
'stop-following': null
|
'stop-following': null
|
||||||
|
'open-cursor-chat': null
|
||||||
}
|
}
|
||||||
|
|
||||||
type Join<T, K> = K extends null
|
type Join<T, K> = K extends null
|
||||||
|
|
|
@ -50,6 +50,7 @@ export type TLUiTranslationKey =
|
||||||
| 'action.leave-shared-project'
|
| 'action.leave-shared-project'
|
||||||
| 'action.new-project'
|
| 'action.new-project'
|
||||||
| 'action.new-shared-project'
|
| 'action.new-shared-project'
|
||||||
|
| 'action.open-cursor-chat'
|
||||||
| 'action.open-file'
|
| 'action.open-file'
|
||||||
| 'action.pack'
|
| 'action.pack'
|
||||||
| 'action.paste'
|
| 'action.paste'
|
||||||
|
@ -282,6 +283,7 @@ export type TLUiTranslationKey =
|
||||||
| 'shortcuts-dialog.tools'
|
| 'shortcuts-dialog.tools'
|
||||||
| 'shortcuts-dialog.transform'
|
| 'shortcuts-dialog.transform'
|
||||||
| 'shortcuts-dialog.view'
|
| 'shortcuts-dialog.view'
|
||||||
|
| 'shortcuts-dialog.collaboration'
|
||||||
| 'home-project-dialog.title'
|
| 'home-project-dialog.title'
|
||||||
| 'home-project-dialog.description'
|
| 'home-project-dialog.description'
|
||||||
| 'rename-project-dialog.title'
|
| 'rename-project-dialog.title'
|
||||||
|
@ -344,3 +346,4 @@ export type TLUiTranslationKey =
|
||||||
| 'vscode.file-open.backup-saved'
|
| 'vscode.file-open.backup-saved'
|
||||||
| 'vscode.file-open.backup-failed'
|
| 'vscode.file-open.backup-failed'
|
||||||
| 'vscode.file-open.dont-show-again'
|
| 'vscode.file-open.dont-show-again'
|
||||||
|
| 'cursor-chat.type-to-chat'
|
||||||
|
|
|
@ -50,6 +50,7 @@ export const DEFAULT_TRANSLATION = {
|
||||||
'action.leave-shared-project': 'Leave shared project',
|
'action.leave-shared-project': 'Leave shared project',
|
||||||
'action.new-project': 'New project',
|
'action.new-project': 'New project',
|
||||||
'action.new-shared-project': 'New shared project',
|
'action.new-shared-project': 'New shared project',
|
||||||
|
'action.open-cursor-chat': 'Cursor chat',
|
||||||
'action.open-file': 'Open file',
|
'action.open-file': 'Open file',
|
||||||
'action.pack': 'Pack',
|
'action.pack': 'Pack',
|
||||||
'action.paste': 'Paste',
|
'action.paste': 'Paste',
|
||||||
|
@ -285,6 +286,7 @@ export const DEFAULT_TRANSLATION = {
|
||||||
'shortcuts-dialog.tools': 'Tools',
|
'shortcuts-dialog.tools': 'Tools',
|
||||||
'shortcuts-dialog.transform': 'Transform',
|
'shortcuts-dialog.transform': 'Transform',
|
||||||
'shortcuts-dialog.view': 'View',
|
'shortcuts-dialog.view': 'View',
|
||||||
|
'shortcuts-dialog.collaboration': 'Collaboration',
|
||||||
'home-project-dialog.title': 'Home project',
|
'home-project-dialog.title': 'Home project',
|
||||||
'home-project-dialog.description': "This is your local home project. It's just for you!",
|
'home-project-dialog.description': "This is your local home project. It's just for you!",
|
||||||
'rename-project-dialog.title': 'Rename project',
|
'rename-project-dialog.title': 'Rename project',
|
||||||
|
@ -354,4 +356,5 @@ export const DEFAULT_TRANSLATION = {
|
||||||
'vscode.file-open.backup-saved': 'Backup saved',
|
'vscode.file-open.backup-saved': 'Backup saved',
|
||||||
'vscode.file-open.backup-failed': 'Backup failed: this is not a .tldr file.',
|
'vscode.file-open.backup-failed': 'Backup failed: this is not a .tldr file.',
|
||||||
'vscode.file-open.dont-show-again': "Don't ask again",
|
'vscode.file-open.dont-show-again': "Don't ask again",
|
||||||
|
'cursor-chat.type-to-chat': 'Type to chat...',
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
--layer-menus: 400;
|
--layer-menus: 400;
|
||||||
--layer-overlays: 500;
|
--layer-overlays: 500;
|
||||||
--layer-toasts: 650;
|
--layer-toasts: 650;
|
||||||
|
--layer-cursor: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------- Layout --------------------- */
|
/* --------------------- Layout --------------------- */
|
||||||
|
|
Loading…
Reference in a new issue