feat(shangwutong): render inbound images inline
This commit is contained in:
@@ -10,6 +10,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { MESSAGE_VARIANTS, ORIENTATION } from '../constants';
|
||||
import { formatShangwutongInlineContent } from '../inlineMedia.js';
|
||||
|
||||
const props = defineProps({
|
||||
hideMeta: { type: Boolean, default: false },
|
||||
@@ -77,11 +78,19 @@ const shouldShowMeta = computed(
|
||||
);
|
||||
|
||||
const replyToPreview = computed(() => {
|
||||
if (!inReplyTo) return '';
|
||||
if (!inReplyTo?.value) return '';
|
||||
|
||||
const { content, attachments } = inReplyTo.value;
|
||||
const { content, attachments, contentAttributes, content_attributes } =
|
||||
inReplyTo.value;
|
||||
|
||||
if (content) return new MessageFormatter(content).formattedMessage;
|
||||
if (content) {
|
||||
const formattedContent = formatShangwutongInlineContent(
|
||||
content,
|
||||
attachments,
|
||||
contentAttributes ?? content_attributes
|
||||
);
|
||||
return new MessageFormatter(formattedContent).formattedMessage;
|
||||
}
|
||||
if (attachments?.length) {
|
||||
const firstAttachment = attachments[0];
|
||||
const fileType = firstAttachment.fileType ?? firstAttachment.file_type;
|
||||
|
||||
+10
-3
@@ -4,6 +4,7 @@ import { useMessageContext } from '../../provider.js';
|
||||
|
||||
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
|
||||
import { MESSAGE_VARIANTS } from '../../constants';
|
||||
import { formatShangwutongInlineContent } from '../../inlineMedia.js';
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
@@ -12,14 +13,20 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const { variant } = useMessageContext();
|
||||
const { variant, attachments, contentAttributes } = useMessageContext();
|
||||
|
||||
const formattedContent = computed(() => {
|
||||
const content = formatShangwutongInlineContent(
|
||||
props.content,
|
||||
attachments.value,
|
||||
contentAttributes.value
|
||||
);
|
||||
|
||||
if (variant.value === MESSAGE_VARIANTS.ACTIVITY) {
|
||||
return props.content;
|
||||
return content;
|
||||
}
|
||||
|
||||
return new MessageFormatter(props.content).formattedMessage;
|
||||
return new MessageFormatter(content).formattedMessage;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+10
-2
@@ -8,6 +8,7 @@ import FileChip from 'next/message/chips/File.vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
|
||||
import { ATTACHMENT_TYPES } from '../constants';
|
||||
import { shangwutongInlineMediaIndexes } from '../inlineMedia.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Attachment
|
||||
@@ -34,7 +35,7 @@ defineOptions({
|
||||
});
|
||||
|
||||
const attrs = useAttrs();
|
||||
const { orientation } = useMessageContext();
|
||||
const { orientation, contentAttributes } = useMessageContext();
|
||||
|
||||
const classToApply = computed(() => {
|
||||
const baseClasses = [attrs.class, 'flex', 'flex-wrap'];
|
||||
@@ -46,8 +47,15 @@ const classToApply = computed(() => {
|
||||
return baseClasses;
|
||||
});
|
||||
|
||||
const inlineAttachmentIndexes = computed(() =>
|
||||
shangwutongInlineMediaIndexes(contentAttributes.value)
|
||||
);
|
||||
|
||||
const allAttachments = computed(() => {
|
||||
return Array.isArray(props.attachments) ? props.attachments : [];
|
||||
const attachments = Array.isArray(props.attachments) ? props.attachments : [];
|
||||
return attachments.filter(
|
||||
(_, index) => !inlineAttachmentIndexes.value.has(index)
|
||||
);
|
||||
});
|
||||
|
||||
const mediaAttachments = computed(() => {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const INLINE_MEDIA_PATTERN = /!\[([^\]]*)\]\(swt-attachment:\/\/(\d+)\)/g;
|
||||
|
||||
export const shangwutongInlineMediaIndexes = contentAttributes => {
|
||||
const indexes = contentAttributes?.swt?.inline_media;
|
||||
if (!Array.isArray(indexes)) return new Set();
|
||||
|
||||
return new Set(
|
||||
indexes.map(Number).filter(index => Number.isInteger(index) && index >= 0)
|
||||
);
|
||||
};
|
||||
|
||||
const attachmentURL = attachment => attachment?.dataUrl ?? attachment?.data_url;
|
||||
|
||||
const resolveAttachmentURL = dataUrl => {
|
||||
if (!dataUrl) return '';
|
||||
|
||||
try {
|
||||
return new URL(
|
||||
dataUrl,
|
||||
typeof window === 'undefined'
|
||||
? 'http://localhost'
|
||||
: window.location.origin
|
||||
).href;
|
||||
} catch {
|
||||
return dataUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatShangwutongInlineContent = (
|
||||
content,
|
||||
attachments,
|
||||
contentAttributes
|
||||
) => {
|
||||
const indexes = shangwutongInlineMediaIndexes(contentAttributes);
|
||||
if (!indexes.size) return content || '';
|
||||
|
||||
const renderedIndexes = new Set();
|
||||
const formattedContent = (content || '').replace(
|
||||
INLINE_MEDIA_PATTERN,
|
||||
(match, alt, rawIndex) => {
|
||||
const index = Number(rawIndex);
|
||||
if (!indexes.has(index)) return match;
|
||||
renderedIndexes.add(index);
|
||||
|
||||
const dataUrl = resolveAttachmentURL(attachmentURL(attachments?.[index]));
|
||||
if (!dataUrl) return alt || '商务通表情';
|
||||
|
||||
return ``;
|
||||
}
|
||||
);
|
||||
|
||||
const missingImages = [...indexes]
|
||||
.filter(index => !renderedIndexes.has(index))
|
||||
.map(index => {
|
||||
const dataUrl = resolveAttachmentURL(attachmentURL(attachments?.[index]));
|
||||
return dataUrl ? `` : '商务通表情';
|
||||
});
|
||||
|
||||
if (!missingImages.length) return formattedContent;
|
||||
return [formattedContent, missingImages.join('')].filter(Boolean).join('\n');
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
formatShangwutongInlineContent,
|
||||
shangwutongInlineMediaIndexes,
|
||||
} from './inlineMedia.js';
|
||||
|
||||
describe('Shangwutong inline media', () => {
|
||||
it('replaces declared attachment markers while preserving order', () => {
|
||||
const content =
|
||||
'前中后';
|
||||
const attachments = [
|
||||
{ dataUrl: '/uploads/emoji-a.gif' },
|
||||
{ dataUrl: '/uploads/emoji-b.gif' },
|
||||
];
|
||||
|
||||
expect(
|
||||
formatShangwutongInlineContent(content, attachments, {
|
||||
swt: { inline_media: [0, 1] },
|
||||
})
|
||||
).toBe(
|
||||
'前中后'
|
||||
);
|
||||
});
|
||||
|
||||
it('appends inline images when translated content omits markers', () => {
|
||||
expect(
|
||||
formatShangwutongInlineContent(
|
||||
'translated',
|
||||
[{ dataUrl: '/emoji.gif' }],
|
||||
{
|
||||
swt: { inline_media: [0] },
|
||||
}
|
||||
)
|
||||
).toBe('translated\n');
|
||||
});
|
||||
|
||||
it('does not resolve undeclared markers', () => {
|
||||
const content = '';
|
||||
expect(
|
||||
formatShangwutongInlineContent(content, [{ dataUrl: '/private.png' }], {
|
||||
swt: { inline_media: [] },
|
||||
})
|
||||
).toBe(content);
|
||||
});
|
||||
|
||||
it('accepts numeric strings from JSON attributes', () => {
|
||||
expect(
|
||||
shangwutongInlineMediaIndexes({ swt: { inline_media: ['2'] } })
|
||||
).toEqual(new Set([2]));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user