Some notification fixes

Unread chats counter on mobile screens
Fix scrolling top peers on touch
Fix wrapping draft emojis
Fix avatar transition on swipe-to-reply
This commit is contained in:
Eduard Kuzmenko 2023-01-31 18:38:29 +04:00
parent c65ef02d49
commit 34bdf75789
21 changed files with 350 additions and 158 deletions

View File

@ -0,0 +1,11 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_0_3)">
<rect width="108" height="108" transform="translate(26 26)" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M80 0C124.183 0 160 35.8172 160 80C160 124.183 124.183 160 80 160C35.8172 160 0 124.183 0 80C0 35.8172 35.8172 0 80 0ZM114.263 46.4516H114.124C111.09 46.5056 106.483 48.0771 85.129 56.9377L81.4134 58.485C72.8665 62.0684 57.2608 68.7965 34.5963 78.6692C30.6592 80.2346 28.5967 81.7659 28.409 83.2633C28.0626 86.0254 31.8704 86.9599 36.789 88.5303L38.2643 89.0045C42.3926 90.3144 47.5535 91.7249 50.3251 91.7848C52.9152 91.8407 55.7945 90.8163 58.9629 88.7115L70.5122 80.9327C85.6657 70.7536 93.6286 65.5353 94.4008 65.2779L94.6778 65.2164C95.1594 65.1213 95.7366 65.0718 96.1481 65.4374C96.6344 65.8696 96.5866 66.688 96.5351 66.9076C96.1275 68.6449 75.2839 87.6143 73.6629 89.2418L73.3122 89.6017C68.7645 94.2255 63.9031 97.1721 71.5638 102.355L73.3594 103.545C79.066 107.335 82.9483 110.084 88.8108 113.958L90.3875 114.996C95.0655 118.062 98.733 121.698 103.563 121.253C105.741 121.053 107.989 119.042 109.175 113.097L109.247 112.728C112.002 98.0013 117.418 66.093 118.67 52.9444C118.779 51.7924 118.641 50.3181 118.53 49.6709L118.474 49.3782C118.341 48.7651 118.068 48.0041 117.347 47.419C116.413 46.6611 115.002 46.4639 114.263 46.4516Z" fill="#3390EC"/>
</g>
<defs>
<clipPath id="clip0_0_3">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,3 @@
<svg width="31" height="26" viewBox="0 0 31 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.56234 11.2029C10.6154 7.69429 15.9854 5.38122 18.6722 4.26366C26.3438 1.07279 27.9379 0.518498 28.9769 0.500195C29.2054 0.49617 29.7163 0.552803 30.0473 0.821365C30.3268 1.04813 30.4037 1.35446 30.4405 1.56947C30.4773 1.78447 30.5231 2.27425 30.4867 2.65694C30.0709 7.025 28.2721 17.6251 27.357 22.5174C26.9697 24.5875 26.2073 25.2816 25.4691 25.3496C23.8649 25.4972 22.6467 24.2894 21.093 23.2709C18.6617 21.6772 17.2882 20.685 14.9282 19.1298C12.2008 17.3325 13.9688 16.3447 15.5232 14.7303C15.9299 14.3078 22.9981 7.87881 23.1349 7.2956C23.152 7.22266 23.1679 6.95077 23.0063 6.80721C22.8448 6.66364 22.6064 6.71273 22.4344 6.75178C22.1905 6.80712 18.3065 9.37433 10.7823 14.4534C9.67983 15.2104 8.68124 15.5793 7.78655 15.5599C6.80022 15.5386 4.90291 15.0023 3.49246 14.5438C1.76249 13.9814 0.387541 13.6841 0.507268 12.7291C0.569629 12.2316 1.25465 11.7229 2.56234 11.2029Z" fill="#3390ec"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="m4356.97 71.3238c18.66-8.128 31.1-13.487 37.32-16.076 17.78-7.393 21.47-8.677 23.88-8.72.53-.009 1.71.122 2.48.745.64.525.82 1.235.91 1.733.08.498.19 1.633.1 2.519-.96 10.12-5.13 34.678-7.25 46.013-.89 4.7962-2.66 6.4042-4.37 6.5612-3.72.342-6.54-2.456-10.14-4.8152-5.63-3.693-8.81-5.991-14.28-9.594-6.32-4.164-2.22-6.453 1.38-10.193.94-.979 17.32-15.874 17.63-17.225.04-.169.08-.799-.3-1.131-.37-.333-.92-.219-1.32-.129-.57.128-9.56 6.076-27 17.843-2.55 1.754-4.86 2.609-6.94 2.564-2.28-.049-6.68-1.292-9.95-2.354-4-1.303-7.19-1.992-6.91-4.205.14-1.152 1.73-2.331 4.76-3.536z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -411,6 +411,9 @@ export default class AppSearchSuper {
unlockScroll = lockTouchScroll(this.tabsContainer);
this.selectTab(idx);
}
},
verifyTouchTarget: (e) => {
return !findUpClassName(e.target, 'scrollable-x');
}
});
}

View File

@ -900,13 +900,15 @@ export default class ChatBubbles {
}
} catch(err) {}
SetTransition({
element: target,
className,
forwards: true,
duration: 250
[target, swipeAvatar].filter(Boolean).forEach((element) => {
SetTransition({
element,
className,
forwards: true,
duration: 250
});
void element.offsetLeft; // reflow
});
void target.offsetLeft; // reflow
if(!icon) {
icon = document.createElement('span');
@ -941,17 +943,22 @@ export default class ChatBubbles {
const _target = target;
const _swipeAvatar = swipeAvatar;
target = swipeAvatar = undefined;
SetTransition({
element: _target,
className,
forwards: false,
duration: 250,
onTransitionEnd: () => {
if(icon.parentElement === _target) {
icon.classList.remove('is-visible');
icon.remove();
}
const onTransitionEnd = () => {
if(icon.parentElement === _target) {
icon.classList.remove('is-visible');
icon.remove();
}
};
[_target, _swipeAvatar].filter(Boolean).forEach((element, idx) => {
SetTransition({
element,
className,
forwards: false,
duration: 250,
onTransitionEnd: idx === 0 ? onTransitionEnd : undefined
});
});
fastRaf(() => {
@ -4595,7 +4602,7 @@ export default class ChatBubbles {
avatarElem.updateWithOptions({
lazyLoadQueue: this.lazyLoadQueue,
peerId: contact.user_id.toPeerId(),
peerTitle: fullName.trim() ? undefined : I18n.format('AttachContact', true)[0]
peerTitle: contact.user_id ? undefined : (fullName.trim() ? fullName : I18n.format('AttachContact', true)[0])
});
avatarElem.classList.add('contact-avatar', 'avatar-54');

View File

@ -17,7 +17,6 @@ import ButtonIcon from '../buttonIcon';
import ButtonMenuToggle from '../buttonMenuToggle';
import ChatAudio from './audio';
import ChatPinnedMessage from './pinnedMessage';
import {ButtonMenuItemOptions} from '../buttonMenu';
import ListenerSetter from '../../helpers/listenerSetter';
import PopupDeleteDialog from '../popups/deleteDialog';
import appNavigationController from '../appNavigationController';
@ -46,12 +45,15 @@ import wrapPeerTitle from '../wrappers/peerTitle';
import groupCallsController from '../../lib/calls/groupCallsController';
import apiManagerProxy from '../../lib/mtproto/mtprotoworker';
import {makeMediaSize} from '../../helpers/mediaSize';
import {FOLDER_ID_ALL} from '../../lib/mtproto/mtproto_config';
import formatNumber from '../../helpers/number/formatNumber';
type ButtonToVerify = {element?: HTMLElement, verify: () => boolean | Promise<boolean>};
export default class ChatTopbar {
public container: HTMLDivElement;
private btnBack: HTMLButtonElement;
private btnBackBadge: HTMLElement;
private chatInfo: HTMLDivElement;
private avatarElement: AvatarElement;
private title: HTMLDivElement;
@ -97,6 +99,9 @@ export default class ChatTopbar {
this.container.dataset.floating = '0';
this.btnBack = ButtonIcon('left sidebar-close-button', {noRipple: true});
this.btnBackBadge = document.createElement('span');
this.btnBackBadge.classList.add('badge', 'badge-20', 'badge-primary', 'back-unread-badge');
this.btnBack.append(this.btnBackBadge);
// * chat info section
this.chatInfoContainer = document.createElement('div');
@ -589,6 +594,17 @@ export default class ChatTopbar {
});
});
this.listenerSetter.add(rootScope)('folder_unread', (folder) => {
if(folder.id !== FOLDER_ID_ALL) {
return;
}
const size = folder.unreadUnmutedPeerIds.size;
this.btnBackBadge.textContent = size ? '' + formatNumber(size, 1) : '';
// this.btnBack.classList.remove('tgico-left', 'tgico-previous');
// this.btnBack.classList.add(size ? 'tgico-previous' : 'tgico-left');
});
this.listenerSetter.add(rootScope)('chat_update', async(chatId) => {
if(this.peerId === chatId.toPeerId(true)) {
const chat = await this.managers.appChatsManager.getChat(chatId) as Channel/* | Chat */;

View File

@ -15,6 +15,7 @@ import {MyDraftMessage} from '../../lib/appManagers/appDraftsManager';
import {MyMessage} from '../../lib/appManagers/appMessagesManager';
import isMessageRestricted from '../../lib/appManagers/utils/messages/isMessageRestricted';
import I18n, {LangPackKey, i18n, UNSUPPORTED_LANG_PACK_KEY} from '../../lib/langPack';
import parseEntities from '../../lib/richTextProcessor/parseEntities';
import sortEntities from '../../lib/richTextProcessor/sortEntities';
import wrapEmojiText from '../../lib/richTextProcessor/wrapEmojiText';
import wrapPlainText from '../../lib/richTextProcessor/wrapPlainText';
@ -221,9 +222,7 @@ export default async function wrapMessageForReply<T extends WrapMessageForReplyO
if(text) {
text = limitSymbols(text, 100);
if(!entities) {
entities = [];
}
entities ??= parseEntities(text);
if(plain) {
parts.push(wrapPlainText(text, entities));

View File

@ -0,0 +1,2 @@
export const NOTIFICATION_ICON_PATH = 'assets/img/logo_filled_rounded.png';
export const NOTIFICATION_BADGE_PATH = 'assets/img/logo_plain.svg'; // masked

View File

@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {MOUNT_CLASS_TO} from '../../config/debug';
import rootScope from '../../lib/rootScope';
export type CustomProperty = string;
@ -42,4 +43,5 @@ export class CustomProperties {
}
const customProperties = new CustomProperties();
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.customProperties = customProperties);
export default customProperties;

View File

@ -39,7 +39,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="assets/img/favicon-32x32.png?v=jw3mK7G9Ry">
<link rel="icon" type="image/png" sizes="192x192" href="assets/img/android-chrome-192x192.png?v=jw3mK7G9Ry">
<link rel="alternate icon" href="assets/img/favicon.ico?v=jw3mK7G9Ry" type="image/x-icon">
<link rel="mask-icon" href="assets/img/safari-pinned-tab.svg?v=jw3mK7G9Ry" color="#3390ec">
<link rel="mask-icon" href="assets/img/logo_filled.svg?v=jw3mK7G9Ry" color="#3390ec">
<link rel="canonical" href="{{htmlWebpackPlugin.options.origin}}">
<link rel="manifest" id="manifest">

View File

@ -4,13 +4,17 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type {PushNotificationObject} from '../serviceWorker/push';
import getPeerTitle from '../../components/wrappers/getPeerTitle';
import wrapMessageForReply from '../../components/wrappers/messageForReply';
import {MOUNT_CLASS_TO} from '../../config/debug';
import {FontFamily} from '../../config/font';
import {NOTIFICATION_BADGE_PATH, NOTIFICATION_ICON_PATH} from '../../config/notifications';
import {IS_MOBILE} from '../../environment/userAgent';
import IS_VIBRATE_SUPPORTED from '../../environment/vibrateSupport';
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
import drawCircle from '../../helpers/canvas/drawCircle';
import customProperties from '../../helpers/dom/customProperties';
import idleController from '../../helpers/idleController';
import deepEqual from '../../helpers/object/deepEqual';
import tsNow from '../../helpers/tsNow';
@ -20,6 +24,7 @@ import apiManagerProxy from '../mtproto/mtprotoworker';
import singleInstance from '../mtproto/singleInstance';
import webPushApiManager, {PushSubscriptionNotify} from '../mtproto/webPushApiManager';
import fixEmoji from '../richTextProcessor/fixEmoji';
import getAbbreviation from '../richTextProcessor/getAbbreviation';
import wrapPlainText from '../richTextProcessor/wrapPlainText';
import rootScope from '../rootScope';
import appImManager from './appImManager';
@ -27,7 +32,9 @@ import appRuntimeManager from './appRuntimeManager';
import {AppManagers} from './managers';
import generateMessageId from './utils/messageId/generateMessageId';
import getMessageThreadId from './utils/messages/getMessageThreadId';
import getPeerColorById from './utils/peers/getPeerColorById';
import getPeerId from './utils/peers/getPeerId';
import {logger} from '../logger';
type MyNotification = Notification & {
hidden?: boolean,
@ -85,9 +92,17 @@ export class UiNotificationsManager {
private managers: AppManagers;
private setAppBadge: (contents?: any) => Promise<void>;
private avatarCanvas: HTMLCanvasElement;
private avatarContext: CanvasRenderingContext2D;
private avatarGradients: {[color: string]: CanvasGradient};
private log: ReturnType<typeof logger>;
construct(managers: AppManagers) {
this.managers = managers;
this.log = logger('NOTIFICATIONS');
navigator.vibrate = navigator.vibrate || (navigator as any).mozVibrate || (navigator as any).webkitVibrate;
this.setAppBadge = (navigator as any).setAppBadge && (navigator as any).setAppBadge.bind(navigator);
this.setAppBadge && this.setAppBadge(0);
@ -197,12 +212,12 @@ export class UiNotificationsManager {
console.log('click', notificationData, peerId);
if(peerId) {
this.topMessagesDeferred.then(async() => {
if(notificationData.custom.channel_id &&
!(await this.managers.appChatsManager.hasChat(notificationData.custom.channel_id))) {
const chatId = peerId.isAnyChat() ? peerId.toChatId() : undefined;
if(chatId && !(await this.managers.appChatsManager.hasChat(chatId))) {
return;
}
if(peerId.isUser() && !(await this.managers.appUsersManager.hasUser(peerId))) {
if(peerId.isUser() && !(await this.managers.appUsersManager.hasUser(peerId.toUserId()))) {
return;
}
@ -268,19 +283,24 @@ export class UiNotificationsManager {
notification.silent = true;
}
const peerTitleOptions/* : Partial<Parameters<typeof getPeerTitle>[0]> */ = {
plainText: true as const,
managers: this.managers
};
const threadId = isForum ? getMessageThreadId(message, isForum) : undefined;
const notificationFromPeerId = peerReaction ? getPeerId(peerReaction.peer_id) : message.fromId;
notification.title = await getPeerTitle({peerId, plainText: true, managers: this.managers, threadId: threadId});
const peerTitle = notification.title = await getPeerTitle({...peerTitleOptions, peerId, threadId: threadId});
if(isForum) {
const peerTitle = await getPeerTitle({peerId, plainText: true, managers: this.managers});
const peerTitle = await getPeerTitle({...peerTitleOptions, peerId});
notification.title += ` (${peerTitle})`;
if(wrappedMessage && notificationFromPeerId !== message.peerId) {
notificationMessage = await getPeerTitle({peerId: notificationFromPeerId, plainText: true, managers: this.managers}) +
notificationMessage = await getPeerTitle({...peerTitleOptions, peerId: notificationFromPeerId}) +
': ' + notificationMessage;
}
} else if(isAnyChat && notificationFromPeerId !== message.peerId) {
notification.title = await getPeerTitle({peerId: notificationFromPeerId, plainText: true, managers: this.managers}) +
notification.title = await getPeerTitle({...peerTitleOptions, peerId: notificationFromPeerId}) +
' @ ' +
notification.title;
}
@ -298,15 +318,75 @@ export class UiNotificationsManager {
const peerPhoto = await this.managers.appPeersManager.getPeerPhoto(peerId);
if(peerPhoto) {
this.managers.appAvatarsManager.loadAvatar(peerId, peerPhoto, 'photo_small').then((url) => {
// ! WARNING, message can be already read
if(message.pFlags.unread || peerReaction) {
notification.image = url;
this.notify(notification);
}
});
const url = await this.managers.appAvatarsManager.loadAvatar(peerId, peerPhoto, 'photo_small');
if(!peerReaction) { // ! WARNING, message can be already read
message = await this.managers.appMessagesManager.getMessageByPeer(message.peerId, message.mid);
if(!message || !message.pFlags.unread) return;
}
notification.image = url;
} else {
this.notify(notification);
const WIDTH = 54, HEIGHT = WIDTH;
let {avatarCanvas, avatarContext} = this;
if(!this.avatarCanvas) {
avatarCanvas = this.avatarCanvas = document.createElement('canvas');
avatarContext = this.avatarContext = avatarCanvas.getContext('2d');
const dpr = window.devicePixelRatio;
avatarCanvas.dpr = dpr;
avatarCanvas.width = 54 * dpr;
avatarCanvas.height = 54 * dpr;
this.avatarGradients = {};
} else {
avatarContext.clearRect(0, 0, avatarCanvas.width, avatarCanvas.height);
}
const color = getPeerColorById(peerId, true);
let gradient = this.avatarGradients[color];
if(!gradient) {
gradient = this.avatarGradients[color] = avatarContext.createLinearGradient(WIDTH / 2, 0, WIDTH / 2, HEIGHT);
const colorTop = customProperties.getProperty(`peer-avatar-${color}-top`);
const colorBottom = customProperties.getProperty(`peer-avatar-${color}-bottom`);
gradient.addColorStop(0, colorTop);
gradient.addColorStop(1, colorBottom);
}
avatarContext.fillStyle = gradient;
drawCircle(avatarContext, WIDTH / 2, HEIGHT / 2, WIDTH / 2);
avatarContext.fill();
const fontSize = 20 * window.devicePixelRatio;
const abbreviation = getAbbreviation(peerTitle);
avatarContext.font = `700 ${fontSize}px ${FontFamily}`;
avatarContext.textBaseline = 'middle';
avatarContext.textAlign = 'center';
avatarContext.fillStyle = 'white';
avatarContext.fillText(abbreviation.text, avatarCanvas.width / 2, avatarCanvas.height * .5625);
notification.image = avatarCanvas.toDataURL();
}
const pushData: PushNotificationObject = {
custom: {
msg_id: '' + message.mid,
peerId: '' + peerId
},
description: '',
loc_key: '',
loc_args: [],
mute: '',
random_id: 0,
title: ''
};
const result = await this.notify(notification, pushData);
if(result && this.registeredDevice) {
webPushApiManager.ignorePushByMid(peerId, message.mid);
}
}
@ -390,9 +470,7 @@ export class UiNotificationsManager {
this.faviconElements.forEach((element, idx, arr) => {
const link = element.cloneNode() as HTMLLinkElement;
if(!link.dataset.href) {
link.dataset.href = link.href;
}
link.dataset.href ||= link.href;
href ??= link.dataset.href;
link.href = href;
@ -400,32 +478,14 @@ export class UiNotificationsManager {
});
}
public notify(data: NotifyOptions) {
// console.log('notify', data, rootScope.idle.isIDLE, this.notificationsUiSupport, this.stopped);
public async notify(data: NotifyOptions, pushData: PushNotificationObject) {
this.log('notify', data, idleController.isIdle, this.notificationsUiSupport, this.stopped);
if(this.stopped) {
return;
}
// FFOS Notification blob src bug workaround
/* if(Config.Navigator.ffos && !Config.Navigator.ffos2p) {
data.image = 'https://telegram.org/img/t_logo.png'
}
else if (data.image && !angular.isString(data.image)) {
if (Config.Navigator.ffos2p) {
FileManager.getDataUrl(data.image, 'image/jpeg').then(function (url) {
data.image = url
notify(data)
})
return false
} else {
data.image = FileManager.getUrl(data.image, 'image/jpeg')
}
}
else */ if(!data.image) {
data.image = 'assets/img/logo_filled_rounded.png';
}
// console.log('notify image', data.image)
data.image ||= NOTIFICATION_ICON_PATH;
if(!data.noIncrement) {
++this.notificationsCount;
@ -453,7 +513,7 @@ export class UiNotificationsManager {
if(!this.notificationsUiSupport ||
'Notification' in window && Notification.permission !== 'granted') {
return false;
return;
}
if(this.settings.nodesktop) {
@ -465,64 +525,67 @@ export class UiNotificationsManager {
return;
}
if(!('Notification' in window)) {
return;
}
let notification: MyNotification;
if('Notification' in window) {
try {
if(data.tag) {
for(const i in this.notificationsShown) {
const notification = this.notificationsShown[i];
if(typeof(notification) !== 'boolean' && notification.tag === data.tag) {
notification.hidden = true;
}
const notificationOptions: NotificationOptions = {
badge: NOTIFICATION_BADGE_PATH,
icon: data.image || '',
body: data.message || '',
tag: data.tag || '',
silent: data.silent || false,
data: pushData
};
try {
if(data.tag) {
for(const i in this.notificationsShown) {
const notification = this.notificationsShown[i];
if(typeof(notification) !== 'boolean' && notification.tag === data.tag) {
notification.hidden = true;
}
}
}
notification = new Notification(data.title, {
icon: data.image || '',
body: data.message || '',
tag: data.tag || '',
silent: data.silent || false
});
// throw new Error('test');
notification = new Notification(data.title, notificationOptions);
} catch(e) {
try {
const registration = await navigator.serviceWorker.ready;
await registration.showNotification(data.title, notificationOptions);
const notifications = await registration.getNotifications({tag: notificationOptions.tag});
notification = notifications[notifications.length - 1];
} catch(err) {
this.log.error('creating push error', err, data, notificationOptions);
}
// console.log('notify constructed notification');
} catch(e) {
if(!notification) {
this.notificationsUiSupport = false;
webPushApiManager.setLocalNotificationsDisabled();
return;
}
} /* else if('mozNotification' in navigator) {
notification = navigator.mozNotification.createNotification(data.title, data.message || '', data.image || '')
} else if(notificationsMsSiteMode) {
window.external.msSiteModeClearIconOverlay()
window.external.msSiteModeSetIconOverlay('img/icons/icon16.png', data.title)
window.external.msSiteModeActivate()
notification = {
index: idx
}
} */ else {
return;
}
notification.onclick = () => {
this.log('notification onclick');
notification.close();
appRuntimeManager.focus();
this.clear();
if(data.onclick) {
data.onclick();
}
data.onclick?.();
};
notification.onclose = () => {
this.log('notification onclose');
if(!notification.hidden) {
delete this.notificationsShown[key];
this.clear();
}
};
if(notification.show) {
notification.show();
}
notification.show?.();
this.notificationsShown[key] = notification;
if(!IS_MOBILE) {
@ -530,6 +593,8 @@ export class UiNotificationsManager {
this.hide(key);
}, 8000);
}
return true;
}
public updateLocalSettings = () => {
@ -570,13 +635,8 @@ export class UiNotificationsManager {
private hide(key: string) {
const notification = this.notificationsShown[key];
if(notification && typeof(notification) !== 'boolean') {
try {
if(notification.close) {
notification.hidden = true;
notification.close();
}
} catch(e) {}
if(notification) {
this.closeNotification(notification);
}
}
@ -615,38 +675,35 @@ export class UiNotificationsManager {
public cancel(key: string) {
const notification = this.notificationsShown[key];
this.log('cancel', key, notification);
if(notification) {
if(this.notificationsCount > 0) {
--this.notificationsCount;
}
try {
if(typeof(notification) !== 'boolean' && notification.close) {
notification.hidden = true;
notification.close();
}/* else if(notificationsMsSiteMode &&
notification.index === notificationIndex) {
window.external.msSiteModeClearIconOverlay()
} */
} catch(e) {}
this.closeNotification(notification);
delete this.notificationsShown[key];
}
}
private closeNotification(notification: boolean | MyNotification) {
try {
if(typeof(notification) !== 'boolean' && notification.close) {
this.log('close notification', notification);
notification.hidden = true;
notification.close();
}
} catch(e) {}
}
public clear() {
/* if(notificationsMsSiteMode) {
window.external.msSiteModeClearIconOverlay()
} else { */
this.log.warn('clear');
for(const i in this.notificationsShown) {
const notification = this.notificationsShown[i];
try {
if(typeof(notification) !== 'boolean' && notification.close) {
notification.close();
}
} catch(e) {}
this.closeNotification(notification);
}
/* } */
this.notificationsShown = {};
this.notificationsCount = 0;
@ -654,6 +711,8 @@ export class UiNotificationsManager {
}
public start() {
this.log('start');
this.updateLocalSettings();
rootScope.addEventListener('settings_updated', this.updateLocalSettings);
webPushApiManager.start();
@ -674,6 +733,8 @@ export class UiNotificationsManager {
}
private stop() {
this.log('stop');
this.clear();
window.clearInterval(this.titleInterval);
this.titleInterval = 0;
@ -693,6 +754,7 @@ export class UiNotificationsManager {
app_sandbox: false,
secret: new Uint8Array()
}).then(() => {
this.log('registered device');
this.registeredDevice = tokenData;
}, (error) => {
error.handled = true;

View File

@ -253,13 +253,15 @@ export default class IDBStorage<T extends Database<any>, StoreName extends strin
public delete(entryName: string | string[], storeName?: StoreName): Promise<void> {
// return Promise.resolve();
if(!Array.isArray(entryName)) {
const isArray = Array.isArray(entryName);
if(!isArray) {
entryName = [].concat(entryName);
}
return this.getObjectStore('readwrite', (objectStore) => {
return (entryName as string[]).map((entryName) => objectStore.delete(entryName));
}, DEBUG ? 'delete: ' + entryName.join(', ') : '', storeName);
const promises = (entryName as string[]).map((entryName) => objectStore.delete(entryName));
return isArray ? promises : promises[0];
}, DEBUG ? 'delete: ' + (entryName as string[]).join(', ') : '', storeName);
}
public clear(storeName?: StoreName): Promise<void> {
@ -278,14 +280,16 @@ export default class IDBStorage<T extends Database<any>, StoreName extends strin
// }
// };
if(!Array.isArray(entryName)) {
const isArray = Array.isArray(entryName);
if(!isArray) {
entryName = [].concat(entryName);
value = [].concat(value);
}
return this.getObjectStore('readwrite', (objectStore) => {
return (entryName as string[]).map((entryName, idx) => objectStore.put(value[idx], entryName));
}, DEBUG ? 'save: ' + entryName.join(', ') : '', storeName);
const promises = (entryName as string[]).map((entryName, idx) => objectStore.put(value[idx], entryName));
return isArray ? promises : promises[0];
}, DEBUG ? 'save: ' + (entryName as string[]).join(', ') : '', storeName);
}
// public saveFile(fileName: string, blob: Blob | Uint8Array) {
@ -366,17 +370,21 @@ export default class IDBStorage<T extends Database<any>, StoreName extends strin
public get<T>(entryName: string | string[], storeName?: StoreName): Promise<T> | Promise<T[]> {
// return Promise.reject();
if(!Array.isArray(entryName)) {
entryName = [].concat(entryName);
}
const isArray = Array.isArray(entryName);
if(!isArray) {
if(!entryName) {
return undefined;
}
if(!entryName.length) {
entryName = [].concat(entryName);
} else if(!entryName.length) {
return Promise.resolve([]) as any;
}
return this.getObjectStore<T>('readonly', (objectStore) => {
return (entryName as string[]).map((entryName) => objectStore.get(entryName));
}, DEBUG ? 'get: ' + entryName.join(', ') : '', storeName);
const promises = (entryName as string[]).map((entryName) => objectStore.get(entryName));
return isArray ? promises : promises[0];
}, DEBUG ? 'get: ' + (entryName as string[]).join(', ') : '', storeName);
}
private getObjectStore<T>(

View File

@ -21,6 +21,7 @@ import appRuntimeManager from '../appManagers/appRuntimeManager';
import copy from '../../helpers/object/copy';
import singleInstance from './singleInstance';
import EventListenerBase from '../../helpers/eventListenerBase';
import getServerMessageId from '../appManagers/utils/messageId/getServerMessageId';
export type PushSubscriptionNotifyType = 'init' | 'subscribe' | 'unsubscribe';
export type PushSubscriptionNotifyEvent = `push_${PushSubscriptionNotifyType}`;
@ -248,6 +249,14 @@ export class WebPushApiManager extends EventListenerBase<{
this.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, false as any);
}
}
public ignorePushByMid(peerId: PeerId, mid: number) {
if(!this.isAvailable) {
return;
}
apiManagerProxy.serviceMessagePort.invokeVoid('shownNotification', peerId + '_' + getServerMessageId(mid));
}
}
const webPushApiManager = new WebPushApiManager();

View File

@ -11,7 +11,7 @@ import '../mtproto/mtproto.worker';
import {logger, LogTypes} from '../logger';
import {CACHE_ASSETS_NAME, requestCache} from './cache';
import onStreamFetch from './stream';
import {closeAllNotifications, onPing} from './push';
import {closeAllNotifications, onPing, onShownNotification} from './push';
import CacheStorageController from '../files/cacheStorage';
import {IS_SAFARI} from '../../environment/userAgent';
import ServiceMessagePort from './serviceMessagePort';
@ -71,7 +71,9 @@ serviceMessagePort.addMultipleEventsListeners({
hello: (payload, source) => {
onWindowConnected(source as any as WindowClient);
}
},
shownNotification: onShownNotification
});
const {

View File

@ -11,6 +11,7 @@
import {Database} from '../../config/databases';
import DATABASE_STATE from '../../config/databases/state';
import {NOTIFICATION_BADGE_PATH, NOTIFICATION_ICON_PATH} from '../../config/notifications';
import {IS_FIREFOX, IS_MOBILE} from '../../environment/userAgent';
import deepEqual from '../../helpers/object/deepEqual';
import IDBStorage from '../files/idb';
@ -23,7 +24,7 @@ const defaultBaseUrl = location.protocol + '//' + location.hostname + location.p
// as in webPushApiManager.ts
const PING_PUSH_TIMEOUT = 10000 + 1500;
let lastPingTime = 0;
let localNotificationsAvailable = !IS_MOBILE;
let localNotificationsAvailable = true;
export type PushNotificationObject = {
loc_key: string,
@ -44,7 +45,7 @@ export type PushNotificationObject = {
mute: string, // should be number
title: string,
message?: string,
} & {
action?: 'mute1d' | 'push_settings', // will be set before postMessage to main thread
};
@ -137,10 +138,6 @@ ctx.addEventListener('push', (event) => {
log('push', {...obj});
try {
if(!obj.badge) {
throw 'no badge';
}
const [muteUntil, settings, lang] = [
getter.getCached('push_mute_until'),
getter.getCached('push_settings'),
@ -153,24 +150,33 @@ ctx.addEventListener('push', (event) => {
muteUntil &&
nowTime < muteUntil
) {
throw `Supress notification because mute for ${Math.ceil((muteUntil - nowTime) / 60000)} min`;
throw `supress notification because mute for ${Math.ceil((muteUntil - nowTime) / 60000)} min`;
}
const hasActiveWindows = (Date.now() - lastPingTime) <= PING_PUSH_TIMEOUT && localNotificationsAvailable;
if(hasActiveWindows) {
throw 'Supress notification because some instance is alive';
throw 'supress notification because some instance is alive';
}
const notificationPromise = fireNotification(obj, settings, lang);
event.waitUntil(notificationPromise);
} catch(err) {
log(err);
const tag = 'fix';
const notificationPromise = ctx.registration.showNotification('Telegram', {tag});
notificationPromise.then(() => {
closeAllNotifications(tag);
});
event.waitUntil(notificationPromise);
}
});
ctx.addEventListener('notificationclick', (event) => {
const notification = event.notification;
log('On notification click: ', notification.tag);
log('on notification click', notification);
notification.close();
const action = event.action as PushNotificationObject['action'];
@ -190,7 +196,7 @@ ctx.addEventListener('notificationclick', (event) => {
}).then((clientList) => {
data.action = action;
pendingNotification = data;
for(let i = 0; i < clientList.length; i++) {
for(let i = 0; i < clientList.length; ++i) {
const client = clientList[i];
if('focus' in client) {
client.focus();
@ -235,7 +241,12 @@ function removeFromNotifications(notification: Notification) {
export function closeAllNotifications(tag?: string) {
for(const notification of notifications) {
try {
if(tag && notification.tag !== tag) {
continue;
}
notification.close();
notifications.delete(notification);
} catch(e) {}
}
@ -254,8 +265,6 @@ export function closeAllNotifications(tag?: string) {
promise = Promise.resolve();
}
notifications.clear();
return promise;
}
@ -264,8 +273,6 @@ function userInvisibleIsSupported() {
}
function fireNotification(obj: PushNotificationObject, settings: PushStorage['push_settings'], lang: PushStorage['push_lang']) {
const icon = 'assets/img/logo_filled_rounded.png';
const badge = 'assets/img/masked.svg';
let title = obj.title || 'Telegram';
let body = obj.description || '';
let peerId: string;
@ -283,14 +290,20 @@ function fireNotification(obj: PushNotificationObject, settings: PushStorage['pu
obj.custom.peerId = '' + peerId;
let tag = 'peer' + peerId;
const messageKey = peerId + '_' + obj.custom.msg_id;
if(ignoreMessages) {
const error = 'ignoring push';
log.warn(error, obj);
ignoreMessages.delete(messageKey);
throw error;
}
if(settings?.nopreview) {
title = 'Telegram';
body = lang.push_message_nopreview;
tag = 'unknown_peer';
}
log('show notify', title, body, icon, obj);
const actions: (Omit<NotificationAction, 'action'> & {action: PushNotificationObject['action']})[] = [{
action: 'mute1d',
title: lang.push_action_mute1d
@ -299,15 +312,19 @@ function fireNotification(obj: PushNotificationObject, settings: PushStorage['pu
title: lang.push_action_settings || 'Settings'
} */];
const notificationPromise = ctx.registration.showNotification(title, {
const notificationOptions: NotificationOptions = {
body,
icon,
icon: NOTIFICATION_ICON_PATH,
tag,
data: obj,
actions,
badge,
badge: NOTIFICATION_BADGE_PATH,
silent: obj.custom.silent === '1'
});
};
log('show notify', title, body, obj, notificationOptions);
const notificationPromise = ctx.registration.showNotification(title, notificationOptions);
return notificationPromise.catch((error) => {
log.error('Show notification promise', error);
@ -331,3 +348,8 @@ export function onPing(payload: ServicePushPingTaskPayload, source?: MessageEven
getter.set('push_settings', payload.settings);
}
}
const ignoreMessages: Set<string> = new Set();
export function onShownNotification(payload: string) {
ignoreMessages.add(payload);
}

View File

@ -42,6 +42,7 @@ export default class ServiceMessagePort<Master extends boolean = false> extends
toggleStorages: (payload: {enabled: boolean, clearWrite: boolean}) => void,
pushPing: (payload: ServicePushPingTaskPayload, source: MessageEventSource, event: MessageEvent) => void,
hello: (payload: void, source: MessageEventSource, event: MessageEvent) => void,
shownNotification: (payload: string) => void,
// from mtproto worker
download: (payload: ServiceDownloadTaskPayload) => void,

View File

@ -580,7 +580,9 @@ $chat-input-border-radius: 1rem;
transform: translate3d(26.5625rem, 0, 0); // + 1px to show left border
.sidebar-close-button {
transform: rotate(180deg);
&:before {
transform: rotate(180deg);
}
}
}
}

View File

@ -68,6 +68,14 @@ $bubble-border-radius-big: 12px;
transform-origin: bottom;
}
&.is-gesturing-reply {
transform: translateX(0);
&.animating.backwards {
transition: transform var(--transition-standard-out);
}
}
&-container {
position: absolute;
top: 0;

View File

@ -452,4 +452,14 @@ body.is-right-column-shown {
}
}
}
.back-unread-badge {
position: absolute;
top: -.25rem;
right: -.5rem;
@include respond-to(medium-screens) {
display: none;
}
}
}

View File

@ -371,6 +371,7 @@
align-items: center;
flex: 0 0 auto;
padding: 0 !important;
border-radius: $border-radius-medium;
}
}

View File

@ -9,10 +9,20 @@
display: flex;
height: 6.5rem;
position: relative;
margin: 0 -.5rem;
width: calc(100% + 1rem);
align-items: center;
@include respond-to(not-handhelds) {
width: calc(100% + 1rem);
margin: 0 -.5rem;
padding: 0;
}
// @include respond-to(handhelds) {
// &:after {
// width: .25rem !important;
// }
// }
&:before,
&:after {
content: " ";