diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css
index 98c523bd9..89de82012 100644
--- a/src/components/blocks/blocks.css
+++ b/src/components/blocks/blocks.css
@@ -4,7 +4,6 @@
.blocks {
height: 100%;
- position: relative;
}
.drag-over:after {
diff --git a/src/components/dash-chat/dash-chat.css b/src/components/dash-chat/dash-chat.css
deleted file mode 100644
index 4895ee156..000000000
--- a/src/components/dash-chat/dash-chat.css
+++ /dev/null
@@ -1,299 +0,0 @@
-@import "../../css/colors.css";
-@import "../../css/units.css";
-@import "../../css/z-index.css";
-
-.wrapper {
- position: relative;
- display: flex;
- align-items: center;
-}
-
-.toggle {
- position: relative;
- padding: 0.15rem 0.45rem;
- border: 1px solid $ui-black-transparent;
- border-radius: 0.25rem;
- background: $ui-primary;
- font-size: 0.95rem;
- line-height: 1.25;
- cursor: pointer;
-}
-
-.badge {
- position: absolute;
- top: -0.35rem;
- right: -0.35rem;
- min-width: 1rem;
- height: 1rem;
- padding: 0 0.2rem;
- border-radius: 0.5rem;
- background: $error-light;
- color: #fff;
- font-size: 0.625rem;
- font-weight: bold;
- line-height: 1rem;
- text-align: center;
-}
-
-.panel {
- position: absolute;
- top: calc(100% + 0.4rem);
- right: 0;
- z-index: $z-index-drag-layer;
- display: flex;
- flex-direction: column;
- width: 20rem;
- height: 26rem;
- border: 1px solid $ui-black-transparent;
- border-radius: $space;
- background: $ui-white;
- color: $text-primary;
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- font-size: 0.8125rem;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.22);
- overflow: hidden;
-}
-
-.header {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 0.35rem;
- padding: 0.4rem 0.6rem;
- border-bottom: 1px solid $ui-black-transparent;
- background: $ui-tertiary;
- font-weight: bold;
-}
-
-.header-button,
-.header-button-active {
- margin-left: 0.15rem;
- padding: 0.1rem 0.35rem;
- border: 1px solid transparent;
- border-radius: 0.25rem;
- background: transparent;
- font-size: 0.85rem;
- cursor: pointer;
-}
-
-.header-button:hover {
- background: rgba(0, 0, 0, 0.08);
-}
-
-.header-button-active {
- background: $looks-transparent;
- border-color: $looks-secondary;
-}
-
-.header-button:first-of-type {
- margin-left: auto;
-}
-
-.list {
- flex: 1;
- overflow-y: auto;
- padding: 0.5rem;
- display: flex;
- flex-direction: column;
- gap: 0.55rem;
-}
-
-.empty {
- margin: auto;
- color: $text-primary-transparent;
- text-align: center;
-}
-
-.message,
-.message-self {
- display: flex;
- flex-direction: column;
- gap: 0.15rem;
- max-width: 100%;
-}
-
-.message-self .message-text {
- background: $looks-transparent;
- border-radius: 0.45rem;
- padding: 0.25rem 0.45rem;
-}
-
-.message-meta {
- display: flex;
- flex-direction: row;
- align-items: baseline;
- gap: 0.4rem;
-}
-
-.message-author {
- font-weight: bold;
- font-size: 0.75rem;
-}
-
-.message-time {
- color: $text-primary-transparent;
- font-size: 0.65rem;
-}
-
-.message-text {
- word-break: break-word;
- white-space: pre-wrap;
- line-height: 1.35;
-}
-
-.mention {
- color: $looks-secondary;
- font-weight: bold;
-}
-
-.mention-me {
- color: #fff;
- background: $error-light;
- border-radius: 0.3rem;
- padding: 0 0.25rem;
- font-weight: bold;
-}
-
-.pin-chip {
- display: inline-flex;
- flex-direction: row;
- align-items: center;
- gap: 0.25rem;
- align-self: flex-start;
- margin-top: 0.15rem;
- padding: 0.1rem 0.4rem;
- border: 1px solid $ui-black-transparent;
- border-radius: 0.5rem;
- background: $ui-tertiary;
- font-size: 0.65rem;
- cursor: pointer;
-}
-
-.pin-chip:hover {
- background: $looks-transparent;
-}
-
-.pin-name {
- margin-left: 0.2rem;
- font-style: italic;
- opacity: 0.85;
-}
-
-.pending-pin {
- padding: 0.25rem 0.6rem;
- border-top: 1px solid $ui-black-transparent;
- background: $looks-transparent;
- font-size: 0.7rem;
-}
-
-.composer {
- display: flex;
- flex-direction: row;
- align-items: flex-end;
- gap: 0.3rem;
- padding: 0.45rem;
- border-top: 1px solid $ui-black-transparent;
- background: $ui-tertiary;
-}
-
-.composer-button,
-.composer-button-active {
- flex: none;
- padding: 0.25rem 0.4rem;
- border: 1px solid $ui-black-transparent;
- border-radius: 0.35rem;
- background: $ui-white;
- font-size: 0.95rem;
- line-height: 1.25;
- cursor: pointer;
-}
-
-.composer-button-active {
- background: $looks-transparent;
- border-color: $looks-secondary;
-}
-
-.input {
- flex: 1;
- resize: none;
- padding: 0.35rem 0.5rem;
- border: 1px solid $ui-black-transparent;
- border-radius: 0.45rem;
- font: inherit;
- line-height: 1.35;
- max-height: 5.5rem;
-}
-
-.send-button {
- flex: none;
- padding: 0.3rem 0.55rem;
- border: 1px solid $ui-black-transparent;
- border-radius: 0.35rem;
- background: $looks-secondary;
- color: #fff;
- font-size: 0.95rem;
- cursor: pointer;
-}
-
-.send-button:disabled {
- opacity: 0.45;
- cursor: default;
-}
-
-.emoji-picker {
- position: absolute;
- right: 0.45rem;
- bottom: 3.6rem;
- z-index: $z-index-drag-layer;
- width: 17.5rem;
- border: 1px solid $ui-black-transparent;
- border-radius: $space;
- background: $ui-white;
- box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2);
- overflow: hidden;
-}
-
-.emoji-categories {
- display: flex;
- flex-direction: row;
- border-bottom: 1px solid $ui-black-transparent;
- background: $ui-tertiary;
-}
-
-.emoji-category,
-.emoji-category-active {
- flex: 1;
- padding: 0.3rem 0;
- border: none;
- background: transparent;
- font-size: 1rem;
- cursor: pointer;
-}
-
-.emoji-category-active {
- background: $looks-transparent;
- box-shadow: inset 0 -2px 0 $looks-secondary;
-}
-
-.emoji-grid {
- display: grid;
- grid-template-columns: repeat(8, 1fr);
- gap: 0.1rem;
- padding: 0.35rem;
- max-height: 11.5rem;
- overflow-y: auto;
-}
-
-.emoji-cell {
- padding: 0.15rem 0;
- border: none;
- border-radius: 0.25rem;
- background: transparent;
- font-size: 1.15rem;
- line-height: 1.35;
- cursor: pointer;
-}
-
-.emoji-cell:hover {
- background: $looks-transparent;
-}
diff --git a/src/components/dash-chat/dash-chat.jsx b/src/components/dash-chat/dash-chat.jsx
deleted file mode 100644
index 3000f170c..000000000
--- a/src/components/dash-chat/dash-chat.jsx
+++ /dev/null
@@ -1,501 +0,0 @@
-import bindAll from 'lodash.bindall';
-import PropTypes from 'prop-types';
-import React from 'react';
-import {connect} from 'react-redux';
-import {FormattedMessage, injectIntl, intlShape} from 'react-intl';
-
-import AddonHooks from '../../addons/hooks';
-import {listenDashChat, MAX_TEXT} from '../../lib/dash-chat.js';
-import {presenceColor} from '../../lib/dash-presence.js';
-import {activateTab, COSTUMES_TAB_INDEX} from '../../reducers/editor-tab';
-import VM from 'scratch-vm';
-import * as styles from './dash-chat.css';
-
-const EMOJI_CATEGORIES = [
- {
- id: 'smileys',
- icon: '\u{1F600}',
- emojis: ['\u{1F600}', '\u{1F601}', '\u{1F602}', '\u{1F923}', '\u{1F603}', '\u{1F604}', '\u{1F605}',
- '\u{1F606}', '\u{1F609}', '\u{1F60A}', '\u{1F60B}', '\u{1F60D}', '\u{1F970}', '\u{1F618}', '\u{1F617}',
- '\u{1F619}', '\u{1F61A}', '\u{1F914}', '\u{1F928}', '\u{1F610}', '\u{1F611}', '\u{1F634}', '\u{1F637}',
- '\u{1F60E}', '\u{1F913}', '\u{1F929}', '\u{1F973}', '\u{1F97A}', '\u{1F62D}', '\u{1F622}', '\u{1F621}',
- '\u{1F92C}', '\u{1F92F}', '\u{1F631}', '\u{1F633}', '\u{1F92A}', '\u{1F92B}', '\u{1F917}', '\u{1F60F}']
- },
- {
- id: 'gestures',
- icon: '\u{1F44D}',
- emojis: ['\u{1F44D}', '\u{1F44E}', '\u{1F44F}', '\u{1F64C}', '\u{1F44C}', '\u{270C}\u{FE0F}', '\u{1F91E}',
- '\u{1F91F}', '\u{1F44B}', '\u{1F91A}', '\u{270B}', '\u{1F590}\u{FE0F}', '\u{1F448}', '\u{1F449}',
- '\u{1F446}', '\u{1F447}', '\u{1F44A}', '\u{1F91B}', '\u{1F91C}', '\u{1F450}', '\u{1F64F}', '\u{1F91D}',
- '\u{1F9E1}', '\u{1F4AA}', '\u{1F9B6}', '\u{1F485}', '\u{1F933}', '\u{1F930}', '\u{1F60C}', '\u{1F971}']
- },
- {
- id: 'hearts',
- icon: '\u{2764}\u{FE0F}',
- emojis: ['\u{2764}\u{FE0F}', '\u{1F9E1}', '\u{1F49B}', '\u{1F49A}', '\u{1F499}', '\u{1F49C}', '\u{1F90E}',
- '\u{1F5A4}', '\u{1F90D}', '\u{1F494}', '\u{2763}\u{FE0F}', '\u{1F495}', '\u{1F496}', '\u{1F497}',
- '\u{1F498}', '\u{1F49D}', '\u{1F49E}', '\u{1F493}', '\u{2764}\u{FE0F}\u{200D}\u{1F525}', '\u{1F4AF}']
- },
- {
- id: 'nature',
- icon: '\u{1F431}',
- emojis: ['\u{1F431}', '\u{1F436}', '\u{1F43A}', '\u{1F98A}', '\u{1F43B}', '\u{1F428}', '\u{1F42F}',
- '\u{1F42E}', '\u{1F437}', '\u{1F438}', '\u{1F435}', '\u{1F414}', '\u{1F427}', '\u{1F426}', '\u{1F424}',
- '\u{1F989}', '\u{1F987}', '\u{1F43C}', '\u{1F43D}', '\u{1F430}', '\u{1F439}', '\u{1F433}', '\u{1F40B}',
- '\u{1F42C}', '\u{1F41F}', '\u{1F420}', '\u{1F421}', '\u{1F33B}', '\u{1F337}', '\u{1F341}', '\u{1F340}']
- },
- {
- id: 'objects',
- icon: '\u{2B50}',
- emojis: ['\u{2B50}', '\u{1F31F}', '\u{2728}', '\u{1F4AB}', '\u{1F525}', '\u{1F389}', '\u{1F388}',
- '\u{1F381}', '\u{1F3A8}', '\u{1F3AC}', '\u{1F3B5}', '\u{1F3B8}', '\u{1F3B2}', '\u{1F3AE}', '\u{1F579}',
- '\u{1F4BB}', '\u{1F4FA}', '\u{1F47E}', '\u{1F916}', '\u{1F680}', '\u{1F6F8}', '\u{2600}\u{FE0F}',
- '\u{1F308}', '\u{1F32A}\u{FE0F}', '\u{26C8}\u{FE0F}', '\u{26A1}', '\u{1F4A1}', '\u{1F4BF}', '\u{1F3B9}',
- '\u{1F3AF}']
- },
- {
- id: 'food',
- icon: '\u{1F354}',
- emojis: ['\u{1F354}', '\u{1F35F}', '\u{1F355}', '\u{1F363}', '\u{1F361}', '\u{1F373}', '\u{1F35D}',
- '\u{1F368}', '\u{1F366}', '\u{1F367}', '\u{1F382}', '\u{1F370}', '\u{1F9C1}', '\u{1F36A}', '\u{1F36B}',
- '\u{1F36C}', '\u{1F36D}', '\u{1F95E}', '\u{1F9C0}', '\u{1F35E}', '\u{1F950}', '\u{1F955}', '\u{1F34E}',
- '\u{1F34A}', '\u{1F349}', '\u{1F347}', '\u{1F353}', '\u{1F352}', '\u{1F34B}', '\u{1F34C}']
- }
-];
-
-const MENTION_PATTERN = /(@[A-Za-z0-9_-]+)/g;
-
-const targetName = target => {
- if (!target) return '';
- if (typeof target.getName === 'function') return target.getName();
- if (target.sprite && target.sprite.name) return target.sprite.name;
- return '';
-};
-
-class DashChat extends React.Component {
- constructor (props) {
- super(props);
- this.state = {
- open: false,
- version: 0,
- draft: '',
- pinEnabled: false,
- showEmoji: false,
- emojiCategory: EMOJI_CATEGORIES[0].id,
- pinnedOnly: false
- };
- bindAll(this, [
- 'handleChatChange',
- 'handleToggleOpen',
- 'handleClose',
- 'handleChangeDraft',
- 'handleKeyDown',
- 'handleSend',
- 'handleToggleEmoji',
- 'handleSelectEmoji',
- 'handleSelectEmojiCategory',
- 'handleTogglePin',
- 'handleTogglePinnedOnly',
- 'handleClickPin',
- 'handleDocumentClick',
- 'setListRef',
- 'setInputRef'
- ]);
- this.detachChat = null;
- this.pendingPin = null;
- }
- componentDidMount () {
- this.detachChat = listenDashChat(this.props.vm, this.handleChatChange);
- document.addEventListener('mousedown', this.handleDocumentClick, true);
- }
- componentWillUnmount () {
- if (this.detachChat) this.detachChat();
- document.removeEventListener('mousedown', this.handleDocumentClick, true);
- }
- componentDidUpdate (prevProps, prevState) {
- if (this.state.open && (prevState.version !== this.state.version || !prevState.open)) {
- this.scrollToBottom();
- }
- }
- handleDocumentClick (event) {
- if (!this.state.showEmoji || !this.wrapperRef) return;
- if (!this.wrapperRef.contains(event.target)) this.setState({showEmoji: false});
- }
- handleChatChange () {
- this.setState({version: this.state.version + 1});
- }
- handleToggleOpen () {
- const open = !this.state.open;
- this.setState({open, showEmoji: false});
- if (open) this.props.vm.dashCollaboration && this.props.vm.dashCollaboration.chat.markRead();
- }
- handleClose () {
- this.setState({open: false, showEmoji: false});
- }
- handleChangeDraft (event) {
- this.setState({draft: event.target.value.slice(0, MAX_TEXT)});
- }
- handleKeyDown (event) {
- if (event.key === 'Enter' && !event.shiftKey) {
- event.preventDefault();
- this.handleSend();
- }
- }
- handleSend () {
- const chat = this.props.vm.dashCollaboration && this.props.vm.dashCollaboration.chat;
- if (!chat) return;
- const pin = this.state.pinEnabled ? this.capturePin() : null;
- if (chat.sendText(this.state.draft, pin)) {
- this.setState({draft: '', pinEnabled: false});
- }
- }
- capturePin () {
- const vm = this.props.vm;
- const target = vm.editingTarget;
- if (!target) return null;
- const targetId = target.id;
- const name = targetName(target);
- const workspace = AddonHooks.blocklyWorkspace;
- const selected = workspace && workspace.selected;
- if (selected && selected.id && this.props.activeTabIndex === 0) {
- return {type: 'block', targetId, blockId: selected.id, name};
- }
- if (this.props.activeTabIndex === COSTUMES_TAB_INDEX) {
- return {type: 'costume', targetId, name};
- }
- return {type: 'sprite', targetId, name};
- }
- handleToggleEmoji () {
- this.setState({showEmoji: !this.state.showEmoji});
- }
- handleSelectEmojiCategory (id) {
- this.setState({emojiCategory: id});
- }
- handleSelectEmoji (emoji) {
- const input = this.inputRef;
- const draft = this.state.draft;
- if (!input) {
- this.setState({draft: (draft + emoji).slice(0, MAX_TEXT)});
- return;
- }
- const start = typeof input.selectionStart === 'number' ? input.selectionStart : draft.length;
- const end = typeof input.selectionEnd === 'number' ? input.selectionEnd : draft.length;
- const next = (draft.slice(0, start) + emoji + draft.slice(end)).slice(0, MAX_TEXT);
- this.setState({draft: next}, () => {
- input.focus();
- const caret = Math.min(start + emoji.length, MAX_TEXT);
- input.setSelectionRange(caret, caret);
- });
- }
- handleTogglePin () {
- this.setState({pinEnabled: !this.state.pinEnabled});
- }
- handleTogglePinnedOnly () {
- this.setState({pinnedOnly: !this.state.pinnedOnly});
- }
- handleClickPin (pin) {
- if (!pin) return;
- const vm = this.props.vm;
- vm.setEditingTarget(pin.targetId);
- if (pin.type === 'costume') this.props.onActivateTab(COSTUMES_TAB_INDEX);
- if (pin.type === 'block' && pin.blockId) {
- const workspace = AddonHooks.blocklyWorkspace;
- const root = workspace && workspace.getInjectionDiv && workspace.getInjectionDiv();
- const element = root && root.querySelector(`[data-id="${pin.blockId}"]`);
- if (element) {
- const restore = element.style.filter;
- element.style.filter =
- 'drop-shadow(0 0 3px rgba(76, 151, 255, 1)) drop-shadow(0 0 3px rgba(76, 151, 255, 1))';
- setTimeout(() => {
- element.style.filter = restore;
- }, 2500);
- }
- }
- }
- setListRef (ref) {
- this.listRef = ref;
- }
- setInputRef (ref) {
- this.inputRef = ref;
- }
- scrollToBottom () {
- if (this.listRef) this.listRef.scrollTop = this.listRef.scrollHeight;
- }
- renderMentions (text) {
- const self = (this.props.vm.dashCollaboration && this.props.vm.dashCollaboration.chat.selfUsername) || '';
- return String(text).split(MENTION_PATTERN).map((part, index) => {
- if (part.startsWith('@')) {
- const isMe = self && part.toLowerCase() === `@${self.toLowerCase()}`;
- return (
- {part}
- );
- }
- return part;
- });
- }
- renderMessage (message) {
- return (
-
-
-
- {message.username}
-
-
- {new Date(message.ts).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'})}
-
-
-
{this.renderMentions(message.text)}
- {message.pin ? (
-
- ) : null}
-
- );
- }
- render () {
- const chat = this.props.vm.dashCollaboration && this.props.vm.dashCollaboration.chat;
- if (!chat) return null;
- const messages = this.state.pinnedOnly ?
- chat.messages.filter(message => message.pin) : chat.messages;
- const category = EMOJI_CATEGORIES.find(entry => entry.id === this.state.emojiCategory) ||
- EMOJI_CATEGORIES[0];
- const pendingPin = this.state.pinEnabled ? this.capturePin() : null;
- return (
- {
- this.wrapperRef = ref;
- }}
- >
-
- {this.state.open ? (
-
-
-
-
-
-
-
- {messages.length ? messages.map(message => this.renderMessage(message)) : (
-
-
-
- )}
-
- {pendingPin ? (
-
- {'\u{1F4CC} '}
- {pendingPin.type === 'block' ? (
-
- ) : pendingPin.type === 'costume' ? (
-
- ) : (
-
- )}
- {pendingPin.name ? {pendingPin.name} : null}
-
- ) : null}
-
-
-
-
-
-
- {this.state.showEmoji ? (
-
-
- {EMOJI_CATEGORIES.map(entry => (
-
- ))}
-
-
- {category.emojis.map((emoji, index) => (
-
- ))}
-
-
- ) : null}
-
- ) : null}
-
- );
- }
-}
-
-DashChat.propTypes = {
- activeTabIndex: PropTypes.number.isRequired,
- intl: intlShape,
- onActivateTab: PropTypes.func.isRequired,
- vm: PropTypes.instanceOf(VM).isRequired
-};
-
-const mapStateToProps = state => ({
- activeTabIndex: state.scratchGui.editorTab.activeTabIndex
-});
-
-const mapDispatchToProps = dispatch => ({
- onActivateTab: index => dispatch(activateTab(index))
-});
-
-export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(DashChat));
diff --git a/src/components/dash-collaboration-status/dash-collaboration-status.jsx b/src/components/dash-collaboration-status/dash-collaboration-status.jsx
index e853c9a06..538db228e 100644
--- a/src/components/dash-collaboration-status/dash-collaboration-status.jsx
+++ b/src/components/dash-collaboration-status/dash-collaboration-status.jsx
@@ -2,7 +2,6 @@ import downloadBlob from '../../lib/download-blob';
import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
-import DashChat from '../dash-chat/dash-chat.jsx';
import * as styles from './dash-collaboration-status.css';
const getStatus = vm => {
@@ -126,7 +125,6 @@ class DashCollaborationStatus extends React.Component {
{memberNames.length ? (
{memberNames.join(', ')}
) : null}
-
);
}
diff --git a/src/components/dash-presence/presence-avatars.css b/src/components/dash-presence/presence-avatars.css
deleted file mode 100644
index ba1a22875..000000000
--- a/src/components/dash-presence/presence-avatars.css
+++ /dev/null
@@ -1,41 +0,0 @@
-@import "../../css/colors.css";
-@import "../../css/units.css";
-
-.avatars {
- display: flex;
- flex-direction: row;
- align-items: center;
- padding: 0 $space;
-}
-
-.avatar,
-.avatar-online {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 1.75rem;
- height: 1.75rem;
- margin-left: -0.35rem;
- border-radius: 50%;
- border: 2px solid $ui-black-transparent;
- background-color: $ui-primary;
- color: #fff;
- font-size: 0.7rem;
- font-weight: bold;
- user-select: none;
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
-}
-
-.avatar:first-child,
-.avatar-online:first-child {
- margin-left: 0;
-}
-
-.avatar {
- opacity: 0.55;
- filter: grayscale(60%);
-}
-
-.avatar-online {
- opacity: 1;
-}
diff --git a/src/components/dash-presence/presence-avatars.jsx b/src/components/dash-presence/presence-avatars.jsx
deleted file mode 100644
index 03261003b..000000000
--- a/src/components/dash-presence/presence-avatars.jsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-
-import {listenPresence, presenceColor} from '../../lib/dash-presence.js';
-import * as styles from './presence-avatars.css';
-
-const MAX_AVATARS = 5;
-
-/**
- * Compact avatar stack for the menu bar showing who is in the live-collaboration
- * session and who is currently online (sending presence).
- */
-class PresenceAvatars extends React.Component {
- constructor (props) {
- super(props);
- this.state = {version: 0};
- this.handlePresenceChange = this.handlePresenceChange.bind(this);
- this.detachPresence = null;
- }
- componentDidMount () {
- this.detachPresence = listenPresence(this.props.vm, this.handlePresenceChange);
- }
- componentWillUnmount () {
- if (this.detachPresence) this.detachPresence();
- }
- handlePresenceChange () {
- this.setState({version: this.state.version + 1});
- }
- getPresence () {
- return (this.props.vm.dashCollaboration && this.props.vm.dashCollaboration.presence) || null;
- }
- render () {
- const session = this.props.vm.dashCollaboration;
- if (!session) return null;
- const presence = this.getPresence();
- const onlinePeers = presence ? presence.getPeers() : [];
- const onlineByUser = new Map();
- for (const peer of onlinePeers) {
- if (peer.userId) onlineByUser.set(peer.userId, peer);
- }
- const client = session.client || {};
- const seen = new Set();
- const entries = [];
- for (const member of session.members || []) {
- const id = member.connectionId || member.userId;
- if (!id || seen.has(member.userId)) continue;
- seen.add(member.userId);
- entries.push({
- id,
- username: member.username,
- role: member.role,
- online: Boolean(onlineByUser.get(member.userId)) ||
- (member.userId === client.userId && session.state === 'connected'),
- self: member.userId === client.userId
- });
- }
- if (!entries.length) return null;
- const visible = entries.slice(0, MAX_AVATARS);
- const overflow = entries.length - visible.length;
- return (
-
- {visible.map(entry => (
-
- {entry.username.slice(0, 1).toUpperCase()}
-
- ))}
- {overflow > 0 ? (
-
- {`+${overflow}`}
-
- ) : null}
-
- );
- }
-}
-
-PresenceAvatars.propTypes = {
- vm: PropTypes.object.isRequired
-};
-
-export default PresenceAvatars;
diff --git a/src/components/dash-presence/presence-cursors.css b/src/components/dash-presence/presence-cursors.css
deleted file mode 100644
index b08136545..000000000
--- a/src/components/dash-presence/presence-cursors.css
+++ /dev/null
@@ -1,49 +0,0 @@
-@import "../../css/z-index.css";
-
-.layer {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- pointer-events: none;
- overflow: hidden;
- z-index: $z-index-drag-layer;
-}
-
-.cursor {
- position: absolute;
- display: flex;
- flex-direction: row;
- align-items: flex-start;
- transform: translate(-2px, -2px);
- will-change: left, top;
-}
-
-.pointer {
- flex: none;
- filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.35));
-}
-
-.label {
- margin-left: -2px;
- margin-top: 12px;
- padding: 0.075rem 0.4rem;
- border-radius: 0.5rem;
- border: 1px solid rgba(255, 255, 255, 0.85);
- color: #fff;
- font-size: 0.625rem;
- font-weight: bold;
- line-height: 1.35;
- white-space: nowrap;
- text-shadow: 0 1px 1px rgba(0, 0, 0, 0.25);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 8rem;
-}
-
-.dragBadge {
- margin-left: 0.25rem;
- font-size: 0.7rem;
-}
diff --git a/src/components/dash-presence/presence-cursors.jsx b/src/components/dash-presence/presence-cursors.jsx
deleted file mode 100644
index a4f4f3b43..000000000
--- a/src/components/dash-presence/presence-cursors.jsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-
-import {listenPresence} from '../../lib/dash-presence.js';
-import * as styles from './presence-cursors.css';
-
-/**
- * Overlay layer that renders remote collaboration cursors (Figma-style).
- * Place it inside a positioned container matching the surface being tracked.
- * Cursor coordinates are surface-local: `x`/`y` in 0..1 for the stage, or
- * workspace units for the blocks area (when a `transform` prop is provided).
- */
-class PresenceCursors extends React.Component {
- constructor (props) {
- super(props);
- this.state = {version: 0};
- this.handlePresenceChange = this.handlePresenceChange.bind(this);
- this.detachPresence = null;
- this.highlighted = new Map();
- }
- componentDidMount () {
- this.detachPresence = listenPresence(this.props.vm, this.handlePresenceChange);
- }
- componentWillUnmount () {
- if (this.detachPresence) this.detachPresence();
- this.clearHighlights();
- }
- componentDidUpdate () {
- this.updateHighlights();
- }
- handlePresenceChange () {
- this.setState({version: this.state.version + 1});
- }
- getPresence () {
- return (this.props.vm.dashCollaboration && this.props.vm.dashCollaboration.presence) || null;
- }
- clearHighlights () {
- for (const [element, restore] of this.highlighted) {
- element.style.filter = restore;
- }
- this.highlighted.clear();
- }
- /**
- * Outline blocks that remote participants are currently dragging.
- */
- updateHighlights () {
- if (!this.props.highlightDragged) return;
- const presence = this.getPresence();
- const root = this.props.highlightRoot;
- const wanted = new Map();
- if (presence && root) {
- for (const peer of presence.getPeers()) {
- if (peer.state.dragging) wanted.set(peer.state.dragging, peer.color);
- }
- }
- for (const [element, restore] of [...this.highlighted]) {
- if (!wanted.has(element.getAttribute('data-id'))) {
- element.style.filter = restore;
- this.highlighted.delete(element);
- }
- }
- for (const [blockId, color] of wanted) {
- const element = root.querySelector(`[data-id="${blockId}"]`);
- if (!element || this.highlighted.has(element)) continue;
- this.highlighted.set(element, element.style.filter);
- element.style.filter = `drop-shadow(0 0 3px ${color}) drop-shadow(0 0 3px ${color})`;
- }
- }
- render () {
- const presence = this.getPresence();
- if (!presence) return null;
- const {surface, transform} = this.props;
- const cursors = presence.getPeers().filter(peer =>
- peer.state.cursor && peer.state.cursor.surface === surface);
- if (!cursors.length) return null;
- return (
-
- {cursors.map(peer => {
- const cursor = peer.state.cursor;
- const position = transform ? transform(cursor) : {
- left: `${Math.max(0, Math.min(1, cursor.x)) * 100}%`,
- top: `${Math.max(0, Math.min(1, cursor.y)) * 100}%`
- };
- return (
-
-
-
- {peer.username}
- {peer.state.dragging ? (
- {'⠿'}
- ) : null}
-
-
- );
- })}
-
- );
- }
-}
-
-PresenceCursors.propTypes = {
- surface: PropTypes.oneOf(['stage', 'blocks']).isRequired,
- transform: PropTypes.func,
- highlightDragged: PropTypes.bool,
- highlightRoot: PropTypes.object,
- vm: PropTypes.object.isRequired
-};
-
-export default PresenceCursors;
diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx
index 5765062da..9fd1a9ffd 100644
--- a/src/components/menu-bar/menu-bar.jsx
+++ b/src/components/menu-bar/menu-bar.jsx
@@ -29,7 +29,6 @@ import SettingsMenu from './settings-menu.jsx';
import AccountNav from '../../containers/account-nav.jsx';
import Spinner from '../spinner/spinner.jsx';
import CollaboratorsModal from '../dash-collaborators/collaborators-modal.jsx';
-import PresenceAvatars from '../dash-presence/presence-avatars.jsx';
import FramerateChanger from '../../containers/tw-framerate-changer.jsx';
import ChangeUsername from '../../containers/tw-change-username.jsx';
@@ -1352,9 +1351,6 @@ class MenuBar extends React.Component {
}
- {!this.props.isPlayerOnly &&
}
{!this.props.isPlayerOnly &&
(
(
{typeof props.number === 'undefined' ? null : (
{props.number}
)}
- {props.presenceUsers && props.presenceUsers.length ? (
-
- {props.presenceUsers.map(user => (
-
- ))}
-
- ) : null}
{props.costumeURL ? (
@@ -127,10 +114,6 @@ SpriteSelectorItem.propTypes = {
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
preventContextMenu: PropTypes.bool,
- presenceUsers: PropTypes.arrayOf(PropTypes.shape({
- color: PropTypes.string,
- username: PropTypes.string
- })),
selected: PropTypes.bool.isRequired
};
diff --git a/src/components/sprite-selector/sprite-list.jsx b/src/components/sprite-selector/sprite-list.jsx
index 490b86503..fe2854a3d 100644
--- a/src/components/sprite-selector/sprite-list.jsx
+++ b/src/components/sprite-selector/sprite-list.jsx
@@ -28,7 +28,6 @@ const SpriteList = function (props) {
onAddSortable,
onRemoveSortable,
ordering,
- presenceUsers,
raised,
selectedId,
items
@@ -90,7 +89,6 @@ const SpriteList = function (props) {
index={index}
key={sprite.id}
name={sprite.name}
- presenceUsers={presenceUsers ? presenceUsers[sprite.id] : null}
selected={sprite.id === selectedId}
onClick={onSelectSprite}
onDeleteButtonClick={onDeleteSprite}
@@ -127,10 +125,6 @@ SpriteList.propTypes = {
order: PropTypes.number.isRequired
})),
onAddSortable: PropTypes.func,
- presenceUsers: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.shape({
- color: PropTypes.string,
- username: PropTypes.string
- }))),
onDeleteSprite: PropTypes.func,
onDuplicateSprite: PropTypes.func,
onExportSprite: PropTypes.func,
diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx
index 302ae245c..9ae811d5b 100644
--- a/src/components/sprite-selector/sprite-selector.jsx
+++ b/src/components/sprite-selector/sprite-selector.jsx
@@ -62,7 +62,6 @@ const SpriteSelectorComponent = function (props) {
onSelectSprite,
onSpriteUpload,
onSurpriseSpriteClick,
- presenceUsers,
raised,
selectedId,
spriteFileInput,
@@ -105,7 +104,6 @@ const SpriteSelectorComponent = function (props) {
editingTarget={editingTarget}
hoveredTarget={hoveredTarget}
items={Object.keys(sprites).map(id => sprites[id])}
- presenceUsers={presenceUsers}
raised={raised}
selectedId={selectedId}
onDeleteSprite={onDeleteSprite}
@@ -172,10 +170,6 @@ SpriteSelectorComponent.propTypes = {
onSelectSprite: PropTypes.func,
onSpriteUpload: PropTypes.func,
onSurpriseSpriteClick: PropTypes.func,
- presenceUsers: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.shape({
- color: PropTypes.string,
- username: PropTypes.string
- }))),
raised: PropTypes.bool,
selectedId: PropTypes.string,
spriteFileInput: PropTypes.func,
diff --git a/src/components/stage/stage.jsx b/src/components/stage/stage.jsx
index 87340b84a..1bdcd2760 100644
--- a/src/components/stage/stage.jsx
+++ b/src/components/stage/stage.jsx
@@ -27,7 +27,6 @@ const StageComponent = props => {
isRtl,
colorInfo,
micIndicator,
- presenceCursors,
question,
stageSize,
vm,
@@ -95,7 +94,6 @@ const StageComponent = props => {
stageWidth={stageDimensions.width}
/>
- {presenceCursors || null}
{isColorPicking && colorInfo ? (
) : null}
diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx
index da671467d..68fb7f58d 100644
--- a/src/components/target-pane/target-pane.jsx
+++ b/src/components/target-pane/target-pane.jsx
@@ -40,7 +40,6 @@ const TargetPane = ({
onSelectSprite,
onSpriteUpload,
onSurpriseSpriteClick,
- presenceUsers,
raiseSprites,
stage,
stageSize,
@@ -56,7 +55,6 @@ const TargetPane = ({
{
@@ -878,15 +794,7 @@ class Blocks extends React.Component {
componentRef={this.setBlocks}
onDrop={this.handleDrop}
{...props}
- >
-
-
+ />
{this.state.prompt ? (
= 0 && y >= 0 && x <= rect.width && y <= rect.height;
- const current = session.presence.local.cursor;
- if (inside) {
- session.presence.update({
- cursor: {
- surface: 'stage',
- x: x / rect.width,
- y: y / rect.height
- }
- });
- } else if (current && current.surface === 'stage') {
- session.presence.update({cursor: null});
- }
- }
- onMouseLeave () {
- const session = this.props.vm.dashCollaboration;
- if (!session || !session.presence) return;
- const current = session.presence.local.cursor;
- if (current && current.surface === 'stage') {
- session.presence.update({cursor: null});
- }
- }
onMouseUp (e) {
const {x, y} = getEventXY(e);
const mousePosition = [x - this.rect.left, y - this.rect.top];
@@ -492,10 +460,6 @@ class Stage extends React.Component {
overlay={this.props.vm.runtime.renderer.overlayContainer}
colorInfo={this.state.colorInfo}
dragRef={this.setDragCanvas}
- presenceCursors={}
question={this.state.question}
onDoubleClick={this.handleDoubleClick}
onQuestionAnswered={this.handleQuestionAnswered}
diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx
index 29ad91991..255ac4733 100644
--- a/src/containers/target-pane.jsx
+++ b/src/containers/target-pane.jsx
@@ -23,7 +23,6 @@ import {fetchSprite, fetchCode} from '../lib/backpack-api';
import randomizeSpritePosition from '../lib/randomize-sprite-position';
import downloadBlob from '../lib/download-blob';
import log from '../lib/log';
-import {listenPresence} from '../lib/dash-presence.js';
import {placeInViewport} from '../lib/backpack/code-payload.js';
class TargetPane extends React.Component {
@@ -49,45 +48,14 @@ class TargetPane extends React.Component {
'handlePaintSpriteClick',
'handleFileUploadClick',
'handleSpriteUpload',
- 'handlePresenceChange',
'setFileInput'
]);
}
componentDidMount () {
this.props.vm.addListener('BLOCK_DRAG_END', this.handleBlockDragEnd);
- this.detachPresence = listenPresence(this.props.vm, this.handlePresenceChange);
- this.publishPresenceSelection();
- }
- componentDidUpdate (prevProps) {
- if (prevProps.editingTarget !== this.props.editingTarget) {
- this.publishPresenceSelection();
- }
}
componentWillUnmount () {
this.props.vm.removeListener('BLOCK_DRAG_END', this.handleBlockDragEnd);
- if (this.detachPresence) this.detachPresence();
- }
- handlePresenceChange () {
- this.forceUpdate();
- }
- publishPresenceSelection () {
- const session = this.props.vm.dashCollaboration;
- if (session && session.presence) {
- session.presence.update({targetId: this.props.editingTarget || null});
- }
- }
- getPresenceUsersByTarget () {
- const session = this.props.vm.dashCollaboration;
- const presence = session && session.presence;
- const byTarget = {};
- if (!presence) return byTarget;
- for (const peer of presence.getPeers()) {
- const targetId = peer.state.targetId;
- if (!targetId) continue;
- if (!byTarget[targetId]) byTarget[targetId] = [];
- byTarget[targetId].push({color: peer.color, username: peer.username});
- }
- return byTarget;
}
handleChangeSpriteDirection (direction) {
if (!this.props.vm.canEditCollaboration()) return;
@@ -286,7 +254,6 @@ class TargetPane extends React.Component {
{
- let chat = null;
- let unsubscribe = null;
- const attach = () => {
- const next = (vm.dashCollaboration && vm.dashCollaboration.chat) || null;
- if (next === chat) return;
- if (unsubscribe) {
- unsubscribe();
- unsubscribe = null;
- }
- chat = next;
- if (chat) unsubscribe = chat.subscribe(onChange);
- onChange();
- };
- const onStatus = () => attach();
- vm.on('DASH_COLLABORATION_STATUS', onStatus);
- attach();
- return () => {
- vm.removeListener('DASH_COLLABORATION_STATUS', onStatus);
- if (unsubscribe) unsubscribe();
- };
-};
-
-class DashChat {
- constructor () {
- this.messages = [];
- this.listeners = new Set();
- this.unread = 0;
- this.lastMention = null;
- this.selfUserId = null;
- this.selfUsername = null;
- this.send = null;
- this.notifyTimer = null;
- }
- /**
- * Wire this chat to a collaboration client.
- * @param {DashCollaboration} client active collaboration transport
- */
- bind (client) {
- this.send = message => client.sendChat(message);
- client.onChat = message => this.receive(message);
- client.onChatHistory = messages => this.setHistory(messages);
- }
- setSelf (userId, username) {
- this.selfUserId = userId || null;
- this.selfUsername = username || null;
- }
- setHistory (messages) {
- const history = messages
- .filter(message => message && typeof message === 'object' &&
- typeof message.id === 'string' && typeof message.text === 'string' &&
- message.text.length <= MAX_TEXT && Number.isFinite(message.ts))
- .slice(-MAX_MESSAGES);
- this.messages = history;
- this.scheduleNotify();
- }
- receive (message) {
- if (!message || typeof message !== 'object') return;
- if (this.messages.some(existing => existing.id === message.id)) return;
- this.messages = [...this.messages, message].slice(-MAX_MESSAGES);
- const fromSelf = message.userId === this.selfUserId;
- if (!fromSelf && this.selfUsername &&
- typeof message.text === 'string' &&
- message.text.toLowerCase().includes(`@${this.selfUsername.toLowerCase()}`)) {
- this.unread++;
- this.lastMention = message;
- }
- this.scheduleNotify();
- }
- /**
- * Send a chat message from the local user.
- * @param {string} text message text (1-500 characters, may contain emoji)
- * @param {?object} pin optional pin context {type, targetId, blockId, name}
- * @returns {boolean} whether the message was handed to the transport
- */
- sendText (text, pin = null) {
- const value = typeof text === 'string' ? text : '';
- if (!value.trim().length || value.length > MAX_TEXT) return false;
- if (!this.send) return false;
- this.send({text: value, pin});
- return true;
- }
- markRead () {
- if (!this.unread && !this.lastMention) return;
- this.unread = 0;
- this.lastMention = null;
- this.scheduleNotify();
- }
- subscribe (listener) {
- this.listeners.add(listener);
- return () => this.listeners.delete(listener);
- }
- scheduleNotify () {
- if (this.notifyTimer) return;
- this.notifyTimer = setTimeout(() => {
- this.notifyTimer = null;
- for (const listener of [...this.listeners]) listener();
- }, 30);
- }
- destroy () {
- clearTimeout(this.notifyTimer);
- this.listeners.clear();
- this.messages = [];
- }
-}
-
-export {DashChat as default, listenDashChat, MAX_TEXT};
diff --git a/src/lib/dash-collaboration-session.js b/src/lib/dash-collaboration-session.js
index 044b5522e..09a7e5d52 100644
--- a/src/lib/dash-collaboration-session.js
+++ b/src/lib/dash-collaboration-session.js
@@ -2,8 +2,6 @@ import * as Y from 'yjs';
import JSZip from 'jszip';
import AddonHooks from '../addons/hooks';
import DashCollaboration from './dash-collaboration';
-import DashPresence from './dash-presence';
-import DashChat from './dash-chat';
const maps = ['blocks', 'variables', 'lists', 'broadcasts', 'comments'];
const blockStructureFields = ['opcode', 'next', 'parent', 'inputs', 'shadow', 'mutation'];
@@ -232,8 +230,6 @@ export default class DashCollaborationSession {
this.uploadedAssets = new Set();
this.extensionReferences = new Map();
this.extensionSources = new Map();
- this.presence = new DashPresence();
- this.chat = new DashChat();
this.installOperationTracking();
this.onLocalChange = () => {
if (this.applying || !this.loaded || this.disposed) return;
@@ -261,7 +257,6 @@ export default class DashCollaborationSession {
this.state = status.state;
this.error = status.error;
if (status.state === 'connected') {
- this.presence.setSelf(client.connectionId, client.userId);
if (this.resolveReady) this.resolveReady();
if (this.restoreOnReconnect) {
this.restoreAfterReconnect().catch(error => this.fail(error));
@@ -276,16 +271,10 @@ export default class DashCollaborationSession {
},
onMembers: members => {
this.members = members;
- this.presence.setMembers(members);
- const me = members.find(member => member.userId === client.userId &&
- (!client.connectionId || member.connectionId === client.connectionId));
- if (me) this.chat.setSelf(me.userId, me.username);
this.notify();
}
});
client.doc.on('update', this.onDocumentChange);
- this.presence.bind(client);
- this.chat.bind(client);
return client;
}
notify () {
@@ -743,8 +732,6 @@ export default class DashCollaborationSession {
}
this.client.doc.off('update', this.onDocumentChange);
this.client.destroy();
- this.presence.destroy();
- this.chat.destroy();
window.removeEventListener('beforeunload', this.beforeUnload);
}
}
diff --git a/src/lib/dash-collaboration.js b/src/lib/dash-collaboration.js
index 1c44cb6e5..e351b51c6 100644
--- a/src/lib/dash-collaboration.js
+++ b/src/lib/dash-collaboration.js
@@ -29,16 +29,12 @@ const projectTargetAssets = target => {
};
export default class DashCollaboration {
- constructor (projectId, {onStatus = () => {}, onMembers = () => {}, onPresence = () => {},
- onChat = () => {}, onChatHistory = () => {}} = {}) {
+ constructor (projectId, {onStatus = () => {}, onMembers = () => {}} = {}) {
if (!/^[1-9]\d{0,19}$/.test(String(projectId))) throw new Error('Invalid Dash project ID');
this.projectId = String(projectId);
this.doc = new Y.Doc();
this.onStatus = onStatus;
this.onMembers = onMembers;
- this.onPresence = onPresence;
- this.onChat = onChat;
- this.onChatHistory = onChatHistory;
this.active = false;
this.generation = 0;
this.attempts = 0;
@@ -48,8 +44,6 @@ export default class DashCollaboration {
this.epoch = null;
this.grantId = null;
this.userId = null;
- this.connectionId = null;
- this.features = new Set();
this.pending = null;
this.queued = null;
this.handleUpdate = this.handleUpdate.bind(this);
@@ -96,7 +90,7 @@ export default class DashCollaboration {
}, 10000);
ws.onopen = () => {
if (!this.active || generation !== this.generation) return ws.close();
- ws.send(JSON.stringify({type: 'auth', token: data.token, features: ['presence', 'chat']}));
+ ws.send(JSON.stringify({type: 'auth', token: data.token}));
};
ws.onmessage = event => {
if (!this.active || generation !== this.generation) return;
@@ -146,9 +140,6 @@ export default class DashCollaboration {
throw new Error('Room or permissions changed. Reopen the project before reconnecting.');
}
this.userId = message.userId;
- this.connectionId = typeof message.connectionId === 'string' ? message.connectionId : null;
- this.features = new Set(Array.isArray(message.features) ?
- message.features.filter(feature => feature === 'presence' || feature === 'chat') : []);
this.epoch = message.epoch;
this.grantId = message.grantId;
this.role = message.role;
@@ -169,24 +160,6 @@ export default class DashCollaboration {
}
} else if (message.type === 'members') {
this.onMembers(message.members);
- } else if (message.type === 'presence') {
- if (!message || typeof message.state !== 'object' || message.state === null ||
- Array.isArray(message.state) ||
- (typeof message.connectionId !== 'string' && typeof message.userId !== 'string')) {
- return;
- }
- this.onPresence(message);
- } else if (message.type === 'chat') {
- if (!message.message || typeof message.message !== 'object' ||
- typeof message.message.id !== 'string' || typeof message.message.userId !== 'string' ||
- typeof message.message.username !== 'string' || typeof message.message.text !== 'string' ||
- !Number.isFinite(message.message.ts)) {
- return;
- }
- this.onChat(message.message);
- } else if (message.type === 'chat-history') {
- if (!Array.isArray(message.messages)) return;
- this.onChatHistory(message.messages.slice(-200));
} else {
throw new Error('Unknown collaboration message');
}
@@ -203,36 +176,6 @@ export default class DashCollaboration {
}
return btoa(binary);
}
- /**
- * Broadcast ephemeral presence state (cursor, selection) to other
- * participants. Best-effort: never queued, never persisted, allowed for
- * viewers. Dropped silently when the connection is busy or closed.
- * @param {object} state small JSON-serializable presence payload
- */
- sendPresence (state) {
- if (!this.active || this.state !== 'connected' || !this.features.has('presence') || !this.socket ||
- this.socket.readyState !== WebSocket.OPEN) return;
- if (this.socket.bufferedAmount > 64 * 1024) return;
- try {
- this.socket.send(JSON.stringify({type: 'presence', state}));
- } catch (error) {
- // Presence is optional; ignore transport errors.
- }
- }
- /**
- * Send a chat message to the collaboration room. Delivered to all
- * participants (including viewers) and kept as room history on the server.
- * @param {{text: string, pin: ?object}} message chat message body
- */
- sendChat (message) {
- if (!this.active || this.state !== 'connected' || !this.features.has('chat') || !this.socket ||
- this.socket.readyState !== WebSocket.OPEN) return;
- try {
- this.socket.send(JSON.stringify({type: 'chat', text: message.text, pin: message.pin || null}));
- } catch (error) {
- // Chat is optional; ignore transport errors.
- }
- }
change (operation) {
if (this.state !== 'connected' || this.role === 'viewer') throw new Error('Collaboration is not writable');
this.doc.transact(() => operation(this.doc));
@@ -352,7 +295,6 @@ export default class DashCollaboration {
if (this.controller) this.controller.abort();
if (this.socket) this.socket.close();
this.socket = null;
- this.features = new Set();
this.onMembers([]);
this.setStatus('disconnected');
}
diff --git a/src/lib/dash-presence.js b/src/lib/dash-presence.js
deleted file mode 100644
index 172afb094..000000000
--- a/src/lib/dash-presence.js
+++ /dev/null
@@ -1,187 +0,0 @@
-/**
- * Ephemeral presence state for Dash live-collaboration sessions.
- * Tracks where each participant's cursor is (stage / blocks workspace), which
- * sprite they have open, and which block they are dragging. State is relayed
- * over the collaboration WebSocket as best-effort "presence" messages (never
- * written to the persistent Yjs document), so viewers can share presence too.
- */
-
-const PEER_TTL = 12000;
-const SEND_INTERVAL = 80;
-const NOTIFY_INTERVAL = 50;
-
-/**
- * Stable per-user color so cursors and avatars keep the same color over time.
- * @param {string} idString user id, connection id or similar stable identifier
- * @returns {string} CSS color
- */
-const presenceColor = idString => {
- let hash = 0;
- const value = String(idString);
- for (let i = 0; i < value.length; i++) {
- hash = ((hash << 5) - hash + value.charCodeAt(i)) | 0;
- }
- return `hsl(${Math.abs(hash) % 360}, 72%, 50%)`;
-};
-
-const sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
-
-/**
- * Subscribe a component to presence/session changes of a VM.
- * Re-attaches automatically when a collaboration session starts or ends.
- * @param {VirtualMachine} vm the VM owning the collaboration session
- * @param {function} onChange called when presence data may have changed
- * @returns {function} detach listener
- */
-const listenPresence = (vm, onChange) => {
- let presence = null;
- let unsubscribe = null;
- const attach = () => {
- const next = (vm.dashCollaboration && vm.dashCollaboration.presence) || null;
- if (next === presence) return;
- if (unsubscribe) {
- unsubscribe();
- unsubscribe = null;
- }
- presence = next;
- if (presence) unsubscribe = presence.subscribe(onChange);
- onChange();
- };
- const onStatus = () => attach();
- vm.on('DASH_COLLABORATION_STATUS', onStatus);
- attach();
- return () => {
- vm.removeListener('DASH_COLLABORATION_STATUS', onStatus);
- if (unsubscribe) unsubscribe();
- };
-};
-
-class DashPresence {
- constructor () {
- this.peers = new Map();
- this.listeners = new Set();
- this.local = {
- cursor: null,
- targetId: null,
- dragging: null
- };
- this.members = [];
- this.selfId = null;
- this.selfUserId = null;
- this.send = null;
- this.sendTimer = null;
- this.notifyTimer = null;
- this.pruneTimer = setInterval(() => this.prune(), 3000);
- }
- /**
- * Wire this presence channel to a collaboration client.
- * @param {DashCollaboration} client active collaboration transport
- */
- bind (client) {
- this.send = state => client.sendPresence(state);
- client.onPresence = message => this.receive(message);
- }
- setSelf (connectionId, userId) {
- this.selfId = connectionId || userId || null;
- this.selfUserId = userId || null;
- }
- setMembers (members) {
- this.members = Array.isArray(members) ? members : [];
- let changed = false;
- for (const [key, peer] of [...this.peers]) {
- const alive = this.members.some(member => (member.connectionId || member.userId) === key);
- if (!alive) {
- this.peers.delete(key);
- changed = true;
- }
- }
- if (changed) this.scheduleNotify();
- }
- /**
- * Publish a patch of the local presence state.
- * @param {{cursor: ?object, targetId: ?string, dragging: ?string}} patch partial local state
- */
- update (patch) {
- let changed = false;
- for (const key of Object.keys(patch)) {
- if (!sameValue(this.local[key], patch[key])) {
- this.local[key] = patch[key];
- changed = true;
- }
- }
- if (!changed) return;
- this.scheduleSend();
- this.scheduleNotify();
- }
- scheduleSend () {
- if (this.sendTimer) return;
- this.sendTimer = setTimeout(() => {
- this.sendTimer = null;
- if (this.send) this.send({...this.local});
- }, SEND_INTERVAL);
- }
- receive (message) {
- if (!message || typeof message !== 'object') return;
- const key = message.connectionId || message.userId;
- if (typeof key !== 'string' || !key) return;
- if (key === this.selfId) return;
- if (!this.selfId && this.selfUserId && message.userId === this.selfUserId) return;
- const state = message.state && typeof message.state === 'object' && !Array.isArray(message.state) ?
- message.state : {};
- this.peers.set(key, {
- id: key,
- userId: typeof message.userId === 'string' ? message.userId : '',
- username: typeof message.username === 'string' && message.username ? message.username : '...',
- color: presenceColor(key),
- state,
- seen: Date.now()
- });
- this.scheduleNotify();
- }
- prune () {
- const now = Date.now();
- let changed = false;
- for (const [key, peer] of [...this.peers]) {
- if (now - peer.seen > PEER_TTL) {
- this.peers.delete(key);
- changed = true;
- }
- }
- if (changed) this.scheduleNotify();
- }
- getPeers () {
- return [...this.peers.values()];
- }
- getMembers () {
- return this.members;
- }
- subscribe (listener) {
- this.listeners.add(listener);
- return () => this.listeners.delete(listener);
- }
- scheduleNotify () {
- if (this.notifyTimer) return;
- this.notifyTimer = setTimeout(() => {
- this.notifyTimer = null;
- for (const listener of [...this.listeners]) listener();
- }, NOTIFY_INTERVAL);
- }
- reset () {
- this.peers.clear();
- this.local = {
- cursor: null,
- targetId: null,
- dragging: null
- };
- this.scheduleNotify();
- }
- destroy () {
- clearInterval(this.pruneTimer);
- clearTimeout(this.sendTimer);
- clearTimeout(this.notifyTimer);
- this.listeners.clear();
- this.peers.clear();
- }
-}
-
-export {DashPresence as default, presenceColor, listenPresence};