Skip to content

Commit e606e2d

Browse files
authored
Utilize EventStatus for message state (#1170)
1 parent 2ff1697 commit e606e2d

File tree

14 files changed

+368
-152
lines changed

14 files changed

+368
-152
lines changed

packages/base/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"ember-css-url": "^1.0.0",
1818
"ember-modifier": "^3.2.1",
1919
"ethers": "^6.6.2",
20+
"matrix-js-sdk": "^31.0.0",
2021
"super-fast-md5": "^1.0.1",
2122
"tracked-built-ins": "^2.0.1"
2223
},

packages/base/room.gts

+22-6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { cached } from '@glimmer/tracking';
2323
import { initSharedState } from './shared-state';
2424
import BooleanField from './boolean';
2525
import { md5 } from 'super-fast-md5';
26+
import { EventStatus } from 'matrix-js-sdk';
2627

2728
// this is so we can have triple equals equivalent room member cards
2829
function upsertRoomMember({
@@ -135,7 +136,7 @@ class RoomMembershipField extends FieldDef {
135136
};
136137
}
137138

138-
type CardArgs = {
139+
type MessageFieldArgs = {
139140
author: RoomMemberField;
140141
created: Date;
141142
updated: Date;
@@ -147,6 +148,7 @@ type CardArgs = {
147148
command: string | null;
148149
isStreamingFinished?: boolean;
149150
clientGeneratedId?: string | null;
151+
status: EventStatus | null;
150152
};
151153

152154
type AttachedCardResource = {
@@ -277,6 +279,7 @@ export class MessageField extends FieldDef {
277279
// ID from the client and can be used by client
278280
// to verify whether the message is already sent or not.
279281
@field clientGeneratedId = contains(StringField);
282+
@field status = contains(StringField);
280283

281284
static embedded = EmbeddedMessageField;
282285
// The edit template is meant to be read-only, this field card is not mutable
@@ -498,7 +501,7 @@ export class RoomField extends FieldDef {
498501
}
499502

500503
let author = upsertRoomMember({ room: this, userId: event.sender });
501-
let cardArgs: CardArgs = {
504+
let cardArgs: MessageFieldArgs = {
502505
author,
503506
created: new Date(event.origin_server_ts),
504507
updated: new Date(), // Changes every time an update from AI bot streaming is received, used for detecting timeouts
@@ -509,8 +512,13 @@ export class RoomField extends FieldDef {
509512
transactionId: event.unsigned?.transaction_id || null,
510513
attachedCard: null,
511514
command: null,
515+
status: event.status,
512516
};
513517

518+
if (event.status === 'cancelled' || event.status === 'not_sent') {
519+
(cardArgs as any).errorMessage = 'Failed to send';
520+
}
521+
514522
if ('errorMessage' in event.content) {
515523
(cardArgs as any).errorMessage = event.content.errorMessage;
516524
}
@@ -573,14 +581,21 @@ export class RoomField extends FieldDef {
573581
}
574582

575583
if (messageField) {
576-
newMessages.set(event_id, messageField);
584+
newMessages.set(
585+
(event.content as CardMessageContent).clientGeneratedId ?? event_id,
586+
messageField,
587+
);
577588
index++;
578589
}
579590
}
580591

581-
// upodate the cache with the new messages
582-
for (let [eventId, message] of newMessages) {
583-
cache.set(eventId, message);
592+
// update the cache with the new messages
593+
for (let [id, message] of newMessages) {
594+
// The `id` can either be an `eventId` or `clientGeneratedId`.
595+
// For messages sent by the user, we prefer to use `clientGeneratedId`
596+
// because `eventId` can change in certain scenarios,
597+
// such as when resending a failed message or updating its status from sending to sent.
598+
cache.set(id, message);
584599
}
585600

586601
// this sort should hopefully be very optimized since events will
@@ -656,6 +671,7 @@ interface BaseMatrixEvent {
656671
prev_content?: any;
657672
prev_sender?: string;
658673
};
674+
status: EventStatus | null;
659675
}
660676

661677
interface RoomStateEvent extends BaseMatrixEvent {

packages/host/app/components/ai-assistant/message/index.gts

+8
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default class AiAssistantMessage extends Component<Signature> {
5353
'ai-assistant-message'
5454
is-from-assistant=@isFromAssistant
5555
is-pending=@isPending
56+
is-error=@errorMessage
5657
}}
5758
{{ScrollIntoView}}
5859
data-test-ai-assistant-message
@@ -209,6 +210,13 @@ export default class AiAssistantMessage extends Component<Signature> {
209210
color: var(--boxel-500);
210211
}
211212
213+
.is-error .content,
214+
.is-error .content .cards > :deep(.card-pill),
215+
.is-error .content .cards > :deep(.card-pill .boxel-card-container) {
216+
background: var(--boxel-200);
217+
color: var(--boxel-500);
218+
}
219+
212220
.content > :deep(.patch-message) {
213221
font-weight: 700;
214222
letter-spacing: var(--boxel-lsp-sm);

packages/host/app/components/matrix/room-message.gts

+5
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ export default class RoomMessage extends Component<Signature> {
158158
.room-message {
159159
--ai-assistant-message-padding: var(--boxel-sp);
160160
}
161+
.is-pending .view-code-button,
162+
.is-error .view-code-button {
163+
background: var(--boxel-200);
164+
color: var(--boxel-500);
165+
}
161166
.patch-button-bar {
162167
display: flex;
163168
justify-content: flex-end;

packages/host/app/components/matrix/room.gts

+24-24
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { tracked } from '@glimmer/tracking';
66

77
import { enqueueTask, restartableTask, timeout, all } from 'ember-concurrency';
88

9+
import { v4 as uuidv4 } from 'uuid';
10+
911
import { getAutoAttachment } from '@cardstack/host/resources/auto-attached-card';
1012
import { getRoom } from '@cardstack/host/resources/room';
1113

@@ -41,12 +43,13 @@ export default class Room extends Component<Signature> {
4143
data-test-room-name={{this.room.name}}
4244
data-test-room={{this.room.roomId}}
4345
>
44-
{{#if this.hasMessagesOrPending}}
46+
{{#if this.room.messages}}
4547
<AiAssistantConversation>
4648
{{#each this.room.messages as |message i|}}
4749
<RoomMessage
4850
@message={{message}}
4951
@index={{i}}
52+
@isPending={{this.isPendingMessage message}}
5053
@monacoSDK={{@monacoSDK}}
5154
@isStreaming={{this.isMessageStreaming message i}}
5255
@currentEditor={{this.currentMonacoContainer}}
@@ -55,17 +58,6 @@ export default class Room extends Component<Signature> {
5558
data-test-message-idx={{i}}
5659
/>
5760
{{/each}}
58-
{{#if this.pendingMessage}}
59-
<RoomMessage
60-
@message={{this.pendingMessage}}
61-
@monacoSDK={{@monacoSDK}}
62-
@isStreaming={{false}}
63-
@isPending={{true}}
64-
@currentEditor={{this.currentMonacoContainer}}
65-
@setCurrentEditor={{this.setCurrentMonacoContainer}}
66-
data-test-message-idx={{this.room.messages.length}}
67-
/>
68-
{{/if}}
6961
</AiAssistantConversation>
7062
{{else}}
7163
<NewSession @sendPrompt={{this.sendPrompt}} />
@@ -157,12 +149,9 @@ export default class Room extends Component<Signature> {
157149
await this.roomResource.loading;
158150
});
159151

160-
private get hasMessagesOrPending() {
161-
return (this.room && this.room.messages.length > 0) || this.pendingMessage;
162-
}
163-
164152
private get room() {
165-
return this.roomResource.room;
153+
let room = this.roomResource.room;
154+
return room;
166155
}
167156

168157
private doWhenRoomChanges = restartableTask(async () => {
@@ -177,10 +166,6 @@ export default class Room extends Component<Signature> {
177166
return this.matrixService.cardsToSend.get(this.args.roomId);
178167
}
179168

180-
private get pendingMessage() {
181-
return this.matrixService.pendingMessages.get(this.args.roomId);
182-
}
183-
184169
@action resendLastMessage() {
185170
if (!this.room) {
186171
throw new Error(
@@ -202,7 +187,11 @@ export default class Room extends Component<Signature> {
202187
.map((resource) => resource.card)
203188
.filter((card) => card !== undefined) as CardDef[];
204189

205-
this.doSendMessage.perform(myLastMessage.message, attachedCards);
190+
this.doSendMessage.perform(
191+
myLastMessage.message,
192+
attachedCards,
193+
myLastMessage.clientGeneratedId,
194+
);
206195
}
207196

208197
@action sendPrompt(prompt: string) {
@@ -253,7 +242,11 @@ export default class Room extends Component<Signature> {
253242
}
254243
}
255244
private doSendMessage = enqueueTask(
256-
async (message: string | undefined, cards?: CardDef[]) => {
245+
async (
246+
message: string | undefined,
247+
cards?: CardDef[],
248+
clientGeneratedId: string = uuidv4(),
249+
) => {
257250
this.matrixService.messagesToSend.set(this.args.roomId, undefined);
258251
this.matrixService.cardsToSend.set(this.args.roomId, undefined);
259252
let context = {
@@ -267,6 +260,7 @@ export default class Room extends Component<Signature> {
267260
this.args.roomId,
268261
message,
269262
cards,
263+
clientGeneratedId,
270264
context,
271265
);
272266
},
@@ -306,7 +300,9 @@ export default class Room extends Component<Signature> {
306300
this.messageToSend ||
307301
this.cardsToAttach?.length ||
308302
this.autoAttachedCard,
309-
)
303+
) &&
304+
!!this.room &&
305+
!this.room.messages.some((m) => this.isPendingMessage(m))
310306
);
311307
}
312308

@@ -320,6 +316,10 @@ export default class Room extends Component<Signature> {
320316
@action private setCurrentMonacoContainer(index: number | undefined) {
321317
this.currentMonacoContainer = index;
322318
}
319+
320+
private isPendingMessage(message: MessageField) {
321+
return message.status === 'sending' || message.status === 'queued';
322+
}
323323
}
324324

325325
declare module '@glint/environment-ember-loose/registry' {

packages/host/app/lib/externals.ts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as emberResources from 'ember-resources';
2020
import * as ethers from 'ethers';
2121
import * as flat from 'flat';
2222
import * as lodash from 'lodash';
23+
import * as matrixJsSDK from 'matrix-js-sdk';
2324
import * as superFastMD5 from 'super-fast-md5';
2425
import * as tracked from 'tracked-built-ins';
2526

@@ -70,4 +71,5 @@ export function shimExternals(virtualNetwork: VirtualNetwork) {
7071
default: class {},
7172
});
7273
virtualNetwork.shimModule('super-fast-md5', superFastMD5);
74+
virtualNetwork.shimModule('matrix-js-sdk', matrixJsSDK);
7375
}

packages/host/app/lib/matrix-handlers/index.ts

+38-15
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ import { type LooseCardResource, baseRealm } from '@cardstack/runtime-common';
99

1010
import type * as CardAPI from 'https://cardstack.com/base/card-api';
1111
import type {
12-
CardMessageContent,
1312
RoomField,
1413
MatrixEvent as DiscreteMatrixEvent,
15-
MessageField,
1614
} from 'https://cardstack.com/base/room';
1715

1816
import type LoaderService from '../../services/loader-service';
@@ -35,11 +33,10 @@ export interface RoomMeta {
3533
name?: string;
3634
}
3735

38-
export type Event = Partial<IEvent>;
36+
export type Event = Partial<IEvent> & { status: MatrixSDK.EventStatus | null };
3937

4038
export interface EventSendingContext {
4139
rooms: Map<string, Promise<RoomField>>;
42-
pendingMessages: Map<string, MessageField | undefined>;
4340
cardAPI: typeof CardAPI;
4441
loaderService: LoaderService;
4542
}
@@ -48,7 +45,7 @@ export interface Context extends EventSendingContext {
4845
flushTimeline: Promise<void> | undefined;
4946
flushMembership: Promise<void> | undefined;
5047
roomMembershipQueue: { event: MatrixEvent; member: RoomMember }[];
51-
timelineQueue: MatrixEvent[];
48+
timelineQueue: { event: MatrixEvent; oldEventId?: string }[];
5249
client: MatrixClient;
5350
matrixSDK: typeof MatrixSDK;
5451
handleMessage?: (
@@ -105,16 +102,42 @@ export async function addRoomEvent(context: EventSendingContext, event: Event) {
105102
...(resolvedRoom.events ?? []),
106103
event as unknown as DiscreteMatrixEvent,
107104
];
105+
}
106+
}
108107

109-
let pendingMessage = context.pendingMessages.get(resolvedRoom.roomId);
110-
if (
111-
pendingMessage &&
112-
event.type === 'm.room.message' &&
113-
event.content?.msgtype === 'org.boxel.message' &&
114-
(event.content as CardMessageContent).clientGeneratedId ===
115-
pendingMessage.clientGeneratedId
116-
) {
117-
context.pendingMessages.set(resolvedRoom.roomId, undefined);
118-
}
108+
export async function updateRoomEvent(
109+
context: EventSendingContext,
110+
event: Event,
111+
oldEventId: string,
112+
) {
113+
if (event.content?.data && typeof event.content.data === 'string') {
114+
event.content.data = JSON.parse(event.content.data);
115+
}
116+
let { event_id: eventId, room_id: roomId, state_key: stateKey } = event;
117+
eventId = eventId ?? stateKey; // room state may not necessary have an event ID
118+
if (!eventId) {
119+
throw new Error(
120+
`bug: event ID is undefined for event ${JSON.stringify(event, null, 2)}`,
121+
);
122+
}
123+
if (!roomId) {
124+
throw new Error(
125+
`bug: roomId is undefined for event ${JSON.stringify(event, null, 2)}`,
126+
);
127+
}
128+
let room = context.rooms.get(roomId);
129+
if (!room) {
130+
throw new Error(
131+
`bug: unknown room for event ${JSON.stringify(event, null, 2)}`,
132+
);
133+
}
134+
let resolvedRoom = await room;
135+
let oldEventIndex = resolvedRoom.events.findIndex(
136+
(e) => e.event_id === oldEventId,
137+
);
138+
if (oldEventIndex >= 0) {
139+
resolvedRoom.events[oldEventIndex] =
140+
event as unknown as DiscreteMatrixEvent;
141+
resolvedRoom.events = [...resolvedRoom.events];
119142
}
120143
}

packages/host/app/lib/matrix-handlers/membership.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ async function drainMembership(context: Context) {
3030
context.roomMembershipQueue = [];
3131

3232
await Promise.all(
33-
events.map(({ event: { event } }) => addRoomEvent(context, event)),
33+
events.map(({ event: { event, status } }) =>
34+
addRoomEvent(context, { ...event, status }),
35+
),
3436
);
3537

3638
// For rooms that we have been invited to we are unable to get the full
@@ -78,6 +80,7 @@ async function drainMembership(context: Context) {
7880
...e.event,
7981
// annoyingly these events have been stripped of their id's
8082
event_id: `${roomId}_${eventType}_${e.localTimestamp}`,
83+
status: e.status,
8184
}))
8285
.map((event) => addRoomEvent(context, event)),
8386
);

0 commit comments

Comments
 (0)