PWA: Support system sharing menu (#2057)
This commit is contained in:
parent
0e4f9e5ae6
commit
08ef6130b7
28
public/share/index.html
Normal file
28
public/share/index.html
Normal file
@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="refresh" content="3;URL='..'">
|
||||
<title>Telegram Web</title>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="message">
|
||||
<p>An error occurred during sharing. You will be redirected back to the app in 3 seconds...</p>
|
||||
<p>If it did not happen automatically, <a href="..">click here</a>.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -34,7 +34,7 @@ const DraftRecipientPicker: FC<OwnProps> = ({
|
||||
}, [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(() => {
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
sendAsChat,
|
||||
sendAsId,
|
||||
editingDraft,
|
||||
requestedText,
|
||||
requestedDraftText,
|
||||
requestedDraftFiles,
|
||||
botMenuButton,
|
||||
attachBots,
|
||||
attachMenuPeerType,
|
||||
@ -795,15 +799,23 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}, [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<OwnProps>(
|
||||
: (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<OwnProps>(
|
||||
sendAsChat,
|
||||
sendAsId,
|
||||
editingDraft,
|
||||
requestedText,
|
||||
requestedDraftText,
|
||||
requestedDraftFiles,
|
||||
attachBots: global.attachMenu.bots,
|
||||
attachMenuPeerType: selectChatType(global, chatId),
|
||||
theme: selectTheme(global),
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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<any>;
|
||||
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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
83
src/serviceWorker/share.ts
Normal file
83
src/serviceWorker/share.ts
Normal file
@ -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<void>();
|
||||
RESOLVED_DEFERRED.resolve();
|
||||
const READY_CLIENT_DEFERREDS = new Map<string, Deferred<void>>();
|
||||
|
||||
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<void>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
14
src/util/Deferred.ts
Normal file
14
src/util/Deferred.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export default class Deferred<T = void> {
|
||||
promise: Promise<T>;
|
||||
|
||||
reject!: (reason?: any) => void;
|
||||
|
||||
resolve!: (value: T | PromiseLike<T>) => void;
|
||||
|
||||
constructor() {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.reject = reject;
|
||||
this.resolve = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user