merge devel
This commit is contained in:
@@ -155,3 +155,5 @@ export const DEFAULT_COLOR_SCHEME = 'light dark';
|
||||
// Default text size in points.
|
||||
export const DEFAULT_TEXT_SIZE = 10;
|
||||
|
||||
// MIME type for the legacy Drafty form-response data.
|
||||
export const DRAFTY_FR_MIME_TYPE_LEGACY = 'application/json'; // Remove in 2026.
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1791
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,8 @@ import InlineVideo from '../widgets/inline-video.jsx';
|
||||
import LazyImage from '../widgets/lazy-image.jsx'
|
||||
import UploadingImage from '../widgets/uploading-image.jsx'
|
||||
|
||||
import { BROKEN_IMAGE_SIZE, CLICKABLE_URL_SCHEMES, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO, REM_SIZE,
|
||||
VIDEO_THUMBNAIL_WIDTH } from '../config.js';
|
||||
import { BROKEN_IMAGE_SIZE, CLICKABLE_URL_SCHEMES, IMAGE_THUMBNAIL_DIM, NO_DIMENSIONS_VIDEO,
|
||||
REM_SIZE, VIDEO_THUMBNAIL_WIDTH } from '../config.js';
|
||||
import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
||||
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
|
||||
import { cancelablePromise, sanitizeUrl, sanitizeUrlForMime } from './utils.js';
|
||||
@@ -304,7 +304,7 @@ export function previewFormatter(style, data, values, key) {
|
||||
break;
|
||||
case 'EX':
|
||||
if (data) {
|
||||
if (data.mime == 'application/json') {
|
||||
if (Drafty.isFormResponseType(data.mime)) {
|
||||
// Ignore JSON attachments: they are form response payloads.
|
||||
return null;
|
||||
}
|
||||
@@ -426,7 +426,7 @@ function quoteFormatter(style, data, values, key) {
|
||||
case 'EX':
|
||||
let fname;
|
||||
if (data) {
|
||||
if (data.mime == 'application/json') {
|
||||
if (Drafty.isFormResponseType(data.mime)) {
|
||||
// Ignore JSON attachments: they are form response payloads.
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
shortDateFormat,
|
||||
relativeDateFormat,
|
||||
secondsToTime,
|
||||
bytesToHumanSize,
|
||||
shortenFileName,
|
||||
idToColorClass,
|
||||
flagEmoji
|
||||
} from './strformat.js';
|
||||
|
||||
describe('shortDateFormat', () => {
|
||||
jest
|
||||
.useFakeTimers()
|
||||
.setSystemTime(new Date('2025-07-25T12:30:00Z'));
|
||||
|
||||
test('should format for same day', () => {
|
||||
const then = new Date('2025-07-25T10:15:00');
|
||||
// Note: output depends on the test runner's locale and timezone.
|
||||
// Assuming a locale where the format is HH:mm.
|
||||
expect(shortDateFormat(then, 'en-US')).toMatch(/10:15/);
|
||||
});
|
||||
|
||||
test('should format for same year, different day', () => {
|
||||
const then = new Date('2025-06-20T10:15:00');
|
||||
expect(shortDateFormat(then, 'en-US')).toMatch(/Jun 20, 10:15/);
|
||||
});
|
||||
|
||||
test('should format for different year', () => {
|
||||
const then = new Date('2024-07-25T10:15:00');
|
||||
expect(shortDateFormat(then, 'en-US')).toBe('Jul 25, 2024');
|
||||
});
|
||||
});
|
||||
|
||||
describe('relativeDateFormat', () => {
|
||||
// Mock current date to a fixed point for consistent testing.
|
||||
jest
|
||||
.useFakeTimers()
|
||||
.setSystemTime(new Date('2025-07-25T12:30:00Z'));
|
||||
|
||||
test('should format for today', () => {
|
||||
const then = new Date('2025-07-25T10:00:00Z');
|
||||
expect(relativeDateFormat(then, 'en-US')).toBe('today');
|
||||
});
|
||||
|
||||
test('should format for yesterday', () => {
|
||||
const then = new Date('2025-07-24T10:00:00Z');
|
||||
expect(relativeDateFormat(then, 'en-US')).toBe('yesterday');
|
||||
});
|
||||
|
||||
test('should format for a past date', () => {
|
||||
const then = new Date('2025-07-20T10:00:00Z');
|
||||
expect(relativeDateFormat(then, 'en-US')).toBe('7/20/2025');
|
||||
});
|
||||
|
||||
test('should format for tomorrow', () => {
|
||||
const then = new Date('2025-07-26T10:00:00Z');
|
||||
expect(relativeDateFormat(then, 'en-US')).toBe('tomorrow');
|
||||
});
|
||||
});
|
||||
|
||||
describe('secondsToTime', () => {
|
||||
test('should format seconds only', () => {
|
||||
expect(secondsToTime(45)).toBe('0:45');
|
||||
});
|
||||
|
||||
test('should format minutes and seconds', () => {
|
||||
expect(secondsToTime(156)).toBe('2:36');
|
||||
});
|
||||
|
||||
test('should format hours, minutes, and seconds', () => {
|
||||
expect(secondsToTime(3756)).toBe('1:02:36');
|
||||
});
|
||||
|
||||
test('should format with fixed minutes', () => {
|
||||
expect(secondsToTime(3726, true)).toBe('1:02:06');
|
||||
expect(secondsToTime(126, true)).toBe('02:06');
|
||||
});
|
||||
|
||||
test('should handle non-number input', () => {
|
||||
expect(secondsToTime('abc')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bytesToHumanSize', () => {
|
||||
test('should format 0 bytes', () => {
|
||||
expect(bytesToHumanSize(0)).toBe('0 Bytes');
|
||||
});
|
||||
|
||||
test('should format bytes', () => {
|
||||
expect(bytesToHumanSize(500)).toBe('500 Bytes');
|
||||
});
|
||||
|
||||
test('should format kilobytes', () => {
|
||||
expect(bytesToHumanSize(1500)).toBe('1.46 KB');
|
||||
});
|
||||
|
||||
test('should format megabytes', () => {
|
||||
expect(bytesToHumanSize(1500000)).toBe('1.43 MB');
|
||||
});
|
||||
|
||||
test('should format gigabytes', () => {
|
||||
expect(bytesToHumanSize(1500000000)).toBe('1.40 GB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('shortenFileName', () => {
|
||||
test('should not shorten if not needed', () => {
|
||||
expect(shortenFileName('shortname.txt', 20)).toBe('shortname.txt');
|
||||
});
|
||||
|
||||
test('should shorten a long file name', () => {
|
||||
const longName = 'this_is_a_very_long_file_name_that_needs_shortening.txt';
|
||||
expect(shortenFileName(longName, 20)).toBe('this_is_a…ening.txt');
|
||||
expect(shortenFileName(longName, 20).length).toBeLessThanOrEqual(20);
|
||||
});
|
||||
|
||||
test('should handle non-string input', () => {
|
||||
expect(shortenFileName(null, 20)).toBeNull();
|
||||
expect(shortenFileName(undefined, 20)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('idToColorClass', () => {
|
||||
test('should generate light background color class', () => {
|
||||
expect(idToColorClass('usr123', true, false)).toMatch(/lt-bg-\d+/);
|
||||
});
|
||||
|
||||
test('should generate dark background color class', () => {
|
||||
expect(idToColorClass('usr123', false, false)).toMatch(/dk-bg-\d+/);
|
||||
});
|
||||
|
||||
test('should generate light foreground color class', () => {
|
||||
expect(idToColorClass('usr123', true, true)).toMatch(/lt-fg-\d+/);
|
||||
});
|
||||
|
||||
test('should generate dark foreground color class', () => {
|
||||
expect(idToColorClass('usr123', false, true)).toMatch(/dk-fg-\d+/);
|
||||
});
|
||||
|
||||
test('should be deterministic', () => {
|
||||
expect(idToColorClass('usr123', true, false)).toBe('lt-bg-2');
|
||||
expect(idToColorClass('grpABC', false, true)).toBe('dk-fg-3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('flagEmoji', () => {
|
||||
test('should convert country code to flag', () => {
|
||||
expect(flagEmoji('US')).toBe('🇺🇸');
|
||||
});
|
||||
|
||||
test('should handle lowercase country code', () => {
|
||||
expect(flagEmoji('jp')).toBe('🇯🇵');
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
// This is a generated file. Don't edit.
|
||||
|
||||
export const PACKAGE_VERSION = "0.24.0";
|
||||
export const PACKAGE_VERSION = "0.25.0";
|
||||
|
||||
@@ -22,8 +22,8 @@ import SendMessage from '../widgets/send-message.jsx';
|
||||
import VideoPreview from '../widgets/video-preview.jsx';
|
||||
|
||||
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, IMMEDIATE_P2P_SUBSCRIPTION,
|
||||
KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
|
||||
READ_DELAY, QUOTED_REPLY_LENGTH, VIDEO_PREVIEW_DIM } from '../config.js';
|
||||
DRAFTY_FR_MIME_TYPE_LEGACY, KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM,
|
||||
MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY, QUOTED_REPLY_LENGTH, VIDEO_PREVIEW_DIM } from '../config.js';
|
||||
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
|
||||
import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
@@ -1022,6 +1022,11 @@ class MessagesView extends React.Component {
|
||||
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
|
||||
MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
|
||||
|
||||
// If the attachment is a JSON file, then use 'application/octet-stream' instead of 'application/json'.
|
||||
// This is a temporary workaround for the collision with the 'application/json' MIME type of form responses.
|
||||
// Remove this code in 2026 or so.
|
||||
const jsonMimeConverter = (fileType) => fileType === DRAFTY_FR_MIME_TYPE_LEGACY ? 'application/octet-stream' : fileType;
|
||||
|
||||
if (file.size > maxInbandAttachmentSize) {
|
||||
// Too large to send inband - uploading out of band and sending as a link.
|
||||
const uploader = this.props.tinode.getLargeFileHelper();
|
||||
@@ -1031,7 +1036,7 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
const uploadCompletionPromise = uploader.upload(file);
|
||||
const msg = Drafty.attachFile(null, {
|
||||
mime: file.type,
|
||||
mime: jsonMimeConverter(file.type),
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
urlPromise: uploadCompletionPromise
|
||||
@@ -1042,7 +1047,7 @@ class MessagesView extends React.Component {
|
||||
// Small enough to send inband.
|
||||
fileToBase64(file)
|
||||
.then(b64 => this.sendMessage(Drafty.attachFile(null, {
|
||||
mime: b64.mime,
|
||||
mime: jsonMimeConverter(b64.mime),
|
||||
data: b64.bits,
|
||||
filename: b64.name,
|
||||
size: file.size
|
||||
|
||||
@@ -162,8 +162,8 @@ class BaseChatMessage extends React.PureComponent {
|
||||
const attachments = [];
|
||||
if (this.props.mimeType == Drafty.getContentType() && Drafty.isValid(content)) {
|
||||
Drafty.attachments(content, (att, i) => {
|
||||
if (att.mime == 'application/json') {
|
||||
// Don't show json objects as attachments.
|
||||
if (Drafty.isFormResponseType(att.mime)) {
|
||||
// Don't show json drafty form response objects as attachments.
|
||||
// They are not meant for users.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class DocPreview extends React.PureComponent {
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
||||
</div>
|
||||
<div id="image-preview-container">
|
||||
<div className="flex-column narrow">
|
||||
<div className="doc-card">
|
||||
<i className="material-icons gray">{iconFromMime(this.props.content.type)}</i>
|
||||
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
|
||||
description="Label for a file name" /></b> {shortenFileName(this.props.content.name, 24) || '-'}</div>
|
||||
|
||||
Reference in New Issue
Block a user