diff --git a/public/share/index.html b/public/share/index.html
new file mode 100644
index 000000000..fb9d17ad8
--- /dev/null
+++ b/public/share/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ Telegram Web
+
+
+
+
+
An error occurred during sharing. You will be redirected back to the app in 3 seconds...
+
If it did not happen automatically, click here.
+
+
+
diff --git a/public/site.webmanifest b/public/site.webmanifest
index 0934f0e61..9516da089 100644
--- a/public/site.webmanifest
+++ b/public/site.webmanifest
@@ -26,6 +26,22 @@
"sizes": "1280x802",
"type": "image/jpeg"
}],
+ "share_target": {
+ "action": "/share/",
+ "method": "POST",
+ "enctype": "multipart/form-data",
+ "params": {
+ "title": "title",
+ "text": "text",
+ "url": "url",
+ "files": [
+ {
+ "name": "files",
+ "accept": "*/*"
+ }
+ ]
+ }
+ },
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
diff --git a/public/site_apple.webmanifest b/public/site_apple.webmanifest
index 658c16d9d..ea66de5e2 100644
--- a/public/site_apple.webmanifest
+++ b/public/site_apple.webmanifest
@@ -26,6 +26,22 @@
"sizes": "1280x802",
"type": "image/jpeg"
}],
+ "share_target": {
+ "action": "/share/",
+ "method": "POST",
+ "enctype": "multipart/form-data",
+ "params": {
+ "title": "title",
+ "text": "text",
+ "url": "url",
+ "files": [
+ {
+ "name": "files",
+ "accept": "*/*"
+ }
+ ]
+ }
+ },
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
diff --git a/public/site_apple_dev.webmanifest b/public/site_apple_dev.webmanifest
index e5763c0a7..b782fa1b9 100644
--- a/public/site_apple_dev.webmanifest
+++ b/public/site_apple_dev.webmanifest
@@ -26,6 +26,22 @@
"sizes": "1280x802",
"type": "image/jpeg"
}],
+ "share_target": {
+ "action": "/share/",
+ "method": "POST",
+ "enctype": "multipart/form-data",
+ "params": {
+ "title": "title",
+ "text": "text",
+ "url": "url",
+ "files": [
+ {
+ "name": "files",
+ "accept": "*/*"
+ }
+ ]
+ }
+ },
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
diff --git a/public/site_dev.webmanifest b/public/site_dev.webmanifest
index 7559baa9b..994e73337 100644
--- a/public/site_dev.webmanifest
+++ b/public/site_dev.webmanifest
@@ -26,6 +26,22 @@
"sizes": "1280x802",
"type": "image/jpeg"
}],
+ "share_target": {
+ "action": "/share/",
+ "method": "POST",
+ "enctype": "multipart/form-data",
+ "params": {
+ "title": "title",
+ "text": "text",
+ "url": "url",
+ "files": [
+ {
+ "name": "files",
+ "accept": "*/*"
+ }
+ ]
+ }
+ },
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
diff --git a/src/components/main/DraftRecipientPicker.tsx b/src/components/main/DraftRecipientPicker.tsx
index 8c3515fc3..604154aee 100644
--- a/src/components/main/DraftRecipientPicker.tsx
+++ b/src/components/main/DraftRecipientPicker.tsx
@@ -34,7 +34,7 @@ const DraftRecipientPicker: FC = ({
}, [isOpen, markIsShown]);
const handleSelectRecipient = useCallback((recipientId: string) => {
- openChatWithDraft({ chatId: recipientId, text: requestedDraft!.text });
+ openChatWithDraft({ chatId: recipientId, text: requestedDraft!.text, files: requestedDraft!.files });
}, [openChatWithDraft, requestedDraft]);
const handleClose = useCallback(() => {
diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx
index 34e005c1f..864511393 100644
--- a/src/components/middle/composer/AttachmentModal.tsx
+++ b/src/components/middle/composer/AttachmentModal.tsx
@@ -7,7 +7,6 @@ import type { FC } from '../../../lib/teact/teact';
import type { ApiAttachment, ApiChatMember, ApiSticker } from '../../../api/types';
import {
- CONTENT_TYPES_WITH_PREVIEW,
EDITABLE_INPUT_MODAL_ID,
SUPPORTED_AUDIO_CONTENT_TYPES,
SUPPORTED_IMAGE_CONTENT_TYPES,
@@ -16,6 +15,7 @@ import {
import { getFileExtension } from '../../common/helpers/documentInfo';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
import getFilesFromDataTransferItems from './helpers/getFilesFromDataTransferItems';
+import { hasPreview } from '../../../util/files';
import usePrevious from '../../../hooks/usePrevious';
import useMentionTooltip from './hooks/useMentionTooltip';
@@ -185,11 +185,7 @@ const AttachmentModal: FC = ({
const files = await getFilesFromDataTransferItems(dataTransfer.items);
if (files?.length) {
- const newFiles = isQuick
- ? Array.from(files).filter((file) => {
- return file.type && CONTENT_TYPES_WITH_PREVIEW.has(file.type);
- })
- : Array.from(files);
+ const newFiles = Array.from(files).filter((file) => !isQuick || hasPreview(file));
onFileAppend(newFiles, isQuick);
}
diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx
index 417a08197..f2cff7341 100644
--- a/src/components/middle/composer/Composer.tsx
+++ b/src/components/middle/composer/Composer.tsx
@@ -50,11 +50,12 @@ import {
selectCanScheduleUntilOnline,
selectEditingScheduledDraft,
selectEditingDraft,
- selectRequestedText,
+ selectRequestedDraftText,
selectTheme,
selectCurrentMessageList,
selectIsCurrentUserPremium,
selectChatType,
+ selectRequestedDraftFiles,
} from '../../../global/selectors';
import {
getAllowedAttachmentOptions,
@@ -75,6 +76,7 @@ import windowSize from '../../../util/windowSize';
import { isSelectionInsideInput } from './helpers/selection';
import applyIosAutoCapitalizationFix from './helpers/applyIosAutoCapitalizationFix';
import { getServerTime } from '../../../util/serverTime';
+import { hasPreview } from '../../../util/files';
import { selectCurrentLimit } from '../../../global/selectors/limits';
import { buildCustomEmojiHtml } from './helpers/customEmoji';
import { processMessageInputForCustomEmoji } from '../../../util/customEmojiManager';
@@ -175,7 +177,8 @@ type StateProps =
sendAsChat?: ApiChat;
sendAsId?: string;
editingDraft?: ApiFormattedText;
- requestedText?: string;
+ requestedDraftText?: string;
+ requestedDraftFiles?: File[];
attachBots: GlobalState['attachMenu']['bots'];
attachMenuPeerType?: ApiAttachMenuPeerType;
theme: ISettings['theme'];
@@ -256,7 +259,8 @@ const Composer: FC = ({
sendAsChat,
sendAsId,
editingDraft,
- requestedText,
+ requestedDraftText,
+ requestedDraftFiles,
botMenuButton,
attachBots,
attachMenuPeerType,
@@ -795,15 +799,23 @@ const Composer: FC = ({
}, [contentToBeScheduled, handleMessageSchedule, requestCalendar]);
useEffect(() => {
- if (requestedText) {
- setHtml(requestedText);
+ if (requestedDraftText) {
+ setHtml(requestedDraftText);
resetOpenChatWithDraft();
requestAnimationFrame(() => {
const messageInput = document.getElementById(EDITABLE_INPUT_ID)!;
focusEditableElement(messageInput, true);
});
}
- }, [requestedText, resetOpenChatWithDraft, setHtml]);
+ }, [requestedDraftText, resetOpenChatWithDraft, setHtml]);
+
+ useEffect(() => {
+ if (requestedDraftFiles?.length) {
+ const isQuick = requestedDraftFiles.every((file) => hasPreview(file));
+ handleFileSelect(requestedDraftFiles, isQuick);
+ resetOpenChatWithDraft();
+ }
+ }, [handleFileSelect, requestedDraftFiles, resetOpenChatWithDraft]);
const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => {
if (!emoji.isFree && !isCurrentUserPremium && !isChatWithSelf) {
@@ -1417,7 +1429,8 @@ export default memo(withGlobal(
: (chat?.adminRights?.anonymous ? chat?.id : undefined);
const sendAsUser = sendAsId ? selectUser(global, sendAsId) : undefined;
const sendAsChat = !sendAsUser && sendAsId ? selectChat(global, sendAsId) : undefined;
- const requestedText = selectRequestedText(global, chatId);
+ const requestedDraftText = selectRequestedDraftText(global, chatId);
+ const requestedDraftFiles = selectRequestedDraftFiles(global, chatId);
const currentMessageList = selectCurrentMessageList(global);
const isForCurrentMessageList = chatId === currentMessageList?.chatId
&& threadId === currentMessageList?.threadId
@@ -1472,7 +1485,8 @@ export default memo(withGlobal(
sendAsChat,
sendAsId,
editingDraft,
- requestedText,
+ requestedDraftText,
+ requestedDraftFiles,
attachBots: global.attachMenu.bots,
attachMenuPeerType: selectChatType(global, chatId),
theme: selectTheme(global),
diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts
index 88e9e4f65..4fbe1548f 100644
--- a/src/global/actions/ui/chats.ts
+++ b/src/global/actions/ui/chats.ts
@@ -71,7 +71,7 @@ addActionHandler('openChatWithInfo', (global, actions, payload) => {
});
addActionHandler('openChatWithDraft', (global, actions, payload) => {
- const { chatId, text } = payload;
+ const { chatId, text, files } = payload;
if (chatId) {
actions.openChat({ id: chatId });
@@ -82,6 +82,7 @@ addActionHandler('openChatWithDraft', (global, actions, payload) => {
requestedDraft: {
chatId,
text,
+ files,
},
};
});
diff --git a/src/global/selectors/chats.ts b/src/global/selectors/chats.ts
index 9d16e1e59..99c0e5ca4 100644
--- a/src/global/selectors/chats.ts
+++ b/src/global/selectors/chats.ts
@@ -193,9 +193,18 @@ export function selectSendAs(global: GlobalState, chatId: string) {
return selectUser(global, id) || selectChat(global, id);
}
-export function selectRequestedText(global: GlobalState, chatId: string) {
- if (global.requestedDraft?.chatId === chatId) {
- return global.requestedDraft.text;
+export function selectRequestedDraftText(global: GlobalState, chatId: string) {
+ const { requestedDraft } = global;
+ if (requestedDraft?.chatId === chatId && !requestedDraft.files?.length) {
+ return requestedDraft.text;
+ }
+ return undefined;
+}
+
+export function selectRequestedDraftFiles(global: GlobalState, chatId: string) {
+ const { requestedDraft } = global;
+ if (requestedDraft?.chatId === chatId) {
+ return requestedDraft.files;
}
return undefined;
}
diff --git a/src/global/types.ts b/src/global/types.ts
index 9ab41d5bf..1364e00f0 100644
--- a/src/global/types.ts
+++ b/src/global/types.ts
@@ -601,6 +601,7 @@ export type GlobalState = {
requestedDraft?: {
chatId?: string;
text: string;
+ files?: File[];
};
pollModal: {
@@ -743,6 +744,7 @@ export interface ActionPayloads {
openChatWithDraft: {
chatId?: string;
text: string;
+ files?: File[];
};
resetOpenChatWithDraft: never;
toggleJoinToSend: {
diff --git a/src/lib/gramjs/Helpers.js b/src/lib/gramjs/Helpers.js
index f574d2f7c..de5fd701e 100644
--- a/src/lib/gramjs/Helpers.js
+++ b/src/lib/gramjs/Helpers.js
@@ -340,22 +340,6 @@ function crc32(buf) {
return (crc ^ (-1)) >>> 0;
}
-/**
- * Creates a deferred object
- * @return {Deferred}
- */
-function createDeferred() {
- let resolve;
- const promise = new Promise((_resolve) => {
- resolve = _resolve;
- });
-
- return {
- promise,
- resolve,
- };
-}
-
module.exports = {
readBigIntFromBuffer,
readBufferFromBigInt,
@@ -376,5 +360,4 @@ module.exports = {
toSignedLittleBuffer,
convertToLittle,
bufferXor,
- createDeferred,
};
diff --git a/src/lib/gramjs/client/downloadFile.ts b/src/lib/gramjs/client/downloadFile.ts
index ee38a763d..150b0376b 100644
--- a/src/lib/gramjs/client/downloadFile.ts
+++ b/src/lib/gramjs/client/downloadFile.ts
@@ -1,9 +1,10 @@
import BigInt from 'big-integer';
import Api from '../tl/api';
import type TelegramClient from './TelegramClient';
-import { sleep, createDeferred } from '../Helpers';
+import { sleep } from '../Helpers';
import { getDownloadPartSize } from '../Utils';
import errors from '../errors';
+import Deferred from '../../../util/Deferred';
interface OnProgress {
isCanceled?: boolean;
@@ -25,11 +26,6 @@ export interface DownloadFileParams {
progressCallback?: OnProgress;
}
-interface Deferred {
- promise: Promise;
- resolve: (value?: any) => void;
-}
-
// Chunk sizes for `upload.getFile` must be multiple of the smallest size
const MIN_CHUNK_SIZE = 4096;
const DEFAULT_CHUNK_SIZE = 64; // kb
@@ -51,7 +47,7 @@ class Foreman {
requestWorker() {
if (this.activeWorkers === this.maxWorkers) {
- const deferred = createDeferred();
+ const deferred = new Deferred();
this.deferreds.push(deferred);
return deferred.promise;
} else {
@@ -225,7 +221,7 @@ async function downloadFile2(
if (deferred) await deferred.promise;
- if (noParallel) deferred = createDeferred();
+ if (noParallel) deferred = new Deferred();
if (hasEnded) {
foremans[senderIndex].releaseWorker();
diff --git a/src/lib/gramjs/network/RequestState.js b/src/lib/gramjs/network/RequestState.js
index 70cd79a56..69c74ad17 100644
--- a/src/lib/gramjs/network/RequestState.js
+++ b/src/lib/gramjs/network/RequestState.js
@@ -1,4 +1,4 @@
-const { createDeferred } = require('../Helpers');
+const { default: Deferred } = require('../../../util/Deferred');
class RequestState {
constructor(request, after = undefined, pending = {}) {
@@ -9,7 +9,7 @@ class RequestState {
this.after = after;
this.result = undefined;
this.pending = pending;
- this.deferred = createDeferred();
+ this.deferred = new Deferred();
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts
index a43326f07..dabafc826 100644
--- a/src/serviceWorker.ts
+++ b/src/serviceWorker.ts
@@ -2,7 +2,13 @@ import { DEBUG } from './config';
import { respondForProgressive } from './serviceWorker/progressive';
import { respondForDownload } from './serviceWorker/download';
import { respondWithCache, clearAssetCache, respondWithCacheNetworkFirst } from './serviceWorker/assetCache';
-import { handlePush, handleNotificationClick, handleClientMessage } from './serviceWorker/pushNotification';
+import {
+ handlePush,
+ handleNotificationClick,
+ handleClientMessage as handleNotificationMessage,
+} from './serviceWorker/pushNotification';
+import { respondForShare, handleClientMessage as handleShareMessage } from './serviceWorker/share';
+
import { pause } from './util/schedulers';
declare const self: ServiceWorkerGlobalScope;
@@ -53,6 +59,10 @@ self.addEventListener('fetch', (e: FetchEvent) => {
return true;
}
+ if (url.includes('/share/')) {
+ e.respondWith(respondForShare(e));
+ }
+
if (url.startsWith('http')) {
if (NETWORK_FIRST_ASSETS.has(new URL(url).pathname)) {
e.respondWith(respondWithCacheNetworkFirst(e));
@@ -70,4 +80,7 @@ self.addEventListener('fetch', (e: FetchEvent) => {
self.addEventListener('push', handlePush);
self.addEventListener('notificationclick', handleNotificationClick);
-self.addEventListener('message', handleClientMessage);
+self.addEventListener('message', (event) => {
+ handleNotificationMessage(event);
+ handleShareMessage(event);
+});
diff --git a/src/serviceWorker/share.ts b/src/serviceWorker/share.ts
new file mode 100644
index 000000000..28178ac84
--- /dev/null
+++ b/src/serviceWorker/share.ts
@@ -0,0 +1,83 @@
+import Deferred from '../util/Deferred';
+
+declare const self: ServiceWorkerGlobalScope;
+
+type ShareData = {
+ title?: string;
+ text?: string;
+ url?: string;
+ files?: File[];
+};
+
+const RESOLVED_DEFERRED = new Deferred();
+RESOLVED_DEFERRED.resolve();
+const READY_CLIENT_DEFERREDS = new Map>();
+
+export async function respondForShare(e: FetchEvent) {
+ if (e.request.method === 'POST') {
+ try {
+ const formData = await e.request.formData();
+ const data = parseFormData(formData);
+ requestShare(data, e.resultingClientId);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.warn('[SHARE] Failed to parse input data', err);
+ }
+ }
+
+ return Response.redirect('..');
+}
+
+export function handleClientMessage(e: ExtendableMessageEvent) {
+ const { source, data } = e;
+ if (!source) return;
+
+ if (data.type === 'clientReady') {
+ const { id } = (source as Client);
+ const deferred = READY_CLIENT_DEFERREDS.get(id);
+ if (deferred) {
+ deferred.resolve();
+ } else {
+ READY_CLIENT_DEFERREDS.set(id, RESOLVED_DEFERRED);
+ }
+ }
+}
+
+async function requestShare(data: ShareData, clientId: string) {
+ const client = await self.clients.get(clientId);
+ if (!client) {
+ return;
+ }
+
+ await getClientReadyDeferred(clientId);
+
+ client.postMessage({
+ type: 'share',
+ payload: data,
+ });
+}
+
+function getClientReadyDeferred(clientId: string) {
+ const deferred = READY_CLIENT_DEFERREDS.get(clientId);
+ if (deferred) {
+ return deferred.promise;
+ }
+
+ const newDeferred = new Deferred();
+ READY_CLIENT_DEFERREDS.set(clientId, newDeferred);
+ return newDeferred.promise;
+}
+
+function parseFormData(formData: FormData): ShareData {
+ const files = formData.getAll('files') as File[];
+ const title = formData.get('title') as string;
+ const text = formData.get('text') as string;
+ const url = formData.get('url') as string;
+
+ return {
+ title,
+ text,
+ url,
+ files,
+ };
+}
diff --git a/src/util/Deferred.ts b/src/util/Deferred.ts
new file mode 100644
index 000000000..2c54832d6
--- /dev/null
+++ b/src/util/Deferred.ts
@@ -0,0 +1,14 @@
+export default class Deferred {
+ promise: Promise;
+
+ reject!: (reason?: any) => void;
+
+ resolve!: (value: T | PromiseLike) => void;
+
+ constructor() {
+ this.promise = new Promise((resolve, reject) => {
+ this.reject = reject;
+ this.resolve = resolve;
+ });
+ }
+}
diff --git a/src/util/deeplink.ts b/src/util/deeplink.ts
index 9f22a4271..088163dee 100644
--- a/src/util/deeplink.ts
+++ b/src/util/deeplink.ts
@@ -132,6 +132,6 @@ export function parseChooseParameter(choose?: string) {
return types.filter((type): type is ApiChatType => API_CHAT_TYPES.includes(type as ApiChatType));
}
-export function formatShareText(url?: string, text?: string) {
- return [url, text].filter(Boolean).join('\n');
+export function formatShareText(url?: string, text?: string, title?: string): string {
+ return [url, title, text].filter(Boolean).join('\n');
}
diff --git a/src/util/files.ts b/src/util/files.ts
index e80aa7d91..d9536d3c1 100644
--- a/src/util/files.ts
+++ b/src/util/files.ts
@@ -1,3 +1,4 @@
+import { CONTENT_TYPES_WITH_PREVIEW } from '../config';
import { pause } from './schedulers';
// Polyfill for Safari: `File` is not available in web worker
@@ -122,3 +123,7 @@ export function imgToCanvas(img: HTMLImageElement) {
return canvas;
}
+
+export function hasPreview(file: File) {
+ return CONTENT_TYPES_WITH_PREVIEW.has(file.type);
+}
diff --git a/src/util/setupServiceWorker.ts b/src/util/setupServiceWorker.ts
index 8749d0944..fef2cbcfd 100644
--- a/src/util/setupServiceWorker.ts
+++ b/src/util/setupServiceWorker.ts
@@ -1,5 +1,6 @@
import { DEBUG, DEBUG_MORE, IS_TEST } from '../config';
import { getActions } from '../global';
+import { formatShareText } from './deeplink';
import { IS_ANDROID, IS_IOS, IS_SERVICE_WORKER_SUPPORTED } from './environment';
import { notifyClientReady, playNotifySoundDebounced } from './notifications';
@@ -26,6 +27,12 @@ function handleWorkerMessage(e: MessageEvent) {
case 'playNotificationSound':
playNotifySoundDebounced(action.payload.id);
break;
+ case 'share':
+ dispatch.openChatWithDraft({
+ text: formatShareText(payload.url, payload.text, payload.title),
+ files: payload.files,
+ });
+ break;
}
}