mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -05:00
fix(realtime): allow realtime user status updates from users that have read-only access
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
46330563fa
commit
d29e840bc6
4 changed files with 276 additions and 56 deletions
|
@ -119,6 +119,13 @@ describe('realtime user status adapter', () => {
|
||||||
username: null,
|
username: null,
|
||||||
displayName: guestDisplayName,
|
displayName: guestDisplayName,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: null,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
username: clientDeclineUsername,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -180,6 +187,13 @@ describe('realtime user status adapter', () => {
|
||||||
username: null,
|
username: null,
|
||||||
displayName: guestDisplayName,
|
displayName: guestDisplayName,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: null,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
username: clientDeclineUsername,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -212,6 +226,55 @@ describe('realtime user status adapter', () => {
|
||||||
username: clientLoggedIn2Username,
|
username: clientLoggedIn2Username,
|
||||||
displayName: clientLoggedIn2Username,
|
displayName: clientLoggedIn2Username,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: null,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
username: clientDeclineUsername,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedMessage5: Message<MessageType.REALTIME_USER_STATE_SET> = {
|
||||||
|
type: MessageType.REALTIME_USER_STATE_SET,
|
||||||
|
payload: {
|
||||||
|
ownUser: {
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
},
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: {
|
||||||
|
from: newFrom,
|
||||||
|
to: newTo,
|
||||||
|
},
|
||||||
|
styleIndex: 0,
|
||||||
|
username: clientLoggedIn1Username,
|
||||||
|
displayName: clientLoggedIn1Username,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
|
styleIndex: 1,
|
||||||
|
username: clientLoggedIn2Username,
|
||||||
|
displayName: clientLoggedIn2Username,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
|
displayName: guestDisplayName,
|
||||||
|
styleIndex: 2,
|
||||||
|
username: null,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -226,7 +289,10 @@ describe('realtime user status adapter', () => {
|
||||||
expectedMessage3,
|
expectedMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expectedMessage5,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will inform other clients about removed client', () => {
|
it('will inform other clients about removed client', () => {
|
||||||
|
@ -256,6 +322,13 @@ describe('realtime user status adapter', () => {
|
||||||
username: null,
|
username: null,
|
||||||
displayName: guestDisplayName,
|
displayName: guestDisplayName,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: null,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
username: clientDeclineUsername,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -278,6 +351,45 @@ describe('realtime user status adapter', () => {
|
||||||
username: clientLoggedIn1Username,
|
username: clientLoggedIn1Username,
|
||||||
displayName: clientLoggedIn1Username,
|
displayName: clientLoggedIn1Username,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: null,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
username: clientDeclineUsername,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedMessage5: Message<MessageType.REALTIME_USER_STATE_SET> = {
|
||||||
|
type: MessageType.REALTIME_USER_STATE_SET,
|
||||||
|
payload: {
|
||||||
|
ownUser: {
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
},
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
|
styleIndex: 0,
|
||||||
|
username: clientLoggedIn1Username,
|
||||||
|
displayName: clientLoggedIn1Username,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
|
displayName: guestDisplayName,
|
||||||
|
styleIndex: 2,
|
||||||
|
username: null,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -292,7 +404,10 @@ describe('realtime user status adapter', () => {
|
||||||
expectedMessage3,
|
expectedMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expectedMessage5,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('will inform other clients about inactivity and reactivity', () => {
|
it('will inform other clients about inactivity and reactivity', () => {
|
||||||
|
@ -338,7 +453,14 @@ describe('realtime user status adapter', () => {
|
||||||
},
|
},
|
||||||
styleIndex: 2,
|
styleIndex: 2,
|
||||||
username: null,
|
username: null,
|
||||||
displayName: 'Virtuous Mockingbird',
|
displayName: guestDisplayName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: null,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
username: clientDeclineUsername,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -350,7 +472,7 @@ describe('realtime user status adapter', () => {
|
||||||
payload: {
|
payload: {
|
||||||
ownUser: {
|
ownUser: {
|
||||||
styleIndex: 2,
|
styleIndex: 2,
|
||||||
displayName: 'Virtuous Mockingbird',
|
displayName: guestDisplayName,
|
||||||
},
|
},
|
||||||
users: [
|
users: [
|
||||||
{
|
{
|
||||||
|
@ -373,6 +495,56 @@ describe('realtime user status adapter', () => {
|
||||||
username: clientLoggedIn2Username,
|
username: clientLoggedIn2Username,
|
||||||
displayName: clientLoggedIn2Username,
|
displayName: clientLoggedIn2Username,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: null,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
username: clientDeclineUsername,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedInactivityMessage5: Message<MessageType.REALTIME_USER_STATE_SET> =
|
||||||
|
{
|
||||||
|
type: MessageType.REALTIME_USER_STATE_SET,
|
||||||
|
payload: {
|
||||||
|
ownUser: {
|
||||||
|
styleIndex: 4,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
},
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
cursor: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
|
styleIndex: 0,
|
||||||
|
username: clientLoggedIn1Username,
|
||||||
|
displayName: clientLoggedIn1Username,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
|
styleIndex: 1,
|
||||||
|
username: clientLoggedIn2Username,
|
||||||
|
displayName: clientLoggedIn2Username,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
|
displayName: guestDisplayName,
|
||||||
|
styleIndex: 2,
|
||||||
|
username: null,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -387,7 +559,10 @@ describe('realtime user status adapter', () => {
|
||||||
expectedInactivityMessage3,
|
expectedInactivityMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expectedInactivityMessage5,
|
||||||
|
);
|
||||||
|
|
||||||
clientLoggedIn1
|
clientLoggedIn1
|
||||||
.getTransporter()
|
.getTransporter()
|
||||||
|
@ -408,7 +583,10 @@ describe('realtime user status adapter', () => {
|
||||||
expectedInactivityMessage3,
|
expectedInactivityMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(1);
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expectedInactivityMessage5,
|
||||||
|
);
|
||||||
|
|
||||||
clientLoggedIn1
|
clientLoggedIn1
|
||||||
.getTransporter()
|
.getTransporter()
|
||||||
|
@ -446,7 +624,14 @@ describe('realtime user status adapter', () => {
|
||||||
},
|
},
|
||||||
styleIndex: 2,
|
styleIndex: 2,
|
||||||
username: null,
|
username: null,
|
||||||
displayName: 'Virtuous Mockingbird',
|
displayName: guestDisplayName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: null,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
username: clientDeclineUsername,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -458,7 +643,7 @@ describe('realtime user status adapter', () => {
|
||||||
payload: {
|
payload: {
|
||||||
ownUser: {
|
ownUser: {
|
||||||
styleIndex: 2,
|
styleIndex: 2,
|
||||||
displayName: 'Virtuous Mockingbird',
|
displayName: guestDisplayName,
|
||||||
},
|
},
|
||||||
users: [
|
users: [
|
||||||
{
|
{
|
||||||
|
@ -481,6 +666,56 @@ describe('realtime user status adapter', () => {
|
||||||
username: clientLoggedIn2Username,
|
username: clientLoggedIn2Username,
|
||||||
displayName: clientLoggedIn2Username,
|
displayName: clientLoggedIn2Username,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: null,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
styleIndex: 4,
|
||||||
|
username: clientDeclineUsername,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedReactivityMessage5: Message<MessageType.REALTIME_USER_STATE_SET> =
|
||||||
|
{
|
||||||
|
type: MessageType.REALTIME_USER_STATE_SET,
|
||||||
|
payload: {
|
||||||
|
ownUser: {
|
||||||
|
styleIndex: 4,
|
||||||
|
displayName: clientDeclineUsername,
|
||||||
|
},
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
|
styleIndex: 0,
|
||||||
|
username: clientLoggedIn1Username,
|
||||||
|
displayName: clientLoggedIn1Username,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
|
styleIndex: 1,
|
||||||
|
username: clientLoggedIn2Username,
|
||||||
|
displayName: clientLoggedIn2Username,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
cursor: {
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
|
displayName: guestDisplayName,
|
||||||
|
styleIndex: 2,
|
||||||
|
username: null,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -495,7 +730,10 @@ describe('realtime user status adapter', () => {
|
||||||
expectedReactivityMessage3,
|
expectedReactivityMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(2);
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
expectedReactivityMessage5,
|
||||||
|
);
|
||||||
|
|
||||||
clientLoggedIn1
|
clientLoggedIn1
|
||||||
.getTransporter()
|
.getTransporter()
|
||||||
|
@ -516,30 +754,9 @@ describe('realtime user status adapter', () => {
|
||||||
expectedReactivityMessage3,
|
expectedReactivityMessage3,
|
||||||
);
|
);
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(2);
|
expect(clientDeclineSendMessageSpy).toHaveBeenNthCalledWith(
|
||||||
});
|
1,
|
||||||
|
expectedReactivityMessage5,
|
||||||
it('will ignore updates from read only clients', () => {
|
);
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
clientDecline
|
|
||||||
.getTransporter()
|
|
||||||
.emit(MessageType.REALTIME_USER_SINGLE_UPDATE, {
|
|
||||||
type: MessageType.REALTIME_USER_SINGLE_UPDATE,
|
|
||||||
payload: {
|
|
||||||
from: 0,
|
|
||||||
to: 1234,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(clientLoggedIn1SendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
expect(clientLoggedIn2SendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
expect(clientGuestSendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
expect(clientNotReadySendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
expect(clientDeclineSendMessageSpy).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -41,10 +41,12 @@ export class RealtimeUserStatusAdapter {
|
||||||
styleIndex: this.findLeastUsedStyleIndex(
|
styleIndex: this.findLeastUsedStyleIndex(
|
||||||
this.createStyleIndexToCountMap(realtimeNote),
|
this.createStyleIndexToCountMap(realtimeNote),
|
||||||
),
|
),
|
||||||
cursor: {
|
cursor: !this.acceptCursorUpdateProvider()
|
||||||
from: 0,
|
? null
|
||||||
to: 0,
|
: {
|
||||||
},
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,10 +55,10 @@ export class RealtimeUserStatusAdapter {
|
||||||
const transporterMessagesListener = connection.getTransporter().on(
|
const transporterMessagesListener = connection.getTransporter().on(
|
||||||
MessageType.REALTIME_USER_SINGLE_UPDATE,
|
MessageType.REALTIME_USER_SINGLE_UPDATE,
|
||||||
(message: Message<MessageType.REALTIME_USER_SINGLE_UPDATE>) => {
|
(message: Message<MessageType.REALTIME_USER_SINGLE_UPDATE>) => {
|
||||||
if (this.acceptCursorUpdateProvider()) {
|
this.realtimeUser.cursor = this.acceptCursorUpdateProvider()
|
||||||
this.realtimeUser.cursor = message.payload;
|
? message.payload
|
||||||
this.sendRealtimeUserStatusUpdateEvent(connection);
|
: null;
|
||||||
}
|
this.sendRealtimeUserStatusUpdateEvent(connection);
|
||||||
},
|
},
|
||||||
{ objectify: true },
|
{ objectify: true },
|
||||||
) as Listener;
|
) as Listener;
|
||||||
|
@ -83,10 +85,7 @@ export class RealtimeUserStatusAdapter {
|
||||||
const realtimeUserSetActivityListener = connection.getTransporter().on(
|
const realtimeUserSetActivityListener = connection.getTransporter().on(
|
||||||
MessageType.REALTIME_USER_SET_ACTIVITY,
|
MessageType.REALTIME_USER_SET_ACTIVITY,
|
||||||
(message: Message<MessageType.REALTIME_USER_SET_ACTIVITY>) => {
|
(message: Message<MessageType.REALTIME_USER_SET_ACTIVITY>) => {
|
||||||
if (
|
if (this.realtimeUser.active === message.payload.active) {
|
||||||
!this.acceptCursorUpdateProvider() ||
|
|
||||||
this.realtimeUser.active === message.payload.active
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.realtimeUser.active = message.payload.active;
|
this.realtimeUser.active = message.payload.active;
|
||||||
|
@ -115,9 +114,6 @@ export class RealtimeUserStatusAdapter {
|
||||||
const realtimeUser =
|
const realtimeUser =
|
||||||
receivingClient.getRealtimeUserStateAdapter().realtimeUser;
|
receivingClient.getRealtimeUserStateAdapter().realtimeUser;
|
||||||
const realtimeUsers = this.collectAllConnectionsExcept(receivingClient)
|
const realtimeUsers = this.collectAllConnectionsExcept(receivingClient)
|
||||||
.filter((client) =>
|
|
||||||
client.getRealtimeUserStateAdapter().acceptCursorUpdateProvider(),
|
|
||||||
)
|
|
||||||
.map((client) => client.getRealtimeUserStateAdapter().realtimeUser)
|
.map((client) => client.getRealtimeUserStateAdapter().realtimeUser)
|
||||||
.filter((realtimeUser) => realtimeUser !== null);
|
.filter((realtimeUser) => realtimeUser !== null);
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ export interface RealtimeUser {
|
||||||
username: string | null
|
username: string | null
|
||||||
active: boolean
|
active: boolean
|
||||||
styleIndex: number
|
styleIndex: number
|
||||||
cursor: RemoteCursor
|
cursor: RemoteCursor | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoteCursor {
|
export interface RemoteCursor {
|
||||||
|
|
|
@ -20,12 +20,19 @@ export class ReceiveRemoteCursorViewPlugin implements PluginValue {
|
||||||
this.listener = messageTransporter.on(
|
this.listener = messageTransporter.on(
|
||||||
MessageType.REALTIME_USER_STATE_SET,
|
MessageType.REALTIME_USER_STATE_SET,
|
||||||
({ payload }) => {
|
({ payload }) => {
|
||||||
const cursors: RemoteCursor[] = payload.users.map((user) => ({
|
const cursors = payload.users
|
||||||
from: user.cursor.from,
|
.map((user) => {
|
||||||
to: user.cursor.to,
|
if (!user.cursor) {
|
||||||
displayName: user.displayName,
|
return undefined
|
||||||
styleIndex: user.styleIndex
|
}
|
||||||
}))
|
return {
|
||||||
|
from: user.cursor.from,
|
||||||
|
to: user.cursor.to,
|
||||||
|
displayName: user.displayName,
|
||||||
|
styleIndex: user.styleIndex
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((value) => value !== undefined) as RemoteCursor[]
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
effects: [remoteCursorUpdateEffect.of(cursors)]
|
effects: [remoteCursorUpdateEffect.of(cursors)]
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue