Skip to content

Commit

Permalink
Feature/Add message read indicator (#22)
Browse files Browse the repository at this point in the history
* feat: add message read indicator in transport

* feat: add MarkMessageAsRead/Unread Use Cases

* test: markMessageAs[Un]Read

* refactor: rename parameter wasReadAt

* feat: add wasReadAt to Message DTO and DVO

* chore: version bump

* fix: add granularity of CoreDate to test

* fix: allow same CoreDate in expect

* feat: return updated message

* test: MarkMessage use cases

* feat: add type to wasReadAt of MessageDVO

* feat: integrate comments

* feat: integrate comments

* refactor: add early returns

---------

Co-authored-by: Julian König <[email protected]>
  • Loading branch information
Milena-Czierlinski and jkoenig134 authored Feb 14, 2024
1 parent a0b7c20 commit f1d82b3
Show file tree
Hide file tree
Showing 18 changed files with 335 additions and 63 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nmshd/runtime",
"version": "4.0.0",
"version": "4.1.0",
"description": "The enmeshed client runtime.",
"homepage": "https://enmeshed.eu",
"repository": {
Expand Down Expand Up @@ -67,7 +67,7 @@
"@nmshd/consumption": "3.9.3",
"@nmshd/content": "2.8.4",
"@nmshd/crypto": "2.0.6",
"@nmshd/transport": "2.2.2",
"@nmshd/transport": "2.3.0",
"ajv": "^8.12.0",
"ajv-errors": "^3.0.0",
"ajv-formats": "^2.1.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/runtime/src/dataViews/DataViewExpander.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ export class DataViewExpander {
statusText: `i18n://dvo.message.${status}`,
image: "",
peer: peer,
content: message.content
content: message.content,
wasReadAt: message.wasReadAt
};

if (message.content["@type"] === "Mail" || message.content["@type"] === "RequestMail") {
Expand Down
5 changes: 5 additions & 0 deletions packages/runtime/src/dataViews/transport/MessageDVO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export interface MessageDVO extends DataViewObject {
* The content of the message.
*/
content: unknown;

/**
* The read indicator of the message
*/
wasReadAt?: string;
}

export interface RecipientDVO extends Omit<IdentityDVO, "type"> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ import {
GetMessagesRequest,
GetMessagesUseCase,
GetMessageUseCase,
MarkMessageAsReadRequest,
MarkMessageAsReadUseCase,
MarkMessageAsUnreadRequest,
MarkMessageAsUnreadUseCase,
SendMessageRequest,
SendMessageUseCase
} from "../../../useCases";

export class MessagesFacade {
public constructor(
@Inject private readonly getMessagesUseCase: GetMessagesUseCase,
@Inject private readonly getMessageUseCase: GetMessageUseCase,
@Inject private readonly sendMessageUseCase: SendMessageUseCase,
@Inject private readonly downloadAttachmentUseCase: DownloadAttachmentUseCase,
@Inject private readonly getAttachmentMetadataUseCase: GetAttachmentMetadataUseCase
@Inject private readonly getAttachmentMetadataUseCase: GetAttachmentMetadataUseCase,
@Inject private readonly getMessageUseCase: GetMessageUseCase,
@Inject private readonly getMessagesUseCase: GetMessagesUseCase,
@Inject private readonly markMessageAsReadUseCase: MarkMessageAsReadUseCase,
@Inject private readonly markMessageAsUnreadUseCase: MarkMessageAsUnreadUseCase,
@Inject private readonly sendMessageUseCase: SendMessageUseCase
) {}

public async sendMessage(request: SendMessageRequest): Promise<Result<MessageDTO>> {
Expand All @@ -43,4 +49,12 @@ export class MessagesFacade {
public async getAttachmentMetadata(request: GetAttachmentMetadataRequest): Promise<Result<FileDTO>> {
return await this.getAttachmentMetadataUseCase.execute(request);
}

public async markMessageAsRead(request: MarkMessageAsReadRequest): Promise<Result<MessageDTO>> {
return await this.markMessageAsReadUseCase.execute(request);
}

public async markMessageAsUnread(request: MarkMessageAsUnreadRequest): Promise<Result<MessageDTO>> {
return await this.markMessageAsUnreadUseCase.execute(request);
}
}
1 change: 1 addition & 0 deletions packages/runtime/src/types/transport/MessageDTO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export interface MessageDTO {
createdAt: string;
attachments: string[];
isOwn: boolean;
wasReadAt?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export interface MessageWithAttachmentsDTO {
createdAt: string;
attachments: FileDTO[];
isOwn: boolean;
wasReadAt?: string;
}
59 changes: 59 additions & 0 deletions packages/runtime/src/useCases/common/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20866,6 +20866,19 @@ export const GetMessagesRequest: any = {
}
]
},
"wasReadAt": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"participant": {
"anyOf": [
{
Expand All @@ -20885,6 +20898,52 @@ export const GetMessagesRequest: any = {
}
}

export const MarkMessageAsReadRequest: any = {
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/MarkMessageAsReadRequest",
"definitions": {
"MarkMessageAsReadRequest": {
"type": "object",
"properties": {
"id": {
"$ref": "#/definitions/MessageIdString"
}
},
"required": [
"id"
],
"additionalProperties": false
},
"MessageIdString": {
"type": "string",
"pattern": "MSG[A-Za-z0-9]{17}"
}
}
}

export const MarkMessageAsUnreadRequest: any = {
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/MarkMessageAsUnreadRequest",
"definitions": {
"MarkMessageAsUnreadRequest": {
"type": "object",
"properties": {
"id": {
"$ref": "#/definitions/MessageIdString"
}
},
"required": [
"id"
],
"additionalProperties": false
},
"MessageIdString": {
"type": "string",
"pattern": "MSG[A-Za-z0-9]{17}"
}
}
}

export const SendMessageRequest: any = {
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/SendMessageRequest",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface GetMessagesQuery {
attachments?: string | string[];
"recipients.address"?: string | string[];
"recipients.relationshipId"?: string | string[];
wasReadAt?: string | string[];
participant?: string | string[];
}

Expand All @@ -42,6 +43,7 @@ export class GetMessagesUseCase extends UseCase<GetMessagesRequest, MessageDTO[]
[nameof<MessageDTO>((m) => m.attachments)]: true,
[`${nameof<MessageDTO>((m) => m.recipients)}.${nameof<RecipientDTO>((r) => r.address)}`]: true,
[`${nameof<MessageDTO>((m) => m.recipients)}.${nameof<RecipientDTO>((r) => r.relationshipId)}`]: true,
[nameof<MessageDTO>((m) => m.wasReadAt)]: true,
participant: true
},

Expand All @@ -54,7 +56,8 @@ export class GetMessagesUseCase extends UseCase<GetMessagesRequest, MessageDTO[]
)}.${nameof<MessageEnvelopeRecipient>((r) => r.address)}`,
[`${nameof<MessageDTO>((m) => m.content)}.@type`]: `${nameof<Message>((m) => m.cache)}.${nameof<CachedMessage>((m) => m.content)}.@type`,
[`${nameof<MessageDTO>((m) => m.content)}.body`]: `${nameof<Message>((m) => m.cache)}.${nameof<CachedMessage>((m) => m.content)}.body`,
[`${nameof<MessageDTO>((m) => m.content)}.subject`]: `${nameof<Message>((m) => m.cache)}.${nameof<CachedMessage>((m) => m.content)}.subject`
[`${nameof<MessageDTO>((m) => m.content)}.subject`]: `${nameof<Message>((m) => m.cache)}.${nameof<CachedMessage>((m) => m.content)}.subject`,
[nameof<MessageDTO>((m) => m.wasReadAt)]: [nameof<Message>((m) => m.wasReadAt)]
},

custom: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Result } from "@js-soft/ts-utils";
import { AccountController, CoreId, MessageController } from "@nmshd/transport";
import { Inject } from "typescript-ioc";
import { MessageDTO } from "../../../types";
import { MessageIdString, SchemaRepository, SchemaValidator, UseCase } from "../../common";
import { MessageMapper } from "./MessageMapper";

export interface MarkMessageAsReadRequest {
id: MessageIdString;
}

class Validator extends SchemaValidator<MarkMessageAsReadRequest> {
public constructor(@Inject schemaRepository: SchemaRepository) {
super(schemaRepository.getSchema("MarkMessageAsReadRequest"));
}
}

export class MarkMessageAsReadUseCase extends UseCase<MarkMessageAsReadRequest, MessageDTO> {
public constructor(
@Inject private readonly messageController: MessageController,
@Inject private readonly accountController: AccountController,
@Inject validator: Validator
) {
super(validator);
}

protected async executeInternal(request: MarkMessageAsReadRequest): Promise<Result<MessageDTO>> {
const updatedMessage = await this.messageController.markMessageAsRead(CoreId.from(request.id));

await this.accountController.syncDatawallet();

return Result.ok(MessageMapper.toMessageDTO(updatedMessage));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Result } from "@js-soft/ts-utils";
import { AccountController, CoreId, MessageController } from "@nmshd/transport";
import { Inject } from "typescript-ioc";
import { MessageDTO } from "../../../types";
import { MessageIdString, SchemaRepository, SchemaValidator, UseCase } from "../../common";
import { MessageMapper } from "./MessageMapper";

export interface MarkMessageAsUnreadRequest {
id: MessageIdString;
}

class Validator extends SchemaValidator<MarkMessageAsUnreadRequest> {
public constructor(@Inject schemaRepository: SchemaRepository) {
super(schemaRepository.getSchema("MarkMessageAsUnreadRequest"));
}
}

export class MarkMessageAsUnreadUseCase extends UseCase<MarkMessageAsUnreadRequest, MessageDTO> {
public constructor(
@Inject private readonly messageController: MessageController,
@Inject private readonly accountController: AccountController,
@Inject validator: Validator
) {
super(validator);
}

protected async executeInternal(request: MarkMessageAsUnreadRequest): Promise<Result<MessageDTO>> {
const updatedMessage = await this.messageController.markMessageAsUnread(CoreId.from(request.id));

await this.accountController.syncDatawallet();

return Result.ok(MessageMapper.toMessageDTO(updatedMessage));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export class MessageMapper {
recipients: message.cache.recipients.map((r, i) => this.toRecipient(r, message.relationshipIds[i])),
createdAt: message.cache.createdAt.toString(),
attachments: attachments.map((f) => FileMapper.toFileDTO(f)),
isOwn: message.isOwn
isOwn: message.isOwn,
wasReadAt: message.wasReadAt?.toString()
};
}

Expand All @@ -48,7 +49,8 @@ export class MessageMapper {
recipients: message.cache.recipients.map((r, i) => this.toRecipient(r, message.relationshipIds[i])),
createdAt: message.cache.createdAt.toString(),
attachments: message.cache.attachments.map((a) => a.toString()),
isOwn: message.isOwn
isOwn: message.isOwn,
wasReadAt: message.wasReadAt?.toString()
};
}

Expand Down
2 changes: 2 additions & 0 deletions packages/runtime/src/useCases/transport/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ export * from "./DownloadAttachment";
export * from "./GetAttachmentMetadata";
export * from "./GetMessage";
export * from "./GetMessages";
export * from "./MarkMessageAsRead";
export * from "./MarkMessageAsUnread";
export * from "./MessageMapper";
export * from "./SendMessage";
Loading

0 comments on commit f1d82b3

Please sign in to comment.