merge devel

This commit is contained in:
or-else
2025-08-10 12:15:14 +03:00
56 changed files with 4510 additions and 2254 deletions
+2
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -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;
}
+154
View File
@@ -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
View File
@@ -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";
+9 -4
View File
@@ -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
+2 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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>