Support redirects from t.me (websync) (#1383)

This commit is contained in:
Alexander Zinchuk 2021-08-12 03:34:56 +03:00
parent 6fa2fae909
commit ae0ad36478
16 changed files with 237 additions and 23 deletions

View File

@ -73,6 +73,7 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet {
thumbs,
count,
hash,
shortName,
} = set;
return {
@ -85,6 +86,7 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet {
hasThumbnail: Boolean(thumbs && thumbs.length),
count,
hash,
shortName,
};
}

View File

@ -113,6 +113,12 @@ export function buildInputStickerSet(id: string, accessHash: string) {
});
}
export function buildInputStickerSetShortName(shortName: string) {
return new GramJs.InputStickerSetShortName({
shortName,
});
}
export function buildInputDocument(media: ApiSticker | ApiVideo) {
const document = localDb.documents[media.id];

View File

@ -3,7 +3,7 @@ import { ApiSticker, ApiVideo, OnApiUpdate } from '../../types';
import { invokeRequest } from './client';
import { buildStickerFromDocument, buildStickerSet, buildStickerSetCovered } from '../apiBuilders/symbols';
import { buildInputStickerSet, buildInputDocument } from '../gramjsBuilders';
import { buildInputStickerSet, buildInputDocument, buildInputStickerSetShortName } from '../gramjsBuilders';
import { buildVideoFromDocument } from '../apiBuilders/messages';
import { RECENT_STICKERS_LIMIT } from '../../../config';
@ -93,9 +93,12 @@ export async function faveSticker({
}
}
export async function fetchStickers({ stickerSetId, accessHash }: { stickerSetId: string; accessHash: string }) {
export async function fetchStickers({ stickerSetShortName, stickerSetId, accessHash }:
{ stickerSetShortName?: string; stickerSetId?: string; accessHash: string }) {
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
stickerset: buildInputStickerSet(stickerSetId, accessHash),
stickerset: stickerSetId
? buildInputStickerSet(stickerSetId, accessHash)
: buildInputStickerSetShortName(stickerSetShortName!),
}));
if (!result) {

View File

@ -43,6 +43,7 @@ export interface ApiStickerSet {
stickers?: ApiSticker[];
packs?: Record<string, ApiSticker[]>;
covers?: ApiSticker[];
shortName: string;
}
export interface ApiVideo {

View File

@ -8,7 +8,7 @@ import { GlobalActions } from '../../global/types';
import { STICKER_SIZE_MODAL } from '../../config';
import { pick } from '../../util/iteratees';
import { selectStickerSet } from '../../modules/selectors';
import { selectStickerSet, selectStickerSetByShortName } from '../../modules/selectors';
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang';
import renderText from './helpers/renderText';
@ -22,7 +22,8 @@ import './StickerSetModal.scss';
export type OwnProps = {
isOpen: boolean;
fromSticker: ApiSticker;
fromSticker?: ApiSticker;
stickerSetShortName?: string;
onClose: () => void;
};
@ -37,6 +38,7 @@ const INTERSECTION_THROTTLE = 200;
const StickerSetModal: FC<OwnProps & StateProps & DispatchProps> = ({
isOpen,
fromSticker,
stickerSetShortName,
stickerSet,
onClose,
loadStickers,
@ -53,10 +55,19 @@ const StickerSetModal: FC<OwnProps & StateProps & DispatchProps> = ({
useEffect(() => {
if (isOpen) {
const { stickerSetId, stickerSetAccessHash } = fromSticker;
loadStickers({ stickerSetId, stickerSetAccessHash });
if (fromSticker) {
const { stickerSetId, stickerSetAccessHash } = fromSticker;
loadStickers({
stickerSetId,
stickerSetAccessHash,
});
} else {
loadStickers({
stickerSetShortName,
});
}
}
}, [isOpen, fromSticker, loadStickers]);
}, [isOpen, fromSticker, loadStickers, stickerSetShortName]);
const handleSelect = useCallback((sticker: ApiSticker) => {
sticker = {
@ -69,9 +80,11 @@ const StickerSetModal: FC<OwnProps & StateProps & DispatchProps> = ({
}, [onClose, sendMessage]);
const handleButtonClick = useCallback(() => {
toggleStickerSet({ stickerSetId: fromSticker.stickerSetId });
onClose();
}, [fromSticker.stickerSetId, onClose, toggleStickerSet]);
if (stickerSet) {
toggleStickerSet({ stickerSetId: stickerSet.id });
onClose();
}
}, [onClose, stickerSet, toggleStickerSet]);
return (
<Modal
@ -117,8 +130,12 @@ const StickerSetModal: FC<OwnProps & StateProps & DispatchProps> = ({
};
export default memo(withGlobal(
(global, { fromSticker }: OwnProps) => {
return { stickerSet: selectStickerSet(global, fromSticker.stickerSetId) };
(global, { fromSticker, stickerSetShortName }: OwnProps) => {
return {
stickerSet: fromSticker
? selectStickerSet(global, fromSticker.stickerSetId)
: selectStickerSetByShortName(global, stickerSetShortName!),
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
'loadStickers',

View File

@ -27,6 +27,8 @@ import useShowTransition from '../../hooks/useShowTransition';
import useBackgroundMode from '../../hooks/useBackgroundMode';
import useBeforeUnload from '../../hooks/useBeforeUnload';
import useOnChange from '../../hooks/useOnChange';
import { processDeepLink } from '../../util/deeplink';
import { LOCATION_HASH } from '../../hooks/useHistoryBack';
import LeftColumn from '../left/LeftColumn';
import MiddleColumn from '../middle/MiddleColumn';
@ -38,6 +40,7 @@ import Dialogs from './Dialogs.async';
import ForwardPicker from './ForwardPicker.async';
import SafeLinkModal from './SafeLinkModal.async';
import HistoryCalendar from './HistoryCalendar.async';
import StickerSetModal from '../common/StickerSetModal.async';
import './Main.scss';
@ -55,11 +58,12 @@ type StateProps = {
isHistoryCalendarOpen: boolean;
shouldSkipHistoryAnimations?: boolean;
language?: LangCode;
openedStickerSetShortName?: string;
};
type DispatchProps = Pick<GlobalActions, (
'loadAnimatedEmojis' | 'loadNotificationSettings' | 'loadNotificationExceptions' | 'updateIsOnline' |
'loadTopInlineBots' | 'loadEmojiKeywords'
'loadTopInlineBots' | 'loadEmojiKeywords' | 'openStickerSetShortName'
)>;
const NOTIFICATION_INTERVAL = 1000;
@ -82,12 +86,14 @@ const Main: FC<StateProps & DispatchProps> = ({
isHistoryCalendarOpen,
shouldSkipHistoryAnimations,
language,
openedStickerSetShortName,
loadAnimatedEmojis,
loadNotificationSettings,
loadNotificationExceptions,
updateIsOnline,
loadTopInlineBots,
loadEmojiKeywords,
openStickerSetShortName,
}) => {
if (DEBUG && !DEBUG_isLogged) {
DEBUG_isLogged = true;
@ -114,6 +120,12 @@ const Main: FC<StateProps & DispatchProps> = ({
loadTopInlineBots, loadEmojiKeywords, language,
]);
useEffect(() => {
if (lastSyncTime && LOCATION_HASH.startsWith('#?tgaddr=')) {
processDeepLink(decodeURIComponent(LOCATION_HASH.substr('#?tgaddr='.length)));
}
}, [lastSyncTime]);
const {
transitionClassNames: middleColumnTransitionClassNames,
} = useShowTransition(!isLeftColumnShown, undefined, true, undefined, shouldSkipHistoryAnimations);
@ -223,6 +235,11 @@ const Main: FC<StateProps & DispatchProps> = ({
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
<SafeLinkModal url={safeLinkModalUrl} />
<HistoryCalendar isOpen={isHistoryCalendarOpen} />
<StickerSetModal
isOpen={!!openedStickerSetShortName}
onClose={() => openStickerSetShortName({ stickerSetShortName: undefined })}
stickerSetShortName={openedStickerSetShortName}
/>
</div>
);
};
@ -269,10 +286,11 @@ export default memo(withGlobal(
isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt),
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
language: global.settings.byKey.language,
openedStickerSetShortName: global.openedStickerSetShortName,
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
'loadAnimatedEmojis', 'loadNotificationSettings', 'loadNotificationExceptions', 'updateIsOnline',
'loadTopInlineBots', 'loadEmojiKeywords',
'loadTopInlineBots', 'loadEmojiKeywords', 'openStickerSetShortName',
]),
)(Main));

View File

@ -421,6 +421,7 @@ export type GlobalState = {
safeLinkModalUrl?: string;
historyCalendarSelectedAt?: number;
openedStickerSetShortName?: string;
// TODO To be removed in August 2021
shouldShowContextMenuHint?: boolean;
@ -491,6 +492,7 @@ export type ActionTypes = (
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' |
'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' | 'loadGreetingStickers' |
'openStickerSetShortName' |
// bots
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
'resetInlineBot' | 'restartBot' |

View File

@ -9,6 +9,7 @@ import { areSortedArraysEqual } from '../util/iteratees';
// TODO: may be different on other devices such as iPad, maybe take dpi into account?
const SAFARI_EDGE_BACK_GESTURE_LIMIT = 300;
const SAFARI_EDGE_BACK_GESTURE_DURATION = 350;
export const LOCATION_HASH = window.location.hash;
type HistoryState = {
currentIndex: number;
@ -54,7 +55,7 @@ if (IS_IOS) {
window.addEventListener('popstate', handleTouchEnd);
}
window.history.replaceState({ index: historyState.currentIndex }, '');
window.history.replaceState({ index: historyState.currentIndex }, '', window.location.pathname);
export default function useHistoryBack(
isActive: boolean | undefined,

View File

@ -24,6 +24,7 @@ import {
importLegacySession,
clearLegacySessions,
} from '../../../util/sessions';
import { forceWebsync } from '../../../util/websync';
addReducer('initApi', (global: GlobalState, actions) => {
(async () => {
@ -128,6 +129,7 @@ addReducer('signOut', () => {
try {
await unsubscribe();
await callApi('destroy');
await forceWebsync(false);
} catch (err) {
// Do nothing
}

View File

@ -83,10 +83,10 @@ addReducer('loadFeaturedStickers', (global) => {
});
addReducer('loadStickers', (global, actions, payload) => {
const { stickerSetId } = payload!;
const { stickerSetId, stickerSetShortName } = payload!;
let { stickerSetAccessHash } = payload!;
if (!stickerSetAccessHash) {
if (!stickerSetAccessHash && !stickerSetShortName) {
const stickerSet = selectStickerSet(global, stickerSetId);
if (!stickerSet) {
return;
@ -95,7 +95,7 @@ addReducer('loadStickers', (global, actions, payload) => {
stickerSetAccessHash = stickerSet.accessHash;
}
void loadStickers(stickerSetId, stickerSetAccessHash);
void loadStickers(stickerSetId, stickerSetAccessHash, stickerSetShortName);
});
addReducer('loadAnimatedEmojis', () => {
@ -257,8 +257,9 @@ async function loadFeaturedStickers(hash = 0) {
));
}
async function loadStickers(stickerSetId: string, accessHash: string) {
const stickerSet = await callApi('fetchStickers', { stickerSetId, accessHash });
async function loadStickers(stickerSetId: string, accessHash: string, stickerSetShortName?: string) {
const stickerSet = await callApi('fetchStickers',
{ stickerSetShortName, stickerSetId, accessHash });
if (!stickerSet) {
return;
}
@ -356,6 +357,14 @@ addReducer('clearStickersForEmoji', (global) => {
};
});
addReducer('openStickerSetShortName', (global, actions, payload) => {
const { stickerSetShortName } = payload!;
return {
...global,
openedStickerSetShortName: stickerSetShortName,
};
});
async function searchStickers(query: string, hash = 0) {
const result = await callApi('searchStickers', { query, hash });

View File

@ -17,6 +17,7 @@ import { subscribe } from '../../../util/notifications';
import { updateUser } from '../../reducers';
import { setLanguage } from '../../../util/langProvider';
import { selectNotifySettings } from '../../selectors';
import { forceWebsync } from '../../../util/websync';
addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
if (DEBUG) {
@ -90,6 +91,8 @@ function onUpdateAuthorizationState(update: ApiUpdateAuthorizationState) {
switch (authState) {
case 'authorizationStateLoggingOut':
void forceWebsync(false);
setGlobal({
...global,
isLoggingOut: true,
@ -119,6 +122,8 @@ function onUpdateAuthorizationState(update: ApiUpdateAuthorizationState) {
break;
}
void forceWebsync(true);
setGlobal({
...global,
isLoggingOut: false,

View File

@ -7,6 +7,7 @@ import {
import { setLanguage } from '../../../util/langProvider';
import switchTheme from '../../../util/switchTheme';
import { selectTheme } from '../../selectors';
import { startWebsync } from '../../../util/websync';
const HISTORY_ANIMATION_DURATION = 450;
@ -28,6 +29,7 @@ addReducer('init', (global) => {
document.body.classList.add(`animation-level-${animationLevel}`);
document.body.classList.add(IS_TOUCH_ENV ? 'is-touch-env' : 'is-pointer-env');
switchTheme(theme, animationLevel === ANIMATION_LEVEL_MAX);
startWebsync();
if (IS_SAFARI) {
document.body.classList.add('is-safari');

View File

@ -18,6 +18,10 @@ export function selectStickerSet(global: GlobalState, id: string) {
return global.stickers.setsById[id];
}
export function selectStickerSetByShortName(global: GlobalState, shortName: string) {
return Object.values(global.stickers.setsById).find((l) => l.shortName.toLowerCase() === shortName.toLowerCase());
}
export function selectStickersForEmoji(global: GlobalState, emoji: string) {
const stickerSets = Object.values(global.stickers.setsById);
let stickersForEmoji: ApiSticker[] = [];

View File

@ -30,9 +30,13 @@ self.addEventListener('activate', (e) => {
// eslint-disable-next-line no-restricted-globals
self.addEventListener('fetch', (e: FetchEvent) => {
e.respondWith((() => {
const { url } = e.request;
const { url } = e.request;
if (url.includes('_websync_')) {
return false;
}
e.respondWith((() => {
if (url.includes('/progressive/')) {
return respondForProgressive(e);
}
@ -43,6 +47,8 @@ self.addEventListener('fetch', (e: FetchEvent) => {
return fetch(e.request);
})());
return true;
});
self.addEventListener('push', handlePush);

54
src/util/deeplink.ts Normal file
View File

@ -0,0 +1,54 @@
import { getDispatch } from '../lib/teact/teactn';
export const processDeepLink = (url: string) => {
const { protocol, searchParams, pathname } = new URL(url);
if (protocol !== 'tg:') return;
const { openChatByUsername, openStickerSetShortName } = getDispatch();
const method = pathname.replace(/^\/\//, '');
const params: Record<string, string> = {};
searchParams.forEach((value, key) => {
params[key] = value;
});
switch (method) {
case 'resolve': {
const {
domain,
} = params;
if (domain !== 'telegrampassport') {
openChatByUsername({
username: domain,
});
}
break;
}
case 'privatepost':
break;
case 'bg':
break;
case 'join':
break;
case 'addstickers': {
const { set } = params;
openStickerSetShortName({
stickerSetShortName: set,
});
break;
}
case 'msg':
break;
default:
// Unsupported deeplink
break;
}
};

82
src/util/websync.ts Normal file
View File

@ -0,0 +1,82 @@
import { APP_VERSION } from '../config';
import { getGlobal } from '../lib/teact/teactn';
import { hasStoredSession } from './sessions';
const WEBSYNC_URLS = [
't.me',
'telegram.me',
].map((domain) => `//${domain}/_websync_?`);
const WEBSYNC_VERSION = `${APP_VERSION} Z`;
const WEBSYNC_KEY = 'tgme_sync';
const WEBSYNC_TIMEOUT = 86400;
const getTs = () => {
return Math.floor(Number(new Date()) / 1000);
};
const saveSync = (authed: boolean) => {
const ts = getTs();
localStorage.setItem(WEBSYNC_KEY, JSON.stringify({
canRedirect: authed,
ts,
}));
};
let lastTimeout: NodeJS.Timeout | undefined;
export const forceWebsync = (authed: boolean) => {
const currentTs = getTs();
const { canRedirect, ts } = JSON.parse(localStorage.getItem(WEBSYNC_KEY) || '{}');
if (canRedirect !== authed || ts + WEBSYNC_TIMEOUT <= currentTs) {
return Promise.all(WEBSYNC_URLS.map((url) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
const removeElement = () => !!document.body.removeChild(script);
script.src = url + new URLSearchParams({
authed: Number(authed).toString(),
version: WEBSYNC_VERSION,
});
document.body.appendChild(script);
script.onload = () => {
saveSync(authed);
removeElement();
if (lastTimeout) {
clearTimeout(lastTimeout);
lastTimeout = undefined;
}
startWebsync();
resolve();
};
script.onerror = () => {
removeElement();
reject();
};
});
}));
} else {
return Promise.resolve();
}
};
export function startWebsync() {
if (lastTimeout !== undefined) return;
const currentTs = getTs();
const { ts } = JSON.parse(localStorage.getItem(WEBSYNC_KEY) || '{}');
const timeout = WEBSYNC_TIMEOUT - (currentTs - ts);
lastTimeout = setTimeout(() => {
const { authState } = getGlobal();
const authed = authState === 'authorizationStateReady' || hasStoredSession(true);
forceWebsync(authed);
}, Math.max(0, timeout * 1000));
}