wip: range authorships backend storage

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2024-04-28 10:38:03 +02:00
parent c5dc671398
commit 68780f54e1
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
29 changed files with 316 additions and 133 deletions

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -76,7 +76,8 @@ const routes: Routes = [
detectTsNode() ? 'ts' : 'js' detectTsNode() ? 'ts' : 'js'
}`, }`,
], ],
migrationsRun: true, migrationsRun: false,
synchronize: true,
}; };
}, },
}), }),

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -11,7 +11,7 @@ import {
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
} from 'typeorm'; } from 'typeorm';
import { Edit } from '../revisions/edit.entity'; import { RangeAuthorship } from '../revisions/range-authorship.entity';
import { Session } from '../sessions/session.entity'; import { Session } from '../sessions/session.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
@ -52,8 +52,8 @@ export class Author {
* List of edits that this author created * List of edits that this author created
* All edits must belong to the same note * All edits must belong to the same note
*/ */
@OneToMany(() => Edit, (edit) => edit.author) @OneToMany(() => RangeAuthorship, (edit) => edit.author)
edits: Promise<Edit[]>; edits: Promise<RangeAuthorship[]>;
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}

View file

@ -1,14 +1,18 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { LoggerModule } from '../logger/logger.module';
import { Author } from './author.entity'; import { Author } from './author.entity';
import { AuthorsService } from './authors.service';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Author])], imports: [TypeOrmModule.forFeature([Author]), LoggerModule],
providers: [AuthorsService],
exports: [AuthorsService],
}) })
export class AuthorsModule {} export class AuthorsModule {}

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ShortRealtimeUser } from '@hedgedoc/commons';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { Author } from './author.entity';
@Injectable()
export class AuthorsService {
constructor(
private readonly logger: ConsoleLoggerService,
@InjectRepository(Author) private authorsRepository: Repository<Author>,
) {
this.logger.setContext(AuthorsService.name);
}
// /**
// * @async
// * Get or create the author specified by the short realtime user
// * @param {Username} username the username by which the user is specified
// * @param {UserRelationEnum[]} [withRelations=[]] if the returned user object should contain certain relations
// * @return {User} the specified user
// */
// async findOrCreateAuthor(
// shortRealtimeUser: ShortRealtimeUser,
// ): Promise<Author> {
// const author = await this.authorsRepository.findOne({
// where: { username: username },
// relations: withRelations,
// });
// if (user === null) {
// throw new NotInDBError(`User with username '${username}' not found`);
// }
// return user;
// }
}

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -28,7 +28,7 @@ import { NotesModule } from '../notes/notes.module';
import { Tag } from '../notes/tag.entity'; import { Tag } from '../notes/tag.entity';
import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
import { NoteUserPermission } from '../permissions/note-user-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { Edit } from '../revisions/edit.entity'; import { RangeAuthorship } from '../revisions/edit.entity';
import { Revision } from '../revisions/revision.entity'; import { Revision } from '../revisions/revision.entity';
import { RevisionsModule } from '../revisions/revisions.module'; import { RevisionsModule } from '../revisions/revisions.module';
import { RevisionsService } from '../revisions/revisions.service'; import { RevisionsService } from '../revisions/revisions.service';
@ -116,7 +116,7 @@ describe('HistoryService', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Identity)) .overrideProvider(getRepositoryToken(Identity))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Edit)) .overrideProvider(getRepositoryToken(RangeAuthorship))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Revision)) .overrideProvider(getRepositoryToken(Revision))
.useValue({}) .useValue({})

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -28,7 +28,7 @@ import { NotesModule } from '../notes/notes.module';
import { Tag } from '../notes/tag.entity'; import { Tag } from '../notes/tag.entity';
import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
import { NoteUserPermission } from '../permissions/note-user-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { Edit } from '../revisions/edit.entity'; import { RangeAuthorship } from '../revisions/edit.entity';
import { Revision } from '../revisions/revision.entity'; import { Revision } from '../revisions/revision.entity';
import { Session } from '../sessions/session.entity'; import { Session } from '../sessions/session.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
@ -82,7 +82,7 @@ describe('MediaService', () => {
EventEmitterModule.forRoot(eventModuleConfig), EventEmitterModule.forRoot(eventModuleConfig),
], ],
}) })
.overrideProvider(getRepositoryToken(Edit)) .overrideProvider(getRepositoryToken(RangeAuthorship))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(AuthToken)) .overrideProvider(getRepositoryToken(AuthToken))
.useValue({}) .useValue({})

View file

@ -30,7 +30,7 @@ import { LoggerModule } from '../logger/logger.module';
import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
import { NoteUserPermission } from '../permissions/note-user-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module'; import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module';
import { Edit } from '../revisions/edit.entity'; import { RangeAuthorship } from '../revisions/edit.entity';
import { Revision } from '../revisions/revision.entity'; import { Revision } from '../revisions/revision.entity';
import { RevisionsModule } from '../revisions/revisions.module'; import { RevisionsModule } from '../revisions/revisions.module';
import { Session } from '../sessions/session.entity'; import { Session } from '../sessions/session.entity';
@ -122,7 +122,7 @@ describe('AliasService', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Identity)) .overrideProvider(getRepositoryToken(Identity))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Edit)) .overrideProvider(getRepositoryToken(RangeAuthorship))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Revision)) .overrideProvider(getRepositoryToken(Revision))
.useClass(Repository) .useClass(Repository)

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -7,7 +7,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsArray, IsString, ValidateNested } from 'class-validator'; import { IsArray, IsString, ValidateNested } from 'class-validator';
import { EditDto } from '../revisions/edit.dto'; import { RangeAuthorshipDto } from '../revisions/range-authorship.dto';
import { BaseDto } from '../utils/base.dto.'; import { BaseDto } from '../utils/base.dto.';
import { NoteMetadataDto } from './note-metadata.dto'; import { NoteMetadataDto } from './note-metadata.dto';
@ -33,7 +33,7 @@ export class NoteDto extends BaseDto {
*/ */
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => EditDto) @Type(() => RangeAuthorshipDto)
@ApiProperty({ isArray: true, type: EditDto }) @ApiProperty({ isArray: true, type: RangeAuthorshipDto })
editedByAtPosition: EditDto[]; editedByAtPosition: RangeAuthorshipDto[];
} }

View file

@ -41,7 +41,7 @@ import { LoggerModule } from '../logger/logger.module';
import { NoteGroupPermission } from '../permissions/note-group-permission.entity'; import { NoteGroupPermission } from '../permissions/note-group-permission.entity';
import { NoteUserPermission } from '../permissions/note-user-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module'; import { RealtimeNoteModule } from '../realtime/realtime-note/realtime-note.module';
import { Edit } from '../revisions/edit.entity'; import { RangeAuthorship } from '../revisions/edit.entity';
import { Revision } from '../revisions/revision.entity'; import { Revision } from '../revisions/revision.entity';
import { RevisionsModule } from '../revisions/revisions.module'; import { RevisionsModule } from '../revisions/revisions.module';
import { RevisionsService } from '../revisions/revisions.service'; import { RevisionsService } from '../revisions/revisions.service';
@ -198,7 +198,7 @@ describe('NotesService', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Identity)) .overrideProvider(getRepositoryToken(Identity))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Edit)) .overrideProvider(getRepositoryToken(RangeAuthorship))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Revision)) .overrideProvider(getRepositoryToken(Revision))
.useClass(Repository) .useClass(Repository)
@ -248,13 +248,13 @@ describe('NotesService', () => {
endPos: 1, endPos: 1,
updatedAt: new Date(1549312452000), updatedAt: new Date(1549312452000),
author: Promise.resolve(author), author: Promise.resolve(author),
} as Edit, } as RangeAuthorship,
{ {
startPos: 0, startPos: 0,
endPos: 1, endPos: 1,
updatedAt: new Date(1549312452001), updatedAt: new Date(1549312452001),
author: Promise.resolve(author), author: Promise.resolve(author),
} as Edit, } as RangeAuthorship,
]), ]),
createdAt: new Date(1549312452000), createdAt: new Date(1549312452000),
tags: Promise.resolve([ tags: Promise.resolve([

View file

@ -104,6 +104,7 @@ export class NotesService {
const newRevision = await this.revisionsService.createRevision( const newRevision = await this.revisionsService.createRevision(
newNote, newNote,
noteContent, noteContent,
[], // TODO Use the correct rangeAuthorships
); );
newNote.revisions = Promise.resolve( newNote.revisions = Promise.resolve(
newRevision === undefined ? [] : [newRevision], newRevision === undefined ? [] : [newRevision],
@ -261,7 +262,7 @@ export class NotesService {
.createQueryBuilder('user') .createQueryBuilder('user')
.innerJoin('user.authors', 'author') .innerJoin('user.authors', 'author')
.innerJoin('author.edits', 'edit') .innerJoin('author.edits', 'edit')
.innerJoin('edit.revisions', 'revision') .innerJoin('edit.revision', 'revision')
.innerJoin('revision.note', 'note') .innerJoin('revision.note', 'note')
.where('note.id = :id', { id: note.id }) .where('note.id = :id', { id: note.id })
.getMany(); .getMany();
@ -351,6 +352,7 @@ export class NotesService {
const newRevision = await this.revisionsService.createRevision( const newRevision = await this.revisionsService.createRevision(
note, note,
noteContent, noteContent,
[], // TODO Use the correct rangeAuthorships
); );
if (newRevision !== undefined) { if (newRevision !== undefined) {
revisions.push(newRevision); revisions.push(newRevision);
@ -367,12 +369,12 @@ export class NotesService {
*/ */
async calculateUpdateUser(note: Note): Promise<User | null> { async calculateUpdateUser(note: Note): Promise<User | null> {
const lastRevision = await this.revisionsService.getLatestRevision(note); const lastRevision = await this.revisionsService.getLatestRevision(note);
const edits = await lastRevision.edits; const rangeAuthorships = await lastRevision.rangeAuthorships;
if (edits.length > 0) { if (rangeAuthorships.length > 0) {
// Sort the last Revisions Edits by their updatedAt Date to get the latest one // Sort the last Revisions Edits by their updatedAt Date to get the latest one
// the user of that Edit is the updateUser // the user of that Edit is the updateUser
return await ( return await (
await edits.sort( await rangeAuthorships.sort(
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(), (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
)[0].author )[0].author
).user; ).user;

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -38,7 +38,7 @@ import {
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
import { NotesModule } from '../notes/notes.module'; import { NotesModule } from '../notes/notes.module';
import { Tag } from '../notes/tag.entity'; import { Tag } from '../notes/tag.entity';
import { Edit } from '../revisions/edit.entity'; import { RangeAuthorship } from '../revisions/edit.entity';
import { Revision } from '../revisions/revision.entity'; import { Revision } from '../revisions/revision.entity';
import { Session } from '../sessions/session.entity'; import { Session } from '../sessions/session.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
@ -166,7 +166,7 @@ describe('PermissionsService', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Identity)) .overrideProvider(getRepositoryToken(Identity))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Edit)) .overrideProvider(getRepositoryToken(RangeAuthorship))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Revision)) .overrideProvider(getRepositoryToken(Revision))
.useValue({}) .useValue({})

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -47,6 +47,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
.createAndSaveRevision( .createAndSaveRevision(
realtimeNote.getNote(), realtimeNote.getNote(),
realtimeNote.getRealtimeDoc().getCurrentContent(), realtimeNote.getRealtimeDoc().getCurrentContent(),
realtimeNote.getRealtimeDoc().getAbsolutePositionAuthorships(),
realtimeNote.getRealtimeDoc().encodeStateAsUpdate(), realtimeNote.getRealtimeDoc().encodeStateAsUpdate(),
) )
.catch((reason) => this.logger.error(reason)); .catch((reason) => this.logger.error(reason));

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -125,6 +125,7 @@ export class RealtimeUserStatusAdapter {
ownUser: { ownUser: {
displayName: this.realtimeUser.displayName, displayName: this.realtimeUser.displayName,
styleIndex: this.realtimeUser.styleIndex, styleIndex: this.realtimeUser.styleIndex,
username: this.realtimeUser.username,
}, },
}, },
}); });

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -33,7 +33,7 @@ import { NotePermission } from '../../permissions/note-permission.enum';
import { NoteUserPermission } from '../../permissions/note-user-permission.entity'; import { NoteUserPermission } from '../../permissions/note-user-permission.entity';
import { PermissionsModule } from '../../permissions/permissions.module'; import { PermissionsModule } from '../../permissions/permissions.module';
import { PermissionsService } from '../../permissions/permissions.service'; import { PermissionsService } from '../../permissions/permissions.service';
import { Edit } from '../../revisions/edit.entity'; import { RangeAuthorship } from '../../revisions/range-authorship.entity';
import { Revision } from '../../revisions/revision.entity'; import { Revision } from '../../revisions/revision.entity';
import { Session } from '../../sessions/session.entity'; import { Session } from '../../sessions/session.entity';
import { SessionModule } from '../../sessions/session.module'; import { SessionModule } from '../../sessions/session.module';
@ -126,7 +126,7 @@ describe('Websocket gateway', () => {
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Identity)) .overrideProvider(getRepositoryToken(Identity))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Edit)) .overrideProvider(getRepositoryToken(RangeAuthorship))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(Revision)) .overrideProvider(getRepositoryToken(Revision))
.useValue({}) .useValue({})

View file

@ -1,24 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { EditDto } from './edit.dto';
import { Edit } from './edit.entity';
@Injectable()
export class EditService {
async toEditDto(edit: Edit): Promise<EditDto> {
const authorUser = await (await edit.author).user;
return {
username: authorUser ? authorUser.username : null,
startPos: edit.startPos,
endPos: edit.endPos,
createdAt: edit.createdAt,
updatedAt: edit.updatedAt,
};
}
}

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -10,7 +10,7 @@ import { IsDate, IsNumber, IsOptional, IsString, Min } from 'class-validator';
import { UserInfoDto } from '../users/user-info.dto'; import { UserInfoDto } from '../users/user-info.dto';
import { BaseDto } from '../utils/base.dto.'; import { BaseDto } from '../utils/base.dto.';
export class EditDto extends BaseDto { export class RangeAuthorshipDto extends BaseDto {
/** /**
* Username of the user who authored this section * Username of the user who authored this section
* Is `null` if the user is anonymous * Is `null` if the user is anonymous

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -7,7 +7,6 @@ import {
Column, Column,
CreateDateColumn, CreateDateColumn,
Entity, Entity,
ManyToMany,
ManyToOne, ManyToOne,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
UpdateDateColumn, UpdateDateColumn,
@ -17,18 +16,18 @@ import { Author } from '../authors/author.entity';
import { Revision } from './revision.entity'; import { Revision } from './revision.entity';
/** /**
* The Edit represents a change in the content of a note by a particular {@link Author} * The RangeAuthorship represents a change in the content of a note by a particular {@link Author}
*/ */
@Entity() @Entity()
export class Edit { export class RangeAuthorship {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
/** /**
* Revisions this edit appears in * Revisions this edit appears in
*/ */
@ManyToMany((_) => Revision, (revision) => revision.edits) @ManyToOne((_) => Revision, (revision) => revision.rangeAuthorships)
revisions: Promise<Revision[]>; revision: Promise<Revision>;
/** /**
* Author that created the change * Author that created the change
@ -55,9 +54,8 @@ export class Edit {
author: Author, author: Author,
startPos: number, startPos: number,
endPos: number, endPos: number,
): Omit<Edit, 'id' | 'createdAt' | 'updatedAt'> { ): Omit<RangeAuthorship, 'id' | 'createdAt' | 'updatedAt' | 'revision'> {
const newEdit = new Edit(); const newEdit = new RangeAuthorship();
newEdit.revisions = Promise.resolve([]);
newEdit.author = Promise.resolve(author); newEdit.author = Promise.resolve(author);
newEdit.startPos = startPos; newEdit.startPos = startPos;
newEdit.endPos = endPos; newEdit.endPos = endPos;

View file

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AbsolutePositionAuthorship } from '@hedgedoc/commons';
import { Injectable } from '@nestjs/common';
import { Author } from '../authors/author.entity';
import { RangeAuthorshipDto } from './range-authorship.dto';
import { RangeAuthorship } from './range-authorship.entity';
@Injectable()
export class RangeAuthorshipService {
createRangeAuthorshipsFromAbsolutePositions(
absolutePositions: AbsolutePositionAuthorship[],
noteLength: number,
): RangeAuthorship[] {
return absolutePositions
.sort(([positionA], [positionB]) => {
return positionB - positionA;
})
.reduce<RangeAuthorship[]>((authorships, [startPosition, _]) => {
const author = Author.create(1) as Author; // ToDo: use the real author here
const endPosition = authorships[0]?.startPos - 1 ?? noteLength;
authorships.unshift(
RangeAuthorship.create(
author,
startPosition,
endPosition,
) as RangeAuthorship,
);
return authorships;
}, []);
}
async toRangeAuthorshipDto(
rangeAuthorship: RangeAuthorship,
): Promise<RangeAuthorshipDto> {
const authorUser = await (await rangeAuthorship.author).user;
return {
username: authorUser ? authorUser.username : null,
startPos: rangeAuthorship.startPos,
endPos: rangeAuthorship.endPos,
createdAt: rangeAuthorship.createdAt,
updatedAt: rangeAuthorship.updatedAt,
};
}
}

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -7,7 +7,7 @@ import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsString, ValidateNested } from 'class-validator'; import { IsString, ValidateNested } from 'class-validator';
import { EditDto } from './edit.dto'; import { RangeAuthorshipDto } from './range-authorship.dto';
import { RevisionMetadataDto } from './revision-metadata.dto'; import { RevisionMetadataDto } from './revision-metadata.dto';
export class RevisionDto extends RevisionMetadataDto { export class RevisionDto extends RevisionMetadataDto {
@ -27,10 +27,10 @@ export class RevisionDto extends RevisionMetadataDto {
patch: string; patch: string;
/** /**
* All edit objects which are used in the revision. * All range authorship objects which are used in the revision.
*/ */
@Type(() => EditDto) @Type(() => RangeAuthorshipDto)
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@ApiProperty({ isArray: true, type: EditDto }) @ApiProperty({ isArray: true, type: RangeAuthorshipDto })
edits: EditDto[]; rangeAuthorships: RangeAuthorshipDto[];
} }

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -10,12 +10,13 @@ import {
JoinTable, JoinTable,
ManyToMany, ManyToMany,
ManyToOne, ManyToOne,
OneToMany,
PrimaryGeneratedColumn, PrimaryGeneratedColumn,
} from 'typeorm'; } from 'typeorm';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
import { Tag } from '../notes/tag.entity'; import { Tag } from '../notes/tag.entity';
import { Edit } from './edit.entity'; import { RangeAuthorship } from './range-authorship.entity';
/** /**
* The state of a note at a particular point in time, * The state of a note at a particular point in time,
@ -84,9 +85,12 @@ export class Revision {
/** /**
* All edit objects which are used in the revision. * All edit objects which are used in the revision.
*/ */
@ManyToMany((_) => Edit, (edit) => edit.revisions) @OneToMany(
@JoinTable() (_) => RangeAuthorship,
edits: Promise<Edit[]>; (rangeAuthorship) => rangeAuthorship.revision,
{ onDelete: 'CASCADE' },
)
rangeAuthorships: Promise<RangeAuthorship[]>;
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {} private constructor() {}
@ -99,6 +103,7 @@ export class Revision {
title: string, title: string,
description: string, description: string,
tags: Tag[], tags: Tag[],
rangeAuthorships: RangeAuthorship[],
): Omit<Revision, 'id' | 'createdAt'> { ): Omit<Revision, 'id' | 'createdAt'> {
const newRevision = new Revision(); const newRevision = new Revision();
newRevision.patch = patch; newRevision.patch = patch;
@ -108,7 +113,7 @@ export class Revision {
newRevision.description = description; newRevision.description = description;
newRevision.tags = Promise.resolve(tags); newRevision.tags = Promise.resolve(tags);
newRevision.note = Promise.resolve(note); newRevision.note = Promise.resolve(note);
newRevision.edits = Promise.resolve([]); newRevision.rangeAuthorships = Promise.resolve(rangeAuthorships);
newRevision.yjsStateVector = yjsStateVector ?? null; newRevision.yjsStateVector = yjsStateVector ?? null;
return newRevision; return newRevision;
} }

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -9,19 +9,19 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthorsModule } from '../authors/authors.module'; import { AuthorsModule } from '../authors/authors.module';
import { LoggerModule } from '../logger/logger.module'; import { LoggerModule } from '../logger/logger.module';
import { Edit } from './edit.entity'; import { RangeAuthorship } from './range-authorship.entity';
import { EditService } from './edit.service'; import { RangeAuthorshipService } from './range-authorship.service';
import { Revision } from './revision.entity'; import { Revision } from './revision.entity';
import { RevisionsService } from './revisions.service'; import { RevisionsService } from './revisions.service';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Revision, Edit]), TypeOrmModule.forFeature([Revision, RangeAuthorship]),
LoggerModule, LoggerModule,
ConfigModule, ConfigModule,
AuthorsModule, AuthorsModule,
], ],
providers: [RevisionsService, EditService], providers: [RevisionsService, RangeAuthorshipService],
exports: [RevisionsService, EditService], exports: [RevisionsService, RangeAuthorshipService],
}) })
export class RevisionsModule {} export class RevisionsModule {}

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -29,8 +29,8 @@ import { NoteGroupPermission } from '../permissions/note-group-permission.entity
import { NoteUserPermission } from '../permissions/note-user-permission.entity'; import { NoteUserPermission } from '../permissions/note-user-permission.entity';
import { Session } from '../sessions/session.entity'; import { Session } from '../sessions/session.entity';
import { User } from '../users/user.entity'; import { User } from '../users/user.entity';
import { Edit } from './edit.entity'; import { RangeAuthorship } from './edit.entity';
import { EditService } from './edit.service'; import { RangeAuthorshipService } from './range-authorship.service';
import { Revision } from './revision.entity'; import { Revision } from './revision.entity';
import { RevisionsService } from './revisions.service'; import { RevisionsService } from './revisions.service';
@ -42,7 +42,7 @@ describe('RevisionsService', () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
RevisionsService, RevisionsService,
EditService, RangeAuthorshipService,
{ {
provide: getRepositoryToken(Revision), provide: getRepositoryToken(Revision),
useClass: Repository, useClass: Repository,
@ -63,7 +63,7 @@ describe('RevisionsService', () => {
EventEmitterModule.forRoot(eventModuleConfig), EventEmitterModule.forRoot(eventModuleConfig),
], ],
}) })
.overrideProvider(getRepositoryToken(Edit)) .overrideProvider(getRepositoryToken(RangeAuthorship))
.useValue({}) .useValue({})
.overrideProvider(getRepositoryToken(User)) .overrideProvider(getRepositoryToken(User))
.useValue({}) .useValue({})
@ -183,12 +183,14 @@ describe('RevisionsService', () => {
author.user = Promise.resolve(user); author.user = Promise.resolve(user);
const anonAuthor = Author.create(123) as Author; const anonAuthor = Author.create(123) as Author;
const anonAuthor2 = Author.create(123) as Author; const anonAuthor2 = Author.create(123) as Author;
const edits = [Edit.create(author, 12, 15) as Edit]; const edits = [RangeAuthorship.create(author, 12, 15) as RangeAuthorship];
edits.push(Edit.create(author, 16, 18) as Edit); edits.push(RangeAuthorship.create(author, 16, 18) as RangeAuthorship);
edits.push(Edit.create(author, 29, 20) as Edit); edits.push(RangeAuthorship.create(author, 29, 20) as RangeAuthorship);
edits.push(Edit.create(anonAuthor, 29, 20) as Edit); edits.push(RangeAuthorship.create(anonAuthor, 29, 20) as RangeAuthorship);
edits.push(Edit.create(anonAuthor, 29, 20) as Edit); edits.push(RangeAuthorship.create(anonAuthor, 29, 20) as RangeAuthorship);
edits.push(Edit.create(anonAuthor2, 29, 20) as Edit); edits.push(
RangeAuthorship.create(anonAuthor2, 29, 20) as RangeAuthorship,
);
const revision = Mock.of<Revision>({}); const revision = Mock.of<Revision>({});
revision.edits = Promise.resolve(edits); revision.edits = Promise.resolve(edits);
@ -210,7 +212,7 @@ describe('RevisionsService', () => {
description: 'mockDescription', description: 'mockDescription',
patch: 'mockPatch', patch: 'mockPatch',
edits: Promise.resolve([ edits: Promise.resolve([
Mock.of<Edit>({ Mock.of<RangeAuthorship>({
endPos: 93, endPos: 93,
startPos: 34, startPos: 34,
createdAt: new Date('2020-03-04T20:12:00.000Z'), createdAt: new Date('2020-03-04T20:12:00.000Z'),
@ -259,7 +261,7 @@ describe('RevisionsService', () => {
description: 'mockDescription', description: 'mockDescription',
patch: 'mockPatch', patch: 'mockPatch',
edits: Promise.resolve([ edits: Promise.resolve([
Mock.of<Edit>({ Mock.of<RangeAuthorship>({
endPos: 93, endPos: 93,
startPos: 34, startPos: 34,
createdAt: new Date('2020-03-04T22:32:00.000Z'), createdAt: new Date('2020-03-04T22:32:00.000Z'),

View file

@ -1,8 +1,9 @@
/* /*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { AbsolutePositionAuthorship } from '@hedgedoc/commons';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { createPatch } from 'diff'; import { createPatch } from 'diff';
@ -12,7 +13,7 @@ import { NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service'; import { ConsoleLoggerService } from '../logger/console-logger.service';
import { Note } from '../notes/note.entity'; import { Note } from '../notes/note.entity';
import { Tag } from '../notes/tag.entity'; import { Tag } from '../notes/tag.entity';
import { EditService } from './edit.service'; import { RangeAuthorshipService } from './range-authorship.service';
import { RevisionMetadataDto } from './revision-metadata.dto'; import { RevisionMetadataDto } from './revision-metadata.dto';
import { RevisionDto } from './revision.dto'; import { RevisionDto } from './revision.dto';
import { Revision } from './revision.entity'; import { Revision } from './revision.entity';
@ -29,7 +30,7 @@ export class RevisionsService {
private readonly logger: ConsoleLoggerService, private readonly logger: ConsoleLoggerService,
@InjectRepository(Revision) @InjectRepository(Revision)
private revisionRepository: Repository<Revision>, private revisionRepository: Repository<Revision>,
private editService: EditService, private rangeAuthorshipService: RangeAuthorshipService,
) { ) {
this.logger.setContext(RevisionsService.name); this.logger.setContext(RevisionsService.name);
} }
@ -97,7 +98,9 @@ export class RevisionsService {
async getRevisionUserInfo(revision: Revision): Promise<RevisionUserInfo> { async getRevisionUserInfo(revision: Revision): Promise<RevisionUserInfo> {
// get a deduplicated list of all authors // get a deduplicated list of all authors
let authors = await Promise.all( let authors = await Promise.all(
(await revision.edits).map(async (edit) => await edit.author), (await revision.rangeAuthorships).map(
async (rangeAuthorship) => await rangeAuthorship.author,
),
); );
authors = [...new Set(authors)]; // remove duplicates with Set authors = [...new Set(authors)]; // remove duplicates with Set
@ -142,9 +145,12 @@ export class RevisionsService {
authorUsernames: revisionUserInfo.usernames, authorUsernames: revisionUserInfo.usernames,
anonymousAuthorCount: revisionUserInfo.anonymousUserCount, anonymousAuthorCount: revisionUserInfo.anonymousUserCount,
patch: revision.patch, patch: revision.patch,
edits: await Promise.all( rangeAuthorships: await Promise.all(
(await revision.edits).map( (await revision.rangeAuthorships).map(
async (edit) => await this.editService.toEditDto(edit), async (rangeAuthorship) =>
await this.rangeAuthorshipService.toRangeAuthorshipDto(
rangeAuthorship,
),
), ),
), ),
}; };
@ -158,12 +164,14 @@ export class RevisionsService {
* @param note The note for which the revision should be created * @param note The note for which the revision should be created
* @param newContent The new note content * @param newContent The new note content
* @param yjsStateVector The yjs state vector that describes the new content * @param yjsStateVector The yjs state vector that describes the new content
* @param absolutePositionAuthorships The absolute positions for authorship marks
* @return {Revision} the created revision * @return {Revision} the created revision
* @return {undefined} if the revision couldn't be created because e.g. the content hasn't changed * @return {undefined} if the revision couldn't be created because e.g. the content hasn't changed
*/ */
async createRevision( async createRevision(
note: Note, note: Note,
newContent: string, newContent: string,
absolutePositionAuthorships: AbsolutePositionAuthorship[],
yjsStateVector?: number[], yjsStateVector?: number[],
): Promise<Revision | undefined> { ): Promise<Revision | undefined> {
const latestRevision = const latestRevision =
@ -186,6 +194,12 @@ export class RevisionsService {
return entity; return entity;
}); });
const rangeAuthorships =
this.rangeAuthorshipService.createRangeAuthorshipsFromAbsolutePositions(
absolutePositionAuthorships,
newContent.length,
);
return Revision.create( return Revision.create(
newContent, newContent,
patch, patch,
@ -194,6 +208,7 @@ export class RevisionsService {
title, title,
description, description,
tagEntities, tagEntities,
rangeAuthorships,
) as Revision; ) as Revision;
} }
@ -203,16 +218,19 @@ export class RevisionsService {
* @async * @async
* @param note The note for which the revision should be created * @param note The note for which the revision should be created
* @param newContent The new note content * @param newContent The new note content
* @param absolutePositionAuthorships The absolute positions for authorship marks
* @param yjsStateVector The yjs state vector that describes the new content * @param yjsStateVector The yjs state vector that describes the new content
*/ */
async createAndSaveRevision( async createAndSaveRevision(
note: Note, note: Note,
newContent: string, newContent: string,
absolutePositionAuthorships: AbsolutePositionAuthorship[],
yjsStateVector?: number[], yjsStateVector?: number[],
): Promise<void> { ): Promise<void> {
const revision = await this.createRevision( const revision = await this.createRevision(
note, note,
newContent, newContent,
absolutePositionAuthorships,
yjsStateVector, yjsStateVector,
); );
if (revision) { if (revision) {

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -17,7 +17,7 @@ import { Note } from './notes/note.entity';
import { Tag } from './notes/tag.entity'; import { Tag } from './notes/tag.entity';
import { NoteGroupPermission } from './permissions/note-group-permission.entity'; import { NoteGroupPermission } from './permissions/note-group-permission.entity';
import { NoteUserPermission } from './permissions/note-user-permission.entity'; import { NoteUserPermission } from './permissions/note-user-permission.entity';
import { Edit } from './revisions/edit.entity'; import { RangeAuthorship } from './revisions/range-authorship.entity';
import { Revision } from './revisions/revision.entity'; import { Revision } from './revisions/revision.entity';
import { Session } from './sessions/session.entity'; import { Session } from './sessions/session.entity';
import { User } from './users/user.entity'; import { User } from './users/user.entity';
@ -33,7 +33,7 @@ const dataSource = new DataSource({
User, User,
Note, Note,
Revision, Revision,
Edit, RangeAuthorship,
NoteGroupPermission, NoteGroupPermission,
NoteUserPermission, NoteUserPermission,
Group, Group,
@ -81,9 +81,14 @@ dataSource
'Test note', 'Test note',
'', '',
[], [],
[],
) as Revision; ) as Revision;
const edit = Edit.create(author, 1, 42) as Edit; const rangeAuthorship = RangeAuthorship.create(
revision.edits = Promise.resolve([edit]); author,
1,
42,
) as RangeAuthorship;
revision.rangeAuthorships = Promise.resolve([rangeAuthorship]);
notes[i].revisions = Promise.all([revision]); notes[i].revisions = Promise.all([revision]);
notes[i].userPermissions = Promise.resolve([]); notes[i].userPermissions = Promise.resolve([]);
notes[i].groupPermissions = Promise.resolve([]); notes[i].groupPermissions = Promise.resolve([]);
@ -92,7 +97,7 @@ dataSource
notes[i], notes[i],
user, user,
revision, revision,
edit, rangeAuthorship,
author, author,
identity, identity,
]); ]);

View file

@ -1,9 +1,13 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { RealtimeUser, RemoteCursor } from './realtime-user.js' import {
RealtimeUser,
RemoteCursor,
ShortRealtimeUser
} from './realtime-user.js'
export enum MessageType { export enum MessageType {
NOTE_CONTENT_STATE_REQUEST = 'NOTE_CONTENT_STATE_REQUEST', NOTE_CONTENT_STATE_REQUEST = 'NOTE_CONTENT_STATE_REQUEST',
@ -33,10 +37,7 @@ export interface MessagePayloads {
[MessageType.NOTE_CONTENT_UPDATE]: number[] [MessageType.NOTE_CONTENT_UPDATE]: number[]
[MessageType.REALTIME_USER_STATE_SET]: { [MessageType.REALTIME_USER_STATE_SET]: {
users: RealtimeUser[] users: RealtimeUser[]
ownUser: { ownUser: ShortRealtimeUser
displayName: string
styleIndex: number
}
} }
[MessageType.REALTIME_USER_SINGLE_UPDATE]: RemoteCursor [MessageType.REALTIME_USER_SINGLE_UPDATE]: RemoteCursor

View file

@ -1,14 +1,11 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
export interface RealtimeUser { export interface RealtimeUser extends ShortRealtimeUser {
displayName: string
username: string | null
active: boolean active: boolean
styleIndex: number
cursor: RemoteCursor | null cursor: RemoteCursor | null
} }
@ -16,3 +13,9 @@ export interface RemoteCursor {
from: number from: number
to?: number to?: number
} }
export interface ShortRealtimeUser {
displayName: string
styleIndex: number
username: string | null
}

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -7,3 +7,4 @@ export * from './y-doc-sync-client-adapter.js'
export * from './y-doc-sync-server-adapter.js' export * from './y-doc-sync-server-adapter.js'
export * from './y-doc-sync-adapter.js' export * from './y-doc-sync-adapter.js'
export * from './realtime-doc.js' export * from './realtime-doc.js'
export * from './position-authorship.js'

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ShortRealtimeUser } from '../message-transporters/index.js'
import { AbsolutePosition, RelativePosition } from 'yjs'
export type RelativePositionAuthorship = [RelativePosition, ShortRealtimeUser]
export type OptionalAbsolutePositionAuthorship = [
AbsolutePosition | null,
ShortRealtimeUser
]
export type AbsolutePositionAuthorship = [number, ShortRealtimeUser]

View file

@ -1,5 +1,5 @@
/* /*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
@ -10,10 +10,21 @@ import {
Doc, Doc,
encodeStateAsUpdate, encodeStateAsUpdate,
encodeStateVector, encodeStateVector,
Text as YText Text as YText,
Array as YArray,
createAbsolutePositionFromRelativePosition,
createRelativePositionFromTypeIndex,
AbsolutePosition
} from 'yjs' } from 'yjs'
import {
AbsolutePositionAuthorship,
OptionalAbsolutePositionAuthorship,
RelativePositionAuthorship
} from './position-authorship.js'
import { RealtimeUser } from '../message-transporters/index.js'
const MARKDOWN_CONTENT_CHANNEL_NAME = 'markdownContent' const MARKDOWN_CONTENT_CHANNEL_NAME = 'markdownContent'
const RELATIVE_POSITION_AUTHORSHIPS_CHANNEL_NAME = 'relativePositionAuthorships'
export interface RealtimeDocEvents extends EventMap { export interface RealtimeDocEvents extends EventMap {
update: (update: number[], origin: unknown) => void update: (update: number[], origin: unknown) => void
@ -36,15 +47,35 @@ export class RealtimeDoc extends EventEmitter2<RealtimeDocEvents> {
* *
* @param initialTextContent the initial text content of the {@link Doc YDoc} * @param initialTextContent the initial text content of the {@link Doc YDoc}
* @param initialYjsState the initial yjs state. If provided this will be used instead of the text content * @param initialYjsState the initial yjs state. If provided this will be used instead of the text content
* @param initialAbsolutePositionAuthorships the initial realtime range authorships
*/ */
constructor(initialTextContent?: string, initialYjsState?: number[]) { constructor(
initialTextContent?: string,
initialYjsState?: number[],
initialAbsolutePositionAuthorships?: AbsolutePositionAuthorship[]
) {
super() super()
if (initialYjsState) { if (initialYjsState) {
this.applyUpdate(initialYjsState, this) this.applyUpdate(initialYjsState, this)
} else if (initialTextContent) { } else {
if (initialTextContent) {
this.getMarkdownContentChannel().insert(0, initialTextContent) this.getMarkdownContentChannel().insert(0, initialTextContent)
} }
if (initialAbsolutePositionAuthorships) {
this.getRelativePositionAuthorshipsChannel().insert(
0,
initialAbsolutePositionAuthorships.map(([index, user]) => [
createRelativePositionFromTypeIndex(
this.getMarkdownContentChannel(),
index
),
user
])
)
}
}
this.docUpdateListener = (update, origin) => { this.docUpdateListener = (update, origin) => {
this.emit('update', Array.from(update), origin) this.emit('update', Array.from(update), origin)
} }
@ -60,6 +91,15 @@ export class RealtimeDoc extends EventEmitter2<RealtimeDocEvents> {
return this.doc.getText(MARKDOWN_CONTENT_CHANNEL_NAME) return this.doc.getText(MARKDOWN_CONTENT_CHANNEL_NAME)
} }
/**
* Extracts the {@link YMap map channel} that contains the realtime range authorships.
*
* @return The realtime range authorships channel
*/
public getRelativePositionAuthorshipsChannel(): YArray<RelativePositionAuthorship> {
return this.doc.getArray(RELATIVE_POSITION_AUTHORSHIPS_CHANNEL_NAME)
}
/** /**
* Gets the current content of the note as it's currently edited in realtime. * Gets the current content of the note as it's currently edited in realtime.
* *
@ -72,6 +112,25 @@ export class RealtimeDoc extends EventEmitter2<RealtimeDocEvents> {
return this.getMarkdownContentChannel().toString() return this.getMarkdownContentChannel().toString()
} }
/**
* Gets the current authorship positions for the realtime.
*
* Please be aware that the return of this method may be very quickly outdated.
*
* @return An array of .
*/
public getAbsolutePositionAuthorships(): AbsolutePositionAuthorship[] {
return this.getRelativePositionAuthorshipsChannel()
.map<OptionalAbsolutePositionAuthorship>(([relativePosition, user]) => [
createAbsolutePositionFromRelativePosition(relativePosition, this.doc),
user
])
.filter(
<T, S>(value: [T | null, S]): value is [T, S] => value[0] !== null
)
.map(([absolutePosition, user]) => [absolutePosition.index, user])
}
/** /**
* Encodes the current state of the doc as update so it can be applied to other y-docs. * Encodes the current state of the doc as update so it can be applied to other y-docs.
* *