feat: Add chat embeds v1
Youtube and Website metadata embeds
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user