feat: Add chat embeds v1

Youtube and Website metadata embeds
This commit is contained in:
2026-04-04 04:47:04 +02:00
parent 35352923a5
commit 84fa45985a
25 changed files with 759 additions and 24 deletions

View File

@@ -9,7 +9,11 @@ import {
emptyProps,
props
} from '@ngrx/store';
import { Message, Reaction } from '../../shared-kernel';
import {
Message,
Reaction,
LinkMetadata
} from '../../shared-kernel';
export const MessagesActions = createActionGroup({
source: 'Messages',
@@ -49,6 +53,12 @@ export const MessagesActions = createActionGroup({
/** Marks the end of a message sync cycle. */
'Sync Complete': emptyProps(),
/** Attaches fetched link metadata to a message. */
'Update Link Metadata': props<{ messageId: string; linkMetadata: LinkMetadata[] }>(),
/** Removes a single link embed from a message by URL. */
'Remove Link Embed': props<{ messageId: string; url: string }>(),
/** Removes all messages from the store (e.g. when leaving a room). */
'Clear Messages': emptyProps()
}

View File

@@ -31,11 +31,13 @@ import { v4 as uuidv4 } from 'uuid';
import { MessagesActions } from './messages.actions';
import { selectCurrentUser } from '../users/users.selectors';
import { selectCurrentRoom } from '../rooms/rooms.selectors';
import { selectMessagesEntities } from './messages.selectors';
import { RealtimeSessionFacade } from '../../core/realtime';
import { DatabaseService } from '../../infrastructure/persistence';
import { reportDebuggingError, trackDebuggingTaskFailure } from '../../core/helpers/debugging-helpers';
import { DebuggingService } from '../../core/services';
import { AttachmentFacade } from '../../domains/attachment';
import { LinkMetadataService } from '../../domains/chat/application/link-metadata.service';
import { TimeSyncService } from '../../core/services/time-sync.service';
import {
DELETED_MESSAGE_CONTENT,
@@ -56,6 +58,7 @@ export class MessagesEffects {
private readonly attachments = inject(AttachmentFacade);
private readonly webrtc = inject(RealtimeSessionFacade);
private readonly timeSync = inject(TimeSyncService);
private readonly linkMetadata = inject(LinkMetadataService);
/** Loads messages for a room from the local database, hydrating reactions. */
loadMessages$ = createEffect(() =>
@@ -374,6 +377,76 @@ export class MessagesEffects {
)
);
/**
* Fetches link metadata for newly sent or received messages that
* contain URLs but don't already have metadata attached.
*/
fetchLinkMetadata$ = createEffect(() =>
this.actions$.pipe(
ofType(MessagesActions.sendMessageSuccess, MessagesActions.receiveMessage),
mergeMap(({ message }) => {
if (message.isDeleted || message.linkMetadata?.length)
return EMPTY;
const urls = this.linkMetadata.extractUrls(message.content);
if (urls.length === 0)
return EMPTY;
return from(this.linkMetadata.fetchAllMetadata(urls)).pipe(
mergeMap((metadata) => {
const meaningful = metadata.filter((md) => !md.failed);
if (meaningful.length === 0)
return EMPTY;
this.trackBackgroundOperation(
this.db.updateMessage(message.id, { linkMetadata: meaningful }),
'Failed to persist link metadata',
{ messageId: message.id }
);
return of(MessagesActions.updateLinkMetadata({
messageId: message.id,
linkMetadata: meaningful
}));
}),
catchError(() => EMPTY)
);
})
)
);
/**
* Removes a single link embed from a message, persists the change,
* and updates the store.
*/
removeLinkEmbed$ = createEffect(() =>
this.actions$.pipe(
ofType(MessagesActions.removeLinkEmbed),
withLatestFrom(this.store.select(selectMessagesEntities)),
mergeMap(([{ messageId, url }, entities]) => {
const message = entities[messageId];
if (!message?.linkMetadata)
return EMPTY;
const remaining = message.linkMetadata.filter((meta) => meta.url !== url);
this.trackBackgroundOperation(
this.db.updateMessage(messageId, { linkMetadata: remaining.length ? remaining : undefined }),
'Failed to persist link embed removal',
{ messageId }
);
return of(MessagesActions.updateLinkMetadata({
messageId,
linkMetadata: remaining
}));
})
)
);
/**
* Central dispatcher for all incoming P2P messages.
* Delegates to handler functions in `messages-incoming.handlers.ts`.

View File

@@ -206,6 +206,17 @@ export const messagesReducer = createReducer(
});
}),
// Update link metadata on a message
on(MessagesActions.updateLinkMetadata, (state, { messageId, linkMetadata }) =>
messagesAdapter.updateOne(
{
id: messageId,
changes: { linkMetadata }
},
state
)
),
// Clear messages
on(MessagesActions.clearMessages, (state) =>
messagesAdapter.removeAll({