diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 48de65c9..2b4b7259 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -39,6 +39,8 @@ import { forEachReverse } from "../helpers/array"; import AppSharedMediaTab from "./sidebarRight/tabs/sharedMedia"; import findUpClassName from "../helpers/dom/findUpClassName"; import renderImageFromUrl from "../helpers/dom/renderImageFromUrl"; +import findUpAsChild from "../helpers/dom/findUpAsChild"; +import getVisibleRect from "../helpers/dom/getVisibleRect"; // TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию // TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода) @@ -332,10 +334,6 @@ class AppMediaViewerBase setTimeout(resolve, 0)); //await new Promise((resolve) => window.requestAnimationFrame(resolve)); // * одного RAF'а недостаточно, иногда анимация с одним не срабатывает (преимущественно на мобильных) @@ -624,6 +654,7 @@ class AppMediaViewerBase { if(!media.url) { - this.preloader.attach(mover, true, cancellablePromise); + this.preloader.attachPromise(cancellablePromise); + //this.preloader.attach(mover, true, cancellablePromise); } }); @@ -1157,9 +1189,11 @@ class AppMediaViewerBase { this.log.error(err); + this.preloader.attach(mover); + this.preloader.setManual(); }); return cancellablePromise; diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index b45cdc6c..7d3421d7 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -5,7 +5,7 @@ */ import { formatDateAccordingToToday, months } from "../helpers/date"; -import { positionElementByIndex, isInDOM, replaceContent } from "../helpers/dom"; +import { positionElementByIndex } from "../helpers/dom"; import { copy, getObjectKeysAndSort, safeAssign } from "../helpers/object"; import { escapeRegExp, limitSymbols } from "../helpers/string"; import appChatsManager from "../lib/appManagers/appChatsManager"; @@ -124,7 +124,7 @@ export default class AppSearchSuper { this.container.classList.add('search-super'); const navScrollableContainer = document.createElement('div'); - navScrollableContainer.classList.add('search-super-tabs-scrollable', 'menu-horizontal-scrollable'); + navScrollableContainer.classList.add('search-super-tabs-scrollable', 'menu-horizontal-scrollable', 'sticky'); const navScrollable = new ScrollableX(navScrollableContainer); diff --git a/src/components/avatar.ts b/src/components/avatar.ts index 6c158e1b..ed1d1555 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -9,7 +9,7 @@ import appProfileManager from "../lib/appManagers/appProfileManager"; import rootScope from "../lib/rootScope"; import { attachClickEvent, cancelEvent } from "../helpers/dom"; import AppMediaViewer, { AppMediaViewerAvatar } from "./appMediaViewer"; -import { Photo } from "../layer"; +import { Message, Photo } from "../layer"; import appPeersManager from "../lib/appManagers/appPeersManager"; //import type { LazyLoadQueueIntersector } from "./lazyLoadQueue"; @@ -21,8 +21,72 @@ const onAvatarUpdate = (peerId: number) => { }); }; -rootScope.on('avatar_update', onAvatarUpdate); -rootScope.on('peer_title_edit', onAvatarUpdate); +rootScope.on('avatar_update', onAvatarUpdate); +rootScope.on('peer_title_edit', onAvatarUpdate); + +export async function openAvatarViewer(target: HTMLElement, peerId: number, middleware: () => boolean, message?: any, prevTargets?: {element: HTMLElement, item: string | Message.messageService}[], nextTargets?: typeof prevTargets) { + const photo = await appProfileManager.getFullPhoto(peerId); + if(!middleware() || !photo) { + return; + } + + const getTarget = () => { + const good = Array.from(target.querySelectorAll('img')).find(img => !img.classList.contains('emoji')); + return good ? target : null; + }; + + if(peerId < 0) { + const hadMessage = !!message; + const inputFilter = 'inputMessagesFilterChatPhotos'; + if(!message) { + message = await appMessagesManager.getSearch({ + peerId, + inputFilter: {_: inputFilter}, + maxId: 0, + limit: 1 + }).then(value => { + //console.log(lol); + // ! by descend + return value.history[0]; + }); + + if(!middleware()) { + return; + } + } + + if(message) { + // ! гений в деле, костылируем (но это гениально) + const messagePhoto = message.action.photo; + if(messagePhoto.id !== photo.id) { + if(!hadMessage) { + message = appMessagesManager.generateFakeAvatarMessage(peerId, photo); + } else { + + } + } + + const f = (arr: typeof prevTargets) => arr.map(el => ({ + element: el.element, + mid: (el.item as Message.messageService).mid, + peerId: (el.item as Message.messageService).peerId + })); + + new AppMediaViewer() + .setSearchContext({ + peerId, + inputFilter, + }) + .openMedia(message, getTarget(), undefined, undefined, prevTargets ? f(prevTargets) : undefined, nextTargets ? f(nextTargets) : undefined); + + return; + } + } + + if(photo) { + new AppMediaViewerAvatar(peerId).openMedia(photo.id, getTarget()); + } +} export default class AvatarElement extends HTMLElement { private peerId: number; @@ -51,64 +115,7 @@ export default class AvatarElement extends HTMLElement { //console.log('avatar clicked'); const peerId = this.peerId; loading = true; - - const photo = await appProfileManager.getFullPhoto(this.peerId); - if(this.peerId !== peerId || !photo) { - loading = false; - return; - } - - if(peerId < 0) { - const maxId = Number.MAX_SAFE_INTEGER; - const inputFilter = 'inputMessagesFilterChatPhotos'; - let message: any = await appMessagesManager.getSearch({ - peerId, - inputFilter: {_: inputFilter}, - maxId, - limit: 2, - backLimit: 1 - }).then(value => { - //console.log(lol); - // ! by descend - return value.history[0]; - }); - - if(message) { - // ! гений в деле, костылируем (но это гениально) - const messagePhoto = message.action.photo; - if(messagePhoto.id !== photo.id) { - message = { - _: 'message', - mid: maxId, - media: { - _: 'messageMediaPhoto', - photo: photo - }, - peerId, - date: (photo as Photo.photo).date, - fromId: peerId - }; - - appMessagesManager.getMessagesStorage(peerId)[maxId] = message; - } - - const good = Array.from(this.querySelectorAll('img')).find(img => !img.classList.contains('emoji')); - new AppMediaViewer() - .setSearchContext({ - peerId, - inputFilter, - }) - .openMedia(message, good ? this : null); - loading = false; - return; - } - } - - if(photo) { - const good = Array.from(this.querySelectorAll('img')).find(img => !img.classList.contains('emoji')); - new AppMediaViewerAvatar(peerId).openMedia(photo.id, good ? this : null); - } - + await openAvatarViewer(this, this.peerId, () => this.peerId === peerId); loading = false; }); } diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 07d00a31..9548e1b5 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -4,7 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ -import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; +import type { AppImManager } from "../../lib/appManagers/appImManager"; import type { AppMessagesManager, HistoryResult, MyMessage } from "../../lib/appManagers/appMessagesManager"; import type { AppStickersManager } from "../../lib/appManagers/appStickersManager"; import type { AppUsersManager } from "../../lib/appManagers/appUsersManager"; @@ -14,6 +14,7 @@ import type { AppDocsManager } from "../../lib/appManagers/appDocsManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type sessionStorage from '../../lib/sessionStorage'; import type Chat from "./chat"; +import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; import { cancelEvent, whichChild, getElementByPoint, attachClickEvent, positionElementByIndex, reflowScrollableElement } from "../../helpers/dom"; import { getObjectKeysAndSort } from "../../helpers/object"; import { isTouchSupported } from "../../helpers/touchSupport"; @@ -28,7 +29,6 @@ import ProgressivePreloader from "../preloader"; import Scrollable from "../scrollable"; import StickyIntersector from "../stickyIntersector"; import animationIntersector from "../animationIntersector"; -import { months } from "../../helpers/date"; import RichTextProcessor from "../../lib/richtextprocessor"; import mediaSizes from "../../helpers/mediaSizes"; import { isAndroid, isApple, isSafari } from "../../helpers/userAgent"; @@ -1415,15 +1415,15 @@ export default class ChatBubbles { topMessage = 0; } - let readMaxId = 0;//, savedPosition: ReturnType; + let readMaxId = 0, savedPosition: ReturnType; if(!isTarget) { - /* if(!samePeer) { + if(!samePeer) { savedPosition = this.chat.appImManager.getChatSavedPosition(this.chat); } if(savedPosition) { - lastMsgId = savedPosition.mid; - } else */if(topMessage) { + + } else if(topMessage) { readMaxId = this.appMessagesManager.getReadMaxIdIfUnread(peerId, this.chat.threadId); if(/* dialog.unread_count */readMaxId && !samePeer) { lastMsgId = readMaxId; @@ -1497,7 +1497,19 @@ export default class ChatBubbles { this.lazyLoadQueue.lock(); - const {promise, cached} = this.getHistory(lastMsgId, true, isJump, additionMsgId); + let result: ReturnType; + if(!savedPosition) { + result = this.getHistory(lastMsgId, true, isJump, additionMsgId); + } else { + result = { + promise: getHeavyAnimationPromise().then(() => { + return this.performHistoryResult(savedPosition.mids, true, false, undefined); + }) as any, + cached: true + }; + } + + const {promise, cached} = result; // clear if(!cached) { @@ -1533,8 +1545,9 @@ export default class ChatBubbles { this.lazyLoadQueue.unlock(); //if(dialog && lastMsgID && lastMsgID !== topMessage && (this.bubbles[lastMsgID] || this.firstUnreadBubble)) { - /* if(savedPosition) { - const mountedByLastMsgId = this.getMountedBubble(lastMsgId); + if(savedPosition) { + this.scrollable.scrollTop = savedPosition.top; + /* const mountedByLastMsgId = this.getMountedBubble(lastMsgId); let bubble: HTMLElement = mountedByLastMsgId?.bubble; if(!bubble?.parentElement) { bubble = this.findNextMountedBubbleByMsgId(lastMsgId); @@ -1544,8 +1557,8 @@ export default class ChatBubbles { const top = bubble.getBoundingClientRect().top; const distance = savedPosition.top - top; this.scrollable.scrollTop += distance; - } - } else */if((topMessage && isJump) || isTarget) { + } */ + } else if((topMessage && isJump) || isTarget) { const fromUp = maxBubbleId > 0 && (maxBubbleId < lastMsgId || lastMsgId < 0); const followingUnread = readMaxId === lastMsgId && !isTarget; if(!fromUp && samePeer) { diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index 7f09f3a8..57578b6f 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -251,8 +251,10 @@ export default class Chat extends EventListenerBase<{ } }); - appSidebarRight.sharedMediaTab.setLoadMutex(this.setPeerPromise); - appSidebarRight.sharedMediaTab.loadSidebarMedia(true); + if(!samePeer) { + appSidebarRight.sharedMediaTab.setLoadMutex(this.setPeerPromise); + appSidebarRight.sharedMediaTab.loadSidebarMedia(true); + } /* this.setPeerPromise.then(() => { appSidebarRight.sharedMediaTab.loadSidebarMedia(false); }); */ diff --git a/src/components/chat/dragAndDrop.ts b/src/components/chat/dragAndDrop.ts index 048edffb..85feec49 100644 --- a/src/components/chat/dragAndDrop.ts +++ b/src/components/chat/dragAndDrop.ts @@ -5,6 +5,7 @@ */ import { generatePathData } from "../../helpers/dom"; +import { i18n, LangPackKey } from "../../lib/langPack"; export default class ChatDragAndDrop { container: HTMLDivElement; @@ -14,8 +15,8 @@ export default class ChatDragAndDrop { constructor(appendTo: HTMLElement, private options: { icon: string, - header: string, - subtitle: string, + header: LangPackKey, + subtitle: LangPackKey, onDrop: (e: DragEvent) => void }) { this.container = document.createElement('div'); @@ -35,11 +36,11 @@ export default class ChatDragAndDrop { const dropHeader = document.createElement('div'); dropHeader.classList.add('drop-header'); - dropHeader.innerHTML = options.header;//'Drop files here to send them'; + dropHeader.append(i18n(options.header)); const dropSubtitle = document.createElement('div'); dropSubtitle.classList.add('drop-subtitle'); - dropSubtitle.innerHTML = options.subtitle;//'without compression'; + dropSubtitle.append(i18n(options.subtitle)); this.svg.append(this.path); this.outlineWrapper.append(this.svg); diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index b4877191..75942dba 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -332,7 +332,9 @@ export default class ChatInput { this.newMessageWrapper.append(...[this.btnToggleEmoticons, this.inputMessageContainer, this.btnScheduled, this.attachMenu, this.recordTimeEl, this.fileInput].filter(Boolean)); - this.rowsWrapper.append(this.replyElements.container, this.newMessageWrapper); + this.rowsWrapper.append(this.replyElements.container); + this.stickersHelper = new StickersHelper(this.rowsWrapper); + this.rowsWrapper.append(this.newMessageWrapper); this.btnCancelRecord = ButtonIcon('delete danger btn-circle z-depth-1 btn-record-cancel'); @@ -389,8 +391,6 @@ export default class ChatInput { } }, {passive: false, capture: true}); */ - this.stickersHelper = new StickersHelper(this.rowsWrapper); - this.listenerSetter.add(rootScope, 'settings_updated', () => { if(this.stickersHelper) { if(!rootScope.settings.stickers.suggest) { diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index f1bc6cb6..a085b57a 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -179,10 +179,12 @@ export default class ChatSelection { checkboxField.label.classList.add('bubble-select-checkbox'); // * if it is a render of new message - const mid = +bubble.dataset.mid; - if(this.selectedMids.has(mid) && (!isGrouped || this.isGroupedMidsSelected(mid))) { - checkboxField.input.checked = true; - bubble.classList.add('is-selected'); + if(this.isSelecting) { // ! avoid breaking animation on start + const mid = +bubble.dataset.mid; + if(this.selectedMids.has(mid) && (!isGrouped || this.isGroupedMidsSelected(mid))) { + checkboxField.input.checked = true; + bubble.classList.add('is-selected'); + } } if(bubble.classList.contains('document-container')) { diff --git a/src/components/chat/stickersHelper.ts b/src/components/chat/stickersHelper.ts index 79943347..26c0b5d9 100644 --- a/src/components/chat/stickersHelper.ts +++ b/src/components/chat/stickersHelper.ts @@ -23,7 +23,10 @@ export default class StickersHelper { private lastEmoticon = ''; constructor(private appendTo: HTMLElement) { + this.container = document.createElement('div'); + this.container.classList.add('stickers-helper', 'z-depth-1'); + this.appendTo.append(this.container); } public checkEmoticon(emoticon: string) { @@ -32,7 +35,9 @@ export default class StickersHelper { if(this.lastEmoticon && !emoticon) { if(this.container) { SetTransition(this.container, 'is-visible', false, 200, () => { - this.stickersContainer.innerHTML = ''; + if(this.stickersContainer) { + this.stickersContainer.innerHTML = ''; + } }); } } @@ -86,8 +91,6 @@ export default class StickersHelper { } private init() { - this.container = document.createElement('div'); - this.container.classList.add('stickers-helper', 'z-depth-1'); this.container.addEventListener('click', (e) => { if(!findUpClassName(e.target, 'super-sticker')) { return; @@ -104,7 +107,5 @@ export default class StickersHelper { this.scrollable = new Scrollable(this.container); this.lazyLoadQueue = new LazyLoadQueue(); this.superStickerRenderer = new SuperStickerRenderer(this.lazyLoadQueue, CHAT_ANIMATION_GROUP); - - this.appendTo.append(this.container); } } diff --git a/src/components/chat/topbar.ts b/src/components/chat/topbar.ts index 1ec258e0..0da54c95 100644 --- a/src/components/chat/topbar.ts +++ b/src/components/chat/topbar.ts @@ -447,7 +447,8 @@ export default class ChatTopbar { public setTitle(count?: number) { let titleEl: HTMLElement; if(this.chat.type === 'pinned') { - titleEl = i18n('PinnedMessagesCount', [count]); + if(count === undefined) titleEl = i18n('Loading'); + else titleEl = i18n('PinnedMessagesCount', [count]); if(count === undefined) { this.appMessagesManager.getSearchCounters(this.peerId, [{_: 'inputMessagesFilterPinned'}]).then(result => { @@ -481,7 +482,8 @@ export default class ChatTopbar { }); } } else if(this.chat.type === 'discussion') { - titleEl = i18n('Chat.Title.Comments', [count]); + if(count === undefined) titleEl = i18n('Loading'); + else titleEl = i18n('Chat.Title.Comments', [count]); if(count === undefined) { Promise.all([ diff --git a/src/components/emoticonsDropdown/tabs/emoji.ts b/src/components/emoticonsDropdown/tabs/emoji.ts index f6865dd8..e7498482 100644 --- a/src/components/emoticonsDropdown/tabs/emoji.ts +++ b/src/components/emoticonsDropdown/tabs/emoji.ts @@ -10,6 +10,7 @@ import { fastRaf } from "../../../helpers/schedulers"; import appImManager from "../../../lib/appManagers/appImManager"; import appStateManager from "../../../lib/appManagers/appStateManager"; import Config from "../../../lib/config"; +import { i18n, LangPackKey } from "../../../lib/langPack"; import { RichTextProcessor } from "../../../lib/richtextprocessor"; import rootScope from "../../../lib/rootScope"; import { putPreloader } from "../../misc"; @@ -25,19 +26,32 @@ export default class EmojiTab implements EmoticonsTab { private scroll: Scrollable; private stickyIntersector: StickyIntersector; + private loadedURLs: Set = new Set(); + init() { this.content = document.getElementById('content-emoji') as HTMLDivElement; - const categories = ["Smileys & Emotion", "Animals & Nature", "Food & Drink", "Travel & Places", "Activities", "Objects", /* "Symbols", */"Flags", "Skin Tones"]; + const categories: LangPackKey[] = [ + 'Emoji.SmilesAndPeople', + 'Emoji.AnimalsAndNature', + 'Emoji.FoodAndDrink', + 'Emoji.TravelAndPlaces', + 'Emoji.ActivityAndSport', + 'Emoji.Objects', + /* 'Emoji.Symbols', */ + 'Emoji.Flags', + 'Skin Tones' as any + ]; const divs: { - [category: string]: HTMLDivElement + [category in LangPackKey]?: HTMLDivElement } = {}; - const sorted: { - [category: string]: string[] - } = { - 'Recent': [] - }; + const sorted: Map = new Map([ + [ + 'Emoji.Recent', + [] + ] + ]); for(const emoji in Config.Emoji) { const details = Config.Emoji[emoji]; @@ -45,32 +59,35 @@ export default class EmojiTab implements EmoticonsTab { const category = categories[+i[0] - 1]; if(!category) continue; // maybe it's skin tones - if(!sorted[category]) sorted[category] = []; - sorted[category][+i.slice(1) || 0] = emoji; + let s = sorted.get(category); + if(!s) { + s = []; + sorted.set(category, s); + } + + s[+i.slice(1) || 0] = emoji; } //console.log('emoticons sorted:', sorted); //Object.keys(sorted).forEach(c => sorted[c].sort((a, b) => a - b)); - categories.pop(); - delete sorted["Skin Tones"]; + sorted.delete(categories.pop()); //console.time('emojiParse'); - for(const category in sorted) { + sorted.forEach((emojis, category) => { const div = document.createElement('div'); div.classList.add('emoji-category'); const titleDiv = document.createElement('div'); titleDiv.classList.add('category-title'); - titleDiv.innerText = category; + titleDiv.append(i18n(category)); const itemsDiv = document.createElement('div'); itemsDiv.classList.add('category-items'); div.append(titleDiv, itemsDiv); - const emojis = sorted[category]; emojis.forEach(emoji => { /* if(emojiUnicode(emoji) === '1f481-200d-2642') { console.log('append emoji', emoji, emojiUnicode(emoji)); @@ -86,7 +103,8 @@ export default class EmojiTab implements EmoticonsTab { }); divs[category] = div; - } + }); + //console.timeEnd('emojiParse'); const menu = this.content.previousElementSibling as HTMLElement; @@ -107,14 +125,14 @@ export default class EmojiTab implements EmoticonsTab { ]).then(() => { preloader.remove(); - this.recentItemsDiv = divs['Recent'].querySelector('.category-items'); + this.recentItemsDiv = divs['Emoji.Recent'].querySelector('.category-items'); for(const emoji of this.recent) { this.appendEmoji(emoji, this.recentItemsDiv); } this.recentItemsDiv.parentElement.classList.toggle('hide', !this.recent.length); - categories.unshift('Recent'); + categories.unshift('Emoji.Recent'); categories.map(category => { const div = divs[category]; @@ -173,27 +191,32 @@ export default class EmojiTab implements EmoticonsTab { if(spanEmoji.firstElementChild && !RichTextProcessor.emojiSupported) { const image = spanEmoji.firstElementChild as HTMLImageElement; image.setAttribute('loading', 'lazy'); - - const placeholder = document.createElement('span'); - placeholder.classList.add('emoji-placeholder'); - if(rootScope.settings.animationsEnabled) { - image.style.opacity = '0'; - placeholder.style.opacity = '1'; + const url = image.src; + if(!this.loadedURLs.has(url)) { + const placeholder = document.createElement('span'); + placeholder.classList.add('emoji-placeholder'); + + if(rootScope.settings.animationsEnabled) { + image.style.opacity = '0'; + placeholder.style.opacity = '1'; + } + + image.addEventListener('load', () => { + fastRaf(() => { + if(rootScope.settings.animationsEnabled) { + image.style.opacity = ''; + placeholder.style.opacity = ''; + } + + spanEmoji.classList.remove('empty'); + + this.loadedURLs.add(url); + }); + }, {once: true}); + + spanEmoji.append(placeholder); } - - image.addEventListener('load', () => { - fastRaf(() => { - if(rootScope.settings.animationsEnabled) { - image.style.opacity = ''; - placeholder.style.opacity = ''; - } - - spanEmoji.classList.remove('empty'); - }); - }, {once: true}); - - spanEmoji.append(placeholder); } //spanEmoji = spanEmoji.firstElementChild as HTMLSpanElement; @@ -216,7 +239,12 @@ export default class EmojiTab implements EmoticonsTab { //if(target.tagName !== 'SPAN') return; if(target.tagName === 'SPAN' && !target.classList.contains('emoji')) { - target = findUpClassName(target, 'category-item').firstChild as HTMLElement; + target = findUpClassName(target, 'category-item'); + if(!target) { + return; + } + + target = target.firstChild as HTMLElement; } else if(target.tagName === 'DIV') return; //console.log('contentEmoji div', target); @@ -253,4 +281,4 @@ export default class EmojiTab implements EmoticonsTab { onClose() { } -} \ No newline at end of file +} diff --git a/src/components/popups/newMedia.ts b/src/components/popups/newMedia.ts index 4eccb6fc..45e59616 100644 --- a/src/components/popups/newMedia.ts +++ b/src/components/popups/newMedia.ts @@ -5,7 +5,6 @@ */ import type Chat from "../chat/chat"; -import { isTouchSupported } from "../../helpers/touchSupport"; import { calcImageInBox, placeCaretAtEnd, isSendShortcutPressed } from "../../helpers/dom"; import InputField from "../inputField"; import PopupElement from "."; @@ -14,7 +13,7 @@ import { toast } from "../toast"; import { prepareAlbum, wrapDocument } from "../wrappers"; import CheckboxField from "../checkboxField"; import SendContextMenu from "../chat/sendContextMenu"; -import { createPosterForVideo, createPosterFromVideo, onVideoLoad } from "../../helpers/files"; +import { createPosterFromVideo, onVideoLoad } from "../../helpers/files"; import { MyDocument } from "../../lib/appManagers/appDocsManager"; import I18n, { i18n, LangPackKey } from "../../lib/langPack"; @@ -50,7 +49,7 @@ export default class PopupNewMedia extends PopupElement { inputField: InputField; constructor(private chat: Chat, files: File[], willAttachType: PopupNewMedia['willAttach']['type']) { - super('popup-send-photo popup-new-media', null, {closable: true, withConfirm: 'PreviewSender.Send'}); + super('popup-send-photo popup-new-media', null, {closable: true, withConfirm: 'Modal.Send'}); this.willAttach.type = willAttachType; diff --git a/src/components/ripple.ts b/src/components/ripple.ts index 45bffe3e..3b7f6589 100644 --- a/src/components/ripple.ts +++ b/src/components/ripple.ts @@ -87,28 +87,18 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise { - let rect = r.getBoundingClientRect(); + const rect = r.getBoundingClientRect(); elem.classList.add('c-ripple__circle'); - let clickX = clientX - rect.left; - let clickY = clientY - rect.top; + const clickX = clientX - rect.left; + const clickY = clientY - rect.top; - let size: number, clickPos: number; - if(rect.width > rect.height) { - size = rect.width; - clickPos = clickX; - } else { - size = rect.height; - clickPos = clickY; - } - - let offsetFromCenter = clickPos > (size / 2) ? size - clickPos : clickPos; - size = size - offsetFromCenter; - size *= 1.1; + const radius = Math.sqrt((Math.abs(clickY - rect.height / 2) + rect.height / 2) ** 2 + (Math.abs(clickX - rect.width / 2) + rect.width / 2) ** 2); + const size = radius; // center of circle - let x = clickX - size / 2; - let y = clickY - size / 2; + const x = clickX - size / 2; + const y = clickY - size / 2; //console.log('ripple click', offsetFromCenter, size, clickX, clickY); diff --git a/src/components/row.ts b/src/components/row.ts index d53172eb..b215340a 100644 --- a/src/components/row.ts +++ b/src/components/row.ts @@ -31,7 +31,7 @@ export default class Row { noCheckboxSubtitle: boolean, title: string, titleLangKey: LangPackKey, - titleRight: string, + titleRight: string | HTMLElement, clickable: boolean | ((e: Event) => void), navigationTab: SliderSuperTab }> = {}) { @@ -45,6 +45,7 @@ export default class Row { } else if(options.subtitleLangKey) { this.subtitle.append(i18n(options.subtitleLangKey)); } + this.container.append(this.subtitle); let havePadding = false; if(options.radioField || options.checkboxField) { @@ -56,9 +57,16 @@ export default class Row { if(options.checkboxField) { this.checkboxField = options.checkboxField; - this.container.append(this.checkboxField.label); + + const isToggle = options.checkboxField.label.classList.contains('checkbox-field-toggle'); + if(isToggle) { + this.container.classList.add('row-with-toggle'); + options.titleRight = this.checkboxField.label; + } else { + this.container.append(this.checkboxField.label); + } - if(!options.noCheckboxSubtitle) { + if(!options.noCheckboxSubtitle && !isToggle) { this.checkboxField.input.addEventListener('change', () => { replaceContent(this.subtitle, i18n(this.checkboxField.input.checked ? 'Checkbox.Enabled' : 'Checkbox.Disabled')); }); @@ -67,41 +75,47 @@ export default class Row { const i = options.radioField || options.checkboxField; i.label.classList.add('disable-hover'); - } else { - if(options.title || options.titleLangKey) { - let c: HTMLElement; - if(options.titleRight) { - c = document.createElement('div'); - c.classList.add('row-title-row'); - this.container.append(c); - } else { - c = this.container; - } + } + + if(options.title || options.titleLangKey) { + let c: HTMLElement; + if(options.titleRight) { + c = document.createElement('div'); + c.classList.add('row-title-row'); + this.container.append(c); + } else { + c = this.container; + } - this.title = document.createElement('div'); - this.title.classList.add('row-title'); - if(options.title) { - this.title.innerHTML = options.title; - } else { - this.title.append(i18n(options.titleLangKey)); - } - c.append(this.title); + this.title = document.createElement('div'); + this.title.classList.add('row-title'); + if(options.title) { + this.title.innerHTML = options.title; + } else { + this.title.append(i18n(options.titleLangKey)); + } + c.append(this.title); - if(options.titleRight) { - const titleRight = document.createElement('div'); - titleRight.classList.add('row-title', 'row-title-right'); + if(options.titleRight) { + const titleRight = document.createElement('div'); + titleRight.classList.add('row-title', 'row-title-right'); + + if(typeof(options.titleRight) === 'string') { titleRight.innerHTML = options.titleRight; - c.append(titleRight); + } else { + titleRight.append(options.titleRight); } - } - if(options.icon) { - havePadding = true; - this.title.classList.add('tgico', 'tgico-' + options.icon); - this.container.classList.add('row-with-icon'); + c.append(titleRight); } } + if(options.icon) { + havePadding = true; + this.title.classList.add('tgico', 'tgico-' + options.icon); + this.container.classList.add('row-with-icon'); + } + if(havePadding) { this.container.classList.add('row-with-padding'); } @@ -125,8 +139,6 @@ export default class Row { this.container.prepend(this.container.lastElementChild); } */ } - - this.container.append(this.subtitle); } diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index 46dd7ff5..3a482ca7 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -8,6 +8,7 @@ import { isTouchSupported } from "../helpers/touchSupport"; import { logger, LogLevels } from "../lib/logger"; import fastSmoothScroll, { FocusDirection } from "../helpers/fastSmoothScroll"; import useHeavyAnimationCheck from "../hooks/useHeavyAnimationCheck"; +import { cancelEvent } from "../helpers/dom"; /* var el = $0; var height = 0; @@ -250,10 +251,11 @@ export class ScrollableX extends ScrollableBase { const scrollHorizontally = (e: any) => { if(!e.deltaX) { this.container!.scrollLeft += e.deltaY / 4; + cancelEvent(e); } }; - this.container.addEventListener('wheel', scrollHorizontally, {passive: true}); + this.container.addEventListener('wheel', scrollHorizontally, {passive: false}); } } } diff --git a/src/components/sidebarLeft/tabs/contacts.ts b/src/components/sidebarLeft/tabs/contacts.ts index 302d0c99..c3a2625c 100644 --- a/src/components/sidebarLeft/tabs/contacts.ts +++ b/src/components/sidebarLeft/tabs/contacts.ts @@ -11,6 +11,7 @@ import appPhotosManager from "../../../lib/appManagers/appPhotosManager"; import rootScope from "../../../lib/rootScope"; import InputSearch from "../../inputSearch"; import { canFocus } from "../../../helpers/dom"; +import { isMobile } from "../../../helpers/userAgent"; // TODO: поиск по людям глобальный, если не нашло в контактах никого @@ -53,7 +54,7 @@ export default class AppContactsTab extends SliderSuperTab { } onOpenAfterTimeout() { - if(!canFocus(true)) return; + if(isMobile || !canFocus(true)) return; this.inputSearch.input.focus(); } diff --git a/src/components/sidebarLeft/tabs/language.ts b/src/components/sidebarLeft/tabs/language.ts index 125e44a3..a1dd0d93 100644 --- a/src/components/sidebarLeft/tabs/language.ts +++ b/src/components/sidebarLeft/tabs/language.ts @@ -7,12 +7,13 @@ import { SettingSection } from ".."; import { randomLong } from "../../../helpers/random"; import I18n from "../../../lib/langPack"; +import apiManager from "../../../lib/mtproto/mtprotoworker"; import RadioField from "../../radioField"; import Row, { RadioFormFromRows } from "../../row"; import { SliderSuperTab } from "../../slider" export default class AppLanguageTab extends SliderSuperTab { - protected init() { + protected async init() { this.container.classList.add('language-container'); this.setTitle('Telegram.LanguageViewController'); @@ -20,94 +21,42 @@ export default class AppLanguageTab extends SliderSuperTab { const radioRows: Map = new Map(); - let r = [{ - code: 'en', - text: 'English', - subtitle: 'English' - }, { - code: 'be', - text: 'Belarusian', - subtitle: 'Беларуская' - }, { - code: 'ca', - text: 'Catalan', - subtitle: 'Català' - }, { - code: 'nl', - text: 'Dutch', - subtitle: 'Nederlands' - }, { - code: 'fr', - text: 'French', - subtitle: 'Français' - }, { - code: 'de', - text: 'German', - subtitle: 'Deutsch' - }, { - code: 'it', - text: 'Italian', - subtitle: 'Italiano' - }, { - code: 'ms', - text: 'Malay', - subtitle: 'Bahasa Melayu' - }, { - code: 'pl', - text: 'Polish', - subtitle: 'Polski' - }, { - code: 'pt', - text: 'Portuguese (Brazil)', - subtitle: 'Português (Brasil)' - }, { - code: 'ru', - text: 'Russian', - subtitle: 'Русский' - }, { - code: 'es', - text: 'Spanish', - subtitle: 'Español' - }, { - code: 'tr', - text: 'Turkish', - subtitle: 'Türkçe' - }, { - code: 'uk', - text: 'Ukrainian', - subtitle: 'Українська' - }]; - - const random = randomLong(); - r.forEach(({code, text, subtitle}) => { - const row = new Row({ - radioField: new RadioField({ - text, - name: random, - value: code - }), - subtitle + const promise = apiManager.invokeApiCacheable('langpack.getLanguages', { + lang_pack: 'macos' + }).then((languages) => { + const random = randomLong(); + languages.forEach((language) => { + const row = new Row({ + radioField: new RadioField({ + text: language.name, + name: random, + value: language.lang_code + }), + subtitle: language.native_name + }); + + radioRows.set(language.lang_code, row); }); - - radioRows.set(code, row); - }); - - const form = RadioFormFromRows([...radioRows.values()], (value) => { - I18n.getLangPack(value); - }); - I18n.getCacheLangPack().then(langPack => { - const row = radioRows.get(langPack.lang_code); - if(!row) { - console.error('no row', row, langPack); - return; - } - - row.radioField.setValueSilently(true); + const form = RadioFormFromRows([...radioRows.values()], (value) => { + I18n.getLangPack(value); + }); + + I18n.getCacheLangPack().then(langPack => { + const row = radioRows.get(langPack.lang_code); + if(!row) { + console.error('no row', row, langPack); + return; + } + + row.radioField.setValueSilently(true); + }); + + section.content.append(form); }); - section.content.append(form); - this.scrollable.append(section.container); + + return promise; } } diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index 9b9ac36d..9d201667 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -5,18 +5,17 @@ */ import appImManager from "../../../lib/appManagers/appImManager"; -import appMessagesManager from "../../../lib/appManagers/appMessagesManager"; +import appMessagesManager, { AppMessagesManager } from "../../../lib/appManagers/appMessagesManager"; import appPeersManager from "../../../lib/appManagers/appPeersManager"; import appProfileManager from "../../../lib/appManagers/appProfileManager"; import appUsersManager, { User } from "../../../lib/appManagers/appUsersManager"; -import { logger } from "../../../lib/logger"; import { RichTextProcessor } from "../../../lib/richtextprocessor"; import rootScope from "../../../lib/rootScope"; import AppSearchSuper, { SearchSuperType } from "../../appSearchSuper."; -import AvatarElement from "../../avatar"; +import AvatarElement, { openAvatarViewer } from "../../avatar"; import SidebarSlider, { SliderSuperTab } from "../../slider"; import CheckboxField from "../../checkboxField"; -import { attachClickEvent, replaceContent, whichChild } from "../../../helpers/dom"; +import { attachClickEvent, replaceContent, cancelEvent } from "../../../helpers/dom"; import appSidebarRight from ".."; import { TransitionSlider } from "../../transition"; import appNotificationsManager from "../../../lib/appManagers/appNotificationsManager"; @@ -25,7 +24,7 @@ import PeerTitle from "../../peerTitle"; import AppEditChannelTab from "./editChannel"; import AppEditContactTab from "./editContact"; import appChatsManager, { Channel } from "../../../lib/appManagers/appChatsManager"; -import { Chat, UserProfilePhoto } from "../../../layer"; +import { Chat, Message, MessageAction, ChatFull, Photo } from "../../../layer"; import Button from "../../button"; import ButtonIcon from "../../buttonIcon"; import I18n, { i18n, LangPackKey } from "../../../lib/langPack"; @@ -43,6 +42,8 @@ import { MOUNT_CLASS_TO } from "../../../config/debug"; import AppAddMembersTab from "../../sidebarLeft/tabs/addMembers"; import PopupPickUser from "../../popups/pickUser"; import PopupPeer from "../../popups/peer"; +import Scrollable from "../../scrollable"; +import { isTouchSupported } from "../../../helpers/touchSupport"; let setText = (text: string, row: Row) => { fastRaf(() => { @@ -166,21 +167,21 @@ class PeerProfileAvatars { public static BASE_CLASS = 'profile-avatars'; public container: HTMLElement; public avatars: HTMLElement; - //public gradient: HTMLElement; + public gradient: HTMLElement; public info: HTMLElement; public tabs: HTMLDivElement; - public listLoader: ListLoader; + public listLoader: ListLoader; public peerId: number; - constructor() { + constructor(public scrollable: Scrollable) { this.container = document.createElement('div'); this.container.classList.add(PeerProfileAvatars.BASE_CLASS + '-container'); this.avatars = document.createElement('div'); this.avatars.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatars'); - //this.gradient = document.createElement('div'); - //this.gradient.classList.add(PeerProfileAvatars.BASE_CLASS + '-gradient'); + this.gradient = document.createElement('div'); + this.gradient.classList.add(PeerProfileAvatars.BASE_CLASS + '-gradient'); this.info = document.createElement('div'); this.info.classList.add(PeerProfileAvatars.BASE_CLASS + '-info'); @@ -188,29 +189,77 @@ class PeerProfileAvatars { this.tabs = document.createElement('div'); this.tabs.classList.add(PeerProfileAvatars.BASE_CLASS + '-tabs'); - this.container.append(this.avatars, /* this.gradient, */this.info, this.tabs); + this.container.append(this.avatars, this.gradient, this.info, this.tabs); + const checkScrollTop = () => { + if(this.scrollable.scrollTop !== 0) { + this.scrollable.scrollIntoViewNew(this.scrollable.container.firstElementChild as HTMLElement, 'start'); + return false; + } + + return true; + }; + + const SWITCH_ZONE = 1 / 3; let cancel = false; - attachClickEvent(this.container, (_e) => { + let freeze = false; + attachClickEvent(this.container, async(_e) => { + if(freeze) { + cancelEvent(_e); + return; + } + if(cancel) { cancel = false; return; } + if(!checkScrollTop()) { + return; + } + const rect = this.container.getBoundingClientRect(); const e = (_e as TouchEvent).touches ? (_e as TouchEvent).touches[0] : _e as MouseEvent; const x = e.pageX; - const centerX = rect.right - (rect.width / 2); - const toRight = x > centerX; + const clickX = x - rect.left; + if(clickX > (rect.width * SWITCH_ZONE) && clickX < (rect.width - rect.width * SWITCH_ZONE)) { + const peerId = this.peerId; + + const targets: {element: HTMLElement, item: string | Message.messageService}[] = []; + this.listLoader.previous.concat(this.listLoader.current, this.listLoader.next).forEach((item, idx) => { + targets.push({ + element: /* null */this.avatars.children[idx] as HTMLElement, + item + }); + }); + + const prevTargets = targets.slice(0, this.listLoader.previous.length); + const nextTargets = targets.slice(this.listLoader.previous.length + 1); - // this.avatars.classList.remove('no-transition'); - // fastRaf(() => { - this.listLoader.go(toRight ? 1 : -1); - // }); + const target = this.avatars.children[this.listLoader.previous.length] as HTMLElement; + freeze = true; + openAvatarViewer(target, peerId, () => peerId === this.peerId, this.listLoader.current, prevTargets, nextTargets); + freeze = false; + } else { + const centerX = rect.right - (rect.width / 2); + const toRight = x > centerX; + + // this.avatars.classList.remove('no-transition'); + // fastRaf(() => { + this.listLoader.go(toRight ? 1 : -1); + // }); + } }); + const cancelNextClick = () => { + cancel = true; + document.body.addEventListener(isTouchSupported ? 'touchend' : 'click', (e) => { + cancel = false; + }, {once: true}); + }; + let width = 0, x = 0, lastDiffX = 0, lastIndex = 0, minX = 0; const swipeHandler = new SwipeHandler({ element: this.avatars, @@ -225,7 +274,11 @@ class PeerProfileAvatars { return false; }, verifyTouchTarget: (e) => { - if(this.tabs.classList.contains('hide')) { + if(!checkScrollTop()) { + cancelNextClick(); + cancelEvent(e); + return false; + } else if(this.tabs.classList.contains('hide') || freeze) { return false; } @@ -247,7 +300,7 @@ class PeerProfileAvatars { }, onReset: () => { const addIndex = Math.ceil(Math.abs(lastDiffX) / (width / 2)) * (lastDiffX >= 0 ? 1 : -1); - cancel = true; + cancelNextClick(); //console.log(addIndex); @@ -268,15 +321,51 @@ class PeerProfileAvatars { } const loadCount = 50; - const listLoader: PeerProfileAvatars['listLoader'] = this.listLoader = new ListLoader({ + const listLoader: PeerProfileAvatars['listLoader'] = this.listLoader = new ListLoader({ loadCount, loadMore: (anchor, older) => { - return appPhotosManager.getUserPhotos(peerId, anchor || listLoader.current, loadCount).then(result => { - return { - count: result.count, - items: result.photos - }; - }); + if(peerId > 0) { + return appPhotosManager.getUserPhotos(peerId, (anchor || listLoader.current) as any, loadCount).then(result => { + return { + count: result.count, + items: result.photos + }; + }); + } else { + const promises: [Promise, ReturnType] = [] as any; + if(!listLoader.current) { + promises.push(appProfileManager.getChatFull(peerId)); + } + + promises.push(appMessagesManager.getSearch({ + peerId, + maxId: Number.MAX_SAFE_INTEGER, + inputFilter: { + _: 'inputMessagesFilterChatPhotos' + }, + limit: loadCount, + backLimit: 0 + })); + + return Promise.all(promises).then((result) => { + const value = result.pop() as typeof result[1]; + + if(!listLoader.current) { + const chatFull = result[0]; + const message = value.history.findAndSplice(m => { + return ((m as Message.messageService).action as MessageAction.messageActionChannelEditPhoto).photo.id === chatFull.chat_photo.id; + }) as Message.messageService; + + listLoader.current = message || appMessagesManager.generateFakeAvatarMessage(this.peerId, chatFull.chat_photo); + } + + //console.log('avatars loaded:', value); + return { + count: value.count, + items: value.history + }; + }); + } }, processItem: this.processItem, onJump: (item, older) => { @@ -292,7 +381,10 @@ class PeerProfileAvatars { } }); - listLoader.current = (photo as UserProfilePhoto.userProfilePhoto).photo_id; + if(photo._ === 'userProfilePhoto') { + listLoader.current = photo.photo_id; + } + this.processItem(listLoader.current); listLoader.load(true); @@ -310,11 +402,17 @@ class PeerProfileAvatars { this.tabs.classList.toggle('hide', this.tabs.childElementCount <= 1); } - public processItem = (photoId: string) => { + public processItem = (photoId: string | Message.messageService) => { const avatar = document.createElement('div'); avatar.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar'); - const photo = appPhotosManager.getPhoto(photoId); + let photo: Photo.photo; + if(photoId) { + photo = typeof(photoId) === 'string' ? + appPhotosManager.getPhoto(photoId) : + (photoId.action as MessageAction.messageActionChannelEditPhoto).photo as Photo.photo; + } + const img = new Image(); img.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar-image'); img.draggable = false; @@ -357,6 +455,10 @@ class PeerProfile { private peerId = 0; private threadId: number; + constructor(public scrollable: Scrollable) { + + } + public init() { this.init = null; @@ -419,11 +521,17 @@ class PeerProfile { }); this.notifications = new Row({ - checkboxField: new CheckboxField({text: 'Notifications'}) + checkboxField: new CheckboxField({toggle: true}), + titleLangKey: 'Notifications', + icon: 'unmute' }); this.section.content.append(this.phone.container, this.username.container, this.bio.container, this.notifications.container); - this.element.append(this.section.container); + + const delimiter = document.createElement('div'); + delimiter.classList.add('gradient-delimiter'); + + this.element.append(this.section.container, delimiter); this.notifications.checkboxField.input.addEventListener('change', (e) => { if(!e.isTrusted) { @@ -502,7 +610,7 @@ class PeerProfile { if(photo) { const oldAvatars = this.avatars; - this.avatars = new PeerProfileAvatars(); + this.avatars = new PeerProfileAvatars(this.scrollable); this.avatars.setPeer(this.peerId); this.avatars.info.append(this.name, this.subtitle); @@ -511,10 +619,14 @@ class PeerProfile { if(oldAvatars) oldAvatars.container.replaceWith(this.avatars.container); else this.element.prepend(this.avatars.container); + this.scrollable.container.classList.add('parallax'); + return; } } + this.scrollable.container.classList.remove('parallax'); + if(this.avatars) { this.avatars.container.remove(); this.avatars = undefined; @@ -699,7 +811,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { // * body - this.profile = new PeerProfile(); + this.profile = new PeerProfile(this.scrollable); this.profile.init(); this.scrollable.append(this.profile.element); @@ -712,6 +824,7 @@ export default class AppSharedMediaTab extends SliderSuperTab { const top = rect.top; const isSharedMedia = top <= HEADER_HEIGHT; animatedCloseIcon.classList.toggle('state-back', isSharedMedia); + this.searchSuper.container.classList.toggle('is-full-viewport', isSharedMedia); transition(+isSharedMedia); if(!isSharedMedia) { diff --git a/src/components/sortedUserList.ts b/src/components/sortedUserList.ts index b31f021a..01a9dcd0 100644 --- a/src/components/sortedUserList.ts +++ b/src/components/sortedUserList.ts @@ -1,8 +1,14 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + import appDialogsManager, { DialogDom } from "../lib/appManagers/appDialogsManager"; import { isInDOM, positionElementByIndex, replaceContent } from "../helpers/dom"; import { getHeavyAnimationPromise } from "../hooks/useHeavyAnimationCheck"; import appUsersManager from "../lib/appManagers/appUsersManager"; -import { insertInDescendSortedArray, forEachReverse } from "../helpers/array"; +import { insertInDescendSortedArray } from "../helpers/array"; type SortedUser = { peerId: number, diff --git a/src/components/swipeHandler.ts b/src/components/swipeHandler.ts index d0d12546..2b272d45 100644 --- a/src/components/swipeHandler.ts +++ b/src/components/swipeHandler.ts @@ -17,7 +17,7 @@ const attachGlobalListenerTo = window; export default class SwipeHandler { private element: HTMLElement; private onSwipe: (xDiff: number, yDiff: number) => boolean; - private verifyTouchTarget: (evt: Touch | MouseEvent) => boolean; + private verifyTouchTarget: (evt: TouchEvent | MouseEvent) => boolean; private onFirstSwipe: () => void; private onReset: () => void; @@ -65,7 +65,7 @@ export default class SwipeHandler { handleStart = (_e: TouchEvent | MouseEvent) => { const e = getEvent(_e); - if(this.verifyTouchTarget && !this.verifyTouchTarget(e)) { + if(this.verifyTouchTarget && !this.verifyTouchTarget(_e)) { return this.reset(); } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index eb124b14..9ac0ef6c 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -649,7 +649,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT }) { if(!((photo as MyPhoto).sizes || (photo as MyDocument).thumbs)) { if(boxWidth && boxHeight && photo._ === 'document') { - size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight); + size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message && message.message); } return { @@ -677,7 +677,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT image = new Image(); if(boxWidth && boxHeight) { // !album - size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight); + size = appPhotosManager.setAttachmentSize(photo, container, boxWidth, boxHeight, undefined, message && message.message); } const gotThumb = appPhotosManager.getStrippedThumbIfNeeded(photo); diff --git a/src/config/app.ts b/src/config/app.ts index 79aeefe4..78ac664e 100644 --- a/src/config/app.ts +++ b/src/config/app.ts @@ -13,7 +13,7 @@ const App = { id: 1025907, hash: '452b0359b988148995f22ff0f4229750', version: '0.4.0', - langPackVersion: '0.1.1', + langPackVersion: '0.1.2', langPack: 'macos', langPackCode: 'en', domains: [] as string[], diff --git a/src/helpers/dom/getVisibleRect.ts b/src/helpers/dom/getVisibleRect.ts new file mode 100644 index 00000000..d2d65ccb --- /dev/null +++ b/src/helpers/dom/getVisibleRect.ts @@ -0,0 +1,50 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +export default function getVisibleRect(element: HTMLElement, overflowElement: HTMLElement) { + const rect = element.getBoundingClientRect(); + const overflowRect = overflowElement.getBoundingClientRect(); + + let {top: overflowTop, bottom: overflowBottom} = overflowRect; + + // * respect sticky headers + const sticky = overflowElement.querySelector('.sticky'); + if(sticky) { + const stickyRect = sticky.getBoundingClientRect(); + overflowTop = stickyRect.bottom; + } + + if(rect.top >= overflowBottom + || rect.bottom <= overflowTop + || rect.right <= overflowRect.left + || rect.left >= overflowRect.right) { + return null; + } + + const overflow = { + top: false, + right: false, + bottom: false, + left: false, + vertical: 0 as 0 | 1 | 2, + horizontal: 0 as 0 | 1 | 2 + }; + + // @ts-ignore + const w: any = 'visualViewport' in window ? window.visualViewport : window; + const windowWidth = w.width || w.innerWidth; + const windowHeight = w.height || w.innerHeight; + + return { + rect: { + top: rect.top < overflowTop && overflowTop !== 0 ? (overflow.top = true, ++overflow.vertical, overflowTop) : rect.top, + right: 0, + bottom: rect.bottom > overflowBottom && overflowBottom !== windowHeight ? (overflow.bottom = true, ++overflow.vertical, overflowBottom) : rect.bottom, + left: 0 + }, + overflow + }; +} diff --git a/src/helpers/mediaSizes.ts b/src/helpers/mediaSizes.ts index 5fa484d3..a15e7469 100644 --- a/src/helpers/mediaSizes.ts +++ b/src/helpers/mediaSizes.ts @@ -37,15 +37,15 @@ class MediaSizes extends EventListenerBase<{ private sizes: {[k in 'desktop' | 'handhelds']: Sizes} = { handhelds: { regular: { - width: 293, - height: 293 + width: 270, + height: 270 }, webpage: { - width: 293, - height: 213 + width: 270, + height: 200 }, album: { - width: 293, + width: 270, height: 0 }, esgSticker: { @@ -55,15 +55,15 @@ class MediaSizes extends EventListenerBase<{ }, desktop: { regular: { - width: 480, - height: 480 + width: 400, + height: 320 }, webpage: { - width: 480, - height: 400 + width: 400, + height: 320 }, album: { - width: 451, + width: 420, height: 0 }, esgSticker: { diff --git a/src/index.ts b/src/index.ts index 869a58c5..e091e83e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import App from './config/app'; import findUpClassName from './helpers/dom/findUpClassName'; import fixSafariStickyInput from './helpers/dom/fixSafariStickyInput'; +import { isMobileSafari } from './helpers/userAgent'; import './materialize.scss'; import './scss/style.scss'; import './scss/tgico.scss'; @@ -259,6 +260,20 @@ console.timeEnd('get storage1'); */ if(authState._ !== 'authStateSignedIn'/* || 1 === 1 */) { console.log('Will mount auth page:', authState._, Date.now() / 1000); + const el = document.getElementById('auth-pages'); + if(el) { + const scrollable = el.querySelector('.scrollable'); + if((!touchSupport.isTouchSupported || isMobileSafari)) { + scrollable.classList.add('no-scrollbar'); + } + + const placeholder = document.createElement('div'); + placeholder.classList.add('auth-placeholder'); + + scrollable.prepend(placeholder); + scrollable.append(placeholder.cloneNode()); + } + //langPromise.then(async() => { switch(authState._) { case 'authStateSignIn': diff --git a/src/lang.ts b/src/lang.ts index 49812b8c..5466e524 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -2,7 +2,7 @@ const lang = { "Animations": "Animations", "AttachAlbum": "Album", "BlockModal.Search.Placeholder": "Block user...", - "DarkMode": "Sith Mode", + "DarkMode": "Dark Mode", "FilterIncludeExcludeInfo": "Choose chats and types of chats that will\nappear and never appear in this folder.", "FilterNameInputLabel": "Folder Name", "FilterMenuDelete": "Delete Folder", @@ -59,7 +59,6 @@ const lang = { "Checkbox.Disabled": "Disabled", "Error.PreviewSender.CaptionTooLong": "Caption is too long.", "PreviewSender.GroupItems": "Group items", - "PreviewSender.Send": "SEND", "PreviewSender.SendAlbum": { "one_value": "Send Album", "other_value": "Send %d Albums" @@ -407,6 +406,9 @@ const lang = { "Chat.Date.ScheduledFor": "Scheduled for %@", //"Chat.Date.ScheduledUntilOnline": "Scheduled until online", "Chat.Date.ScheduledForToday": "Scheduled for today", + "Chat.DropTitle": "Drop files here to send them", + "Chat.DropQuickDesc": "in a quick way", + "Chat.DropAsFilesDesc": "without compression", "Chat.Service.PeerJoinedTelegram": "%@ joined Telegram", "Chat.Service.Channel.UpdatedTitle": "Channel renamed to \"%@\"", "Chat.Service.Channel.UpdatedPhoto": "Channel photo updated", @@ -487,11 +489,21 @@ const lang = { "EditAccount.Username": "Username", "EditAccount.Title": "Edit Profile", "EditAccount.Logout": "Log Out", + "Emoji.Recent": "Frequently Used", + "Emoji.SmilesAndPeople": "Smileys & People", + "Emoji.AnimalsAndNature": "Animals & Nature", + "Emoji.FoodAndDrink": "Food & Drink", + "Emoji.ActivityAndSport": "Activity & Sport", + "Emoji.TravelAndPlaces": "Travel & Places", + "Emoji.Objects": "Objects", + //"Emoji.Symbols": "Symbols", + "Emoji.Flags": "Flags", "LastSeen.HoursAgo": { "one_value": "last seen %d hour ago", "other_value": "last seen %d hours ago" }, "Login.Register.LastName.Placeholder": "Last Name", + "Modal.Send": "Send", "Telegram.GeneralSettingsViewController": "General Settings", "Telegram.InstalledStickerPacksController": "Stickers", "Telegram.NotificationSettingsViewController": "Notifications", diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index abbdd340..abce6cd6 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -7,7 +7,7 @@ //import apiManager from '../mtproto/apiManager'; import animationIntersector from '../../components/animationIntersector'; import appSidebarLeft, { LEFT_COLUMN_ACTIVE_CLASSNAME } from "../../components/sidebarLeft"; -import appSidebarRight, { AppSidebarRight, RIGHT_COLUMN_ACTIVE_CLASSNAME } from '../../components/sidebarRight'; +import appSidebarRight, { RIGHT_COLUMN_ACTIVE_CLASSNAME } from '../../components/sidebarRight'; import mediaSizes, { ScreenSize } from '../../helpers/mediaSizes'; import { logger, LogLevels } from "../logger"; import apiManager from '../mtproto/mtprotoworker'; @@ -46,6 +46,7 @@ import AppPrivateSearchTab from '../../components/sidebarRight/tabs/search'; import { i18n } from '../langPack'; import { SendMessageAction } from '../../layer'; import { highlightningColor } from '../../helpers/color'; +import { getObjectKeysAndSort } from '../../helpers/object'; //console.log('appImManager included33!'); @@ -54,6 +55,11 @@ appSidebarLeft; // just to include export const CHAT_ANIMATION_GROUP = 'chat'; const FOCUS_EVENT_NAME = isTouchSupported ? 'touchstart' : 'mousemove'; +export type ChatSavedPosition = { + mids: number[], + top: number +}; + export class AppImManager { public columnEl = document.getElementById('column-center') as HTMLDivElement; public chatsContainer: HTMLElement; @@ -165,13 +171,13 @@ export class AppImManager { this.setInnerPeer(peerId); }); - /* rootScope.on('peer_changing', (chat) => { + rootScope.on('peer_changing', (chat) => { this.saveChatPosition(chat); }); sessionStorage.get('chatPositions').then((c) => { sessionStorage.setToCache('chatPositions', c || {}); - }); */ + }); } private onHashChange = () => { @@ -230,28 +236,34 @@ export class AppImManager { }); } - /* public saveChatPosition(chat: Chat) { - const bubble = chat.bubbles.getBubbleByPoint('top'); - if(bubble) { - const top = bubble.getBoundingClientRect().top; - - this.log('saving position by bubble:', bubble, top); + public saveChatPosition(chat: Chat) { + if(!(['chat', 'discussion'] as ChatType[]).includes(chat.type) || !chat.peerId) { + return; + } + + //const bubble = chat.bubbles.getBubbleByPoint('top'); + //if(bubble) { + //const top = bubble.getBoundingClientRect().top; + const top = chat.bubbles.scrollable.scrollTop; const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : ''); const chatPositions = sessionStorage.getFromCache('chatPositions'); chatPositions[key] = { - mid: +bubble.dataset.mid, + mids: getObjectKeysAndSort(chat.bubbles.bubbles, 'desc'), top - }; - sessionStorage.set({chatPositions}); - } + } as ChatSavedPosition; + + this.log('saved chat position:', chatPositions[key]); + + sessionStorage.set({chatPositions}, true); + //} } - public getChatSavedPosition(chat: Chat) { + public getChatSavedPosition(chat: Chat): ChatSavedPosition { const key = chat.peerId + (chat.threadId ? '_' + chat.threadId : ''); return sessionStorage.getFromCache('chatPositions')[key]; - } */ + } private setSettings = () => { document.documentElement.style.setProperty('--messages-text-size', rootScope.settings.messagesTextSize + 'px'); @@ -439,8 +451,8 @@ export class AppImManager { if(types.length || force) { drops.push(new ChatDragAndDrop(dropsContainer, { icon: 'dragfiles', - header: 'Drop files here to send them', - subtitle: 'without compression', + header: 'Chat.DropTitle', + subtitle: 'Chat.DropAsFilesDesc', onDrop: (e: DragEvent) => { toggle(e, false); appImManager.log('drop', e); @@ -452,8 +464,8 @@ export class AppImManager { if((foundMedia && !foundDocuments) || force) { drops.push(new ChatDragAndDrop(dropsContainer, { icon: 'dragmedia', - header: 'Drop files here to send them', - subtitle: 'in a quick way', + header: 'Chat.DropTitle', + subtitle: 'Chat.DropQuickDesc', onDrop: (e: DragEvent) => { toggle(e, false); appImManager.log('drop', e); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 2fb66956..db6c8476 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -17,7 +17,7 @@ import { createPosterForVideo } from "../../helpers/files"; import { copy, defineNotNumerableProperties, getObjectKeysAndSort } from "../../helpers/object"; import { randomLong } from "../../helpers/random"; import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string"; -import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update } from "../../layer"; +import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo } from "../../layer"; import { InvokeApiOptions } from "../../types"; import I18n, { i18n, join, langPack, LangPackKey, _i18n } from "../langPack"; import { logger, LogLevels } from "../logger"; @@ -1548,6 +1548,24 @@ export class AppMessagesManager { return fwdHeader; } + public generateFakeAvatarMessage(peerId: number, photo: Photo) { + const maxId = Number.MAX_SAFE_INTEGER; + const message = { + _: 'messageService', + action: { + _: 'messageActionChannelEditPhoto', + photo + }, + mid: maxId, + peerId, + date: (photo as Photo.photo).date, + fromId: peerId + } as Message.messageService; + + this.getMessagesStorage(peerId)[maxId] = message; + return message; + } + public setDialogTopMessage(message: MyMessage, dialog: MTDialog.dialog = this.getDialogByPeerId(message.peerId)[0]) { if(dialog) { dialog.top_message = message.mid; diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index 887f98d0..fd5a70ad 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -219,7 +219,7 @@ export class AppPhotosManager { return {image, loadPromise}; } - public setAttachmentSize(photo: MyPhoto | MyDocument, element: HTMLElement | SVGForeignObjectElement, boxWidth: number, boxHeight: number, noZoom = true) { + public setAttachmentSize(photo: MyPhoto | MyDocument, element: HTMLElement | SVGForeignObjectElement, boxWidth: number, boxHeight: number, noZoom = true, hasText?: boolean) { const photoSize = this.choosePhotoSize(photo, boxWidth, boxHeight); //console.log('setAttachmentSize', photo, photo.sizes[0].bytes, div); @@ -233,7 +233,12 @@ export class AppPhotosManager { height = 'h' in photoSize ? photoSize.h : 100; } - const {w, h} = calcImageInBox(width, height, boxWidth, boxHeight, noZoom); + let {w, h} = calcImageInBox(width, height, boxWidth, boxHeight, noZoom); + + /* if(hasText) { + w = Math.max(boxWidth, w); + } */ + if(element instanceof SVGForeignObjectElement) { element.setAttributeNS(null, 'width', '' + w); element.setAttributeNS(null, 'height', '' + h); @@ -257,7 +262,7 @@ export class AppPhotosManager { } const sizes = (photo as MyPhoto).sizes || (photo as MyDocument).thumbs; - const thumb = sizes?.length ? sizes[0] : null; + const thumb = sizes?.length ? sizes.find(size => size._ === 'photoStrippedSize') : null; if(thumb && ('bytes' in thumb)) { return appPhotosManager.getImageFromStrippedThumb(thumb as any); } diff --git a/src/lib/sessionStorage.ts b/src/lib/sessionStorage.ts index e0ff5722..a24e96e2 100644 --- a/src/lib/sessionStorage.ts +++ b/src/lib/sessionStorage.ts @@ -4,9 +4,10 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import type { ChatSavedPosition } from './appManagers/appImManager'; +import type { State } from './appManagers/appStateManager'; import { MOUNT_CLASS_TO } from '../config/debug'; import { LangPackDifference } from '../layer'; -import type { State } from './appManagers/appStateManager'; import AppStorage from './storage'; const sessionStorage = new AppStorage<{ @@ -21,10 +22,7 @@ const sessionStorage = new AppStorage<{ server_time_offset: number, chatPositions: { - [peerId_threadId: string]: { - mid: number, - top: number - } + [peerId_threadId: string]: ChatSavedPosition }, langPack: LangPackDifference } & State>({ diff --git a/src/pages/pageSignIn.ts b/src/pages/pageSignIn.ts index b1b63d46..da93291e 100644 --- a/src/pages/pageSignIn.ts +++ b/src/pages/pageSignIn.ts @@ -12,7 +12,6 @@ import apiManager from "../lib/mtproto/mtprotoworker"; import { RichTextProcessor } from '../lib/richtextprocessor'; import { attachClickEvent, cancelEvent, replaceContent } from "../helpers/dom"; import Page from "./page"; -import pageAuthCode from "./pageAuthCode"; import InputField from "../components/inputField"; import CheckboxField from "../components/checkboxField"; import Button from "../components/button"; @@ -341,7 +340,7 @@ let onFirstMount = () => { }).then((code) => { //console.log('got code', code); - pageAuthCode.mount(Object.assign(code, {phone_number: phone_number})); + import('./pageAuthCode').then(m => m.default.mount(Object.assign(code, {phone_number: phone_number}))); }).catch(err => { this.removeAttribute('disabled'); diff --git a/src/pages/pagesManager.ts b/src/pages/pagesManager.ts index 32f55842..2d191892 100644 --- a/src/pages/pagesManager.ts +++ b/src/pages/pagesManager.ts @@ -9,6 +9,7 @@ import { whichChild } from "../helpers/dom"; import lottieLoader from "../lib/lottieLoader"; import { horizontalMenu } from "../components/horizontalMenu"; import { MOUNT_CLASS_TO } from "../config/debug"; +import fastSmoothScroll from "../helpers/fastSmoothScroll"; class PagesManager { private pageId = -1; @@ -16,10 +17,12 @@ class PagesManager { private selectTab: ReturnType; public pagesDiv: HTMLDivElement; + public scrollableDiv: HTMLElement; constructor() { this.pagesDiv = document.getElementById('auth-pages') as HTMLDivElement; - this.selectTab = horizontalMenu(null, this.pagesDiv.firstElementChild.firstElementChild as HTMLDivElement, null, () => { + this.scrollableDiv = this.pagesDiv.querySelector('.scrollable') as HTMLElement; + this.selectTab = horizontalMenu(null, this.scrollableDiv.querySelector('.tabs-container') as HTMLDivElement, null, () => { if(this.page?.onShown) { this.page.onShown(); } @@ -39,7 +42,13 @@ class PagesManager { lottieLoader.loadLottieWorkers(); } + + this.pageId = id; + + if(this.scrollableDiv) { + fastSmoothScroll(this.scrollableDiv, this.scrollableDiv.firstElementChild as HTMLElement, 'start'); + } } else { this.pagesDiv.style.display = 'none'; page.pageEl.style.display = ''; diff --git a/src/scss/partials/_avatar.scss b/src/scss/partials/_avatar.scss index 87dd2ef7..a6e9c1b5 100644 --- a/src/scss/partials/_avatar.scss +++ b/src/scss/partials/_avatar.scss @@ -7,8 +7,8 @@ avatar-element { --size: 54px; --multiplier: 1; - --color-top: var(--peer-avatar-blue-top); - --color-bottom: var(--peer-avatar-blue-bottom); + --color-top: var(--avatar-color-top); + --color-bottom: var(--avatar-color-bottom); color: #fff; width: var(--size); height: var(--size); @@ -52,6 +52,11 @@ avatar-element { --color-bottom: var(--peer-avatar-pink-bottom); } + &[data-color="blue"] { + --color-top: var(--peer-avatar-blue-top); + --color-bottom: var(--peer-avatar-blue-bottom); + } + &.tgico:before { font-size: calc(32px / var(--multiplier)); } diff --git a/src/scss/partials/_button.scss b/src/scss/partials/_button.scss index 7afae8d8..db3c56ae 100644 --- a/src/scss/partials/_button.scss +++ b/src/scss/partials/_button.scss @@ -195,49 +195,6 @@ &-text { flex: 1 1 auto; } - - .checkbox-field { - --size: 20px; - margin: 0 .3125rem; - padding: 0; - display: flex; - align-items: center; - pointer-events: none; - - .checkbox-toggle { - --toggle-width: 1.9375rem; - width: var(--toggle-width); - height: .875rem; - background-color: var(--secondary-color); - border-radius: $border-radius-big; - position: relative; - display: flex; - align-items: center; - transition: background-color .2s; - - &:before { - --offset: 3px; - width: 1.25rem; - height: 1.25rem; - border: 2px solid var(--secondary-color); - transition: border-color .2s, transform .2s; - background-color: var(--surface-color); - content: " "; - transform: translateX(calc(var(--offset) * -1)); - border-radius: 50%; - position: absolute; - } - } - - [type="checkbox"]:checked + .checkbox-toggle { - background-color: var(--primary-color); - - &:before { - border-color: var(--primary-color); - transform: translateX(calc(var(--toggle-width) - 1.25rem + var(--offset))); - } - } - } } /* &-overlay { diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index dc652c65..06c42dc0 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -833,7 +833,7 @@ $chat-helper-size: 39px; .btn-menu { padding: 8px 0; right: -11px; - bottom: calc(100% + 16px); + bottom: calc(100% + 1.25rem); > div { padding: 0 38px 0 16px; @@ -980,7 +980,7 @@ $chat-helper-size: 39px; // } // } - &.is-chat-input-hidden.is-selecting:not(.backwards) { + .chat.type-chat &.is-chat-input-hidden.is-selecting:not(.backwards) { --translateY: -78px; @include respond-to(handhelds) { @@ -1130,7 +1130,7 @@ $chat-helper-size: 39px; } } - .bubbles.is-chat-input-hidden & { + .chat.type-chat .bubbles.is-chat-input-hidden & { margin-bottom: 1rem; // .25rem is eaten by the last bubble's margin-bottom } diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 4cded4a9..f4249552 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -321,6 +321,17 @@ $bubble-margin: .25rem; //background: rgba(0, 0, 0, .16); background: var(--message-highlightning-color); cursor: pointer; + + html.no-touch & { + opacity: 0; + transition: opacity .2s ease-in-out; + } + } + + @include hover() { + .bubble-beside-button { + opacity: 1; + } } .forward { diff --git a/src/scss/partials/_chatTopbar.scss b/src/scss/partials/_chatTopbar.scss index 7b4469d0..47d8997a 100644 --- a/src/scss/partials/_chatTopbar.scss +++ b/src/scss/partials/_chatTopbar.scss @@ -89,7 +89,6 @@ font-size: 1rem; line-height: 24px; max-width: calc(100% - 1.5rem); - margin-bottom: 1px; /* @include respond-to(handhelds) { text-overflow: ellipsis; @@ -109,6 +108,10 @@ line-height: var(--line-height); } + .info:not(:empty) { + margin-top: 1px; + } + .btn-menu-toggle { .btn-menu { top: calc(100% + 7px); diff --git a/src/scss/partials/_chatlist.scss b/src/scss/partials/_chatlist.scss index 751d5af3..5af1421d 100644 --- a/src/scss/partials/_chatlist.scss +++ b/src/scss/partials/_chatlist.scss @@ -236,6 +236,10 @@ ul.chatlist { padding: .0625rem .4375rem .0625rem .5625rem; } + .dialog-avatar, .user-caption { + pointer-events: none; + } + .user-title { display: flex !important; align-items: center; diff --git a/src/scss/partials/_checkbox.scss b/src/scss/partials/_checkbox.scss index 6b465676..03e92f79 100644 --- a/src/scss/partials/_checkbox.scss +++ b/src/scss/partials/_checkbox.scss @@ -309,3 +309,47 @@ } } } + +.checkbox-field-toggle { + --size: 20px; + margin: 0 .3125rem; + padding: 0; + display: flex; + align-items: center; + pointer-events: none; + + .checkbox-toggle { + --offset: 3px; + --toggle-width: 1.9375rem; + width: var(--toggle-width); + height: .875rem; + background-color: var(--secondary-color); + border-radius: $border-radius-big; + position: relative; + display: flex; + align-items: center; + transition: background-color .2s; + margin: 0 var(--offset); + + &:before { + width: 1.25rem; + height: 1.25rem; + border: 2px solid var(--secondary-color); + transition: border-color .2s, transform .2s; + background-color: var(--surface-color); + content: " "; + transform: translateX(calc(var(--offset) * -1)); + border-radius: 50%; + position: absolute; + } + } + + [type="checkbox"]:checked + .checkbox-toggle { + background-color: var(--primary-color); + + &:before { + border-color: var(--primary-color); + transform: translateX(calc(var(--toggle-width) - 1.25rem + var(--offset))); + } + } +} diff --git a/src/scss/partials/_emojiDropdown.scss b/src/scss/partials/_emojiDropdown.scss index 556c9a53..f7cf5806 100644 --- a/src/scss/partials/_emojiDropdown.scss +++ b/src/scss/partials/_emojiDropdown.scss @@ -18,13 +18,13 @@ @include respond-to(esg-top) { position: absolute !important; left: $chat-padding; - bottom: 85px; + bottom: 84px; width: 420px !important; height: 420px; max-height: 420px; box-shadow: 0px 5px 10px 5px rgba(16, 35, 47, .14); z-index: 3; - border-radius: 10px; + border-radius: $border-radius-medium; transition: transform var(--esg-transition), opacity var(--esg-transition); transform: scale(0); opacity: 0; diff --git a/src/scss/partials/_leftSidebar.scss b/src/scss/partials/_leftSidebar.scss index d241f936..3e960c1d 100644 --- a/src/scss/partials/_leftSidebar.scss +++ b/src/scss/partials/_leftSidebar.scss @@ -60,17 +60,17 @@ .menu-horizontal-scrollable { z-index: 1; background-color: var(--surface-color); - border-bottom: 1px solid var(--border-color); position: relative; box-shadow: 0px 1px 5px -1px rgba(0, 0, 0, .16); top: unset; + height: 43px; .scrollable { position: relative; } .menu-horizontal-div { - border-bottom: none; + height: 43px; position: relative !important; justify-content: flex-start; @@ -107,8 +107,8 @@ } &:not(.hide) + .scrollable { - top: 44px; - height: calc(100% - 44px); + top: 43px; + height: calc(100% - 43px); #folders-container { margin-top: .5rem; @@ -116,11 +116,13 @@ } } - .folders-tabs-scrollable .menu-horizontal-div-item:first-child { - margin-left: .6875rem; - - @include respond-to(handhelds) { - margin-left: .1875rem; + .folders-tabs-scrollable { + .menu-horizontal-div-item:first-child { + margin-left: .6875rem; + + @include respond-to(handhelds) { + margin-left: .1875rem; + } } } @@ -791,6 +793,12 @@ .sidebar-left { &-section { + /* padding-bottom: .75rem; + + @include respond-to(handhelds) { + padding-bottom: .5rem; + } */ + padding: .5rem 0 1rem; @include respond-to(handhelds) { @@ -798,10 +806,8 @@ } &-content { - margin: 0 .5rem; - - @include respond-to(handhelds) { - margin: 0 .25rem; + @include respond-to(not-handhelds) { + margin: 0 .5rem; } > .btn-primary { @@ -813,6 +819,13 @@ left: auto; } } + + @include respond-to(handhelds) { + > .checkbox-ripple, + > .btn-primary { + border-radius: 0; + } + } } &-name { @@ -841,6 +854,7 @@ margin: 0; } + // * comment later &:first-child:not(.no-delimiter) { padding-top: 0; } @@ -1020,8 +1034,6 @@ height: 66px; padding-top: 9px; padding-bottom: 9px; - - border-radius: $border-radius-medium; } .user-caption { @@ -1035,7 +1047,10 @@ ul { margin-top: .3125rem; - padding: 0 .6875rem; + + @include respond-to(not-handhelds) { + padding: 0 .6875rem; + } } } diff --git a/src/scss/partials/_mediaViewer.scss b/src/scss/partials/_mediaViewer.scss index 38073303..e1fc3a6f 100644 --- a/src/scss/partials/_mediaViewer.scss +++ b/src/scss/partials/_mediaViewer.scss @@ -282,11 +282,15 @@ } &.active { - transition: var(--open-duration) transform, var(--open-duration) border-radius; + transition: transform var(--open-duration), border-radius var(--open-duration), opacity var(--open-duration) calc(var(--open-duration) / 8); + } + + &.active.opening { + transition: transform var(--open-duration), border-radius var(--open-duration), opacity var(--open-duration) 0s; } &.moving { - transition: var(--move-duration) transform ease; + transition: transform var(--move-duration) ease; } /* &.center { diff --git a/src/scss/partials/_profile.scss b/src/scss/partials/_profile.scss index d1a163a4..16381be6 100644 --- a/src/scss/partials/_profile.scss +++ b/src/scss/partials/_profile.scss @@ -8,7 +8,8 @@ &-avatars { &-container { width: 100%; - height: 26.25rem; + padding-bottom: 100%; + //height: 26.25rem; //overflow: hidden; position: relative; cursor: pointer; @@ -23,14 +24,14 @@ } &-avatars { - width: inherit; - height: inherit; + width: 100%; + height: 100%; display: flex; flex-wrap: nowrap; transform: translateZ(-1px) scale(2); transform-origin: left top; transition: transform .2s ease-in-out; - position: relative; + position: absolute; &:before { content: " "; @@ -57,14 +58,14 @@ } } - /* &-gradient { + &-gradient { position: absolute; left: 0; right: 0; bottom: 0; height: 80px; - background: linear-gradient(360deg, var(--secondary-color) 8.98%, rgba(0, 0, 0, 0) 100%); - } */ + background: linear-gradient(360deg, rgba(0, 0, 0, .3) 8.98%, rgba(0, 0, 0, 0) 100%); + } &-info { position: absolute; @@ -74,12 +75,17 @@ align-items: flex-start; left: 1.5rem; bottom: .5625rem; + pointer-events: none; .profile-name, .profile-subtitle { color: #fff; margin: 0; } + .peer-typing-text-dot { + background-color: #fff; + } + .profile-name { margin-bottom: -1px; } @@ -100,6 +106,7 @@ left: .375rem; right: .375rem; height: .125rem; + pointer-events: none; } &-tab { @@ -152,8 +159,21 @@ position: relative; background-color: var(--surface-color); //padding-top: .5625rem; - padding-bottom: 0; + padding-bottom: .5rem; + //margin-bottom: .75rem; + //box-shadow: 0px 1px 5px -1px rgba(0, 0, 0, .16); } + + /* .search-super { + &:before { + content: " "; + height: 12px; + width: 100%; + background-color: var(--background-color-true); + position: absolute; + top: -12px; + } + } */ } &-container { diff --git a/src/scss/partials/_rightSidebar.scss b/src/scss/partials/_rightSidebar.scss index b7247257..b500da36 100644 --- a/src/scss/partials/_rightSidebar.scss +++ b/src/scss/partials/_rightSidebar.scss @@ -100,6 +100,7 @@ } .shared-media-container { + //background-color: var(--background-color-true) !important; /* .search-super { top: 100%; min-height: calc((var(--vh, 1vh) * 100) - 100% - 56px); @@ -109,8 +110,12 @@ } } */ .scrollable { - perspective: 1px; + perspective: 0px; perspective-origin: left top; + + &.parallax { + perspective: 1px; + } } .search-super { @@ -156,6 +161,23 @@ } } } + + .search-super { + .menu-horizontal-div-item { + height: 3.5rem; + padding-top: 0; + padding-bottom: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + line-height: var(--line-height); + } + + .menu-horizontal-div i { + bottom: calc(-.625rem - 7px); + } + } } .search-super { @@ -197,10 +219,10 @@ flex: 1 1 auto; //margin-top: 36px; - i { + /* i { padding-right: 1.5rem !important; margin-left: -.75rem !important; - } + } */ } &-tabs-scrollable { @@ -211,16 +233,22 @@ top: 0px; z-index: 2; background-color: var(--surface-color); + height: 3.5rem; &:before { position: absolute; width: 100%; height: 1px; left: 0; - top: -1px; + top: 0; background-color: inherit; display: block; content: " "; + z-index: -1; + + .search-super.is-full-viewport & { + top: -1px; + } } .scrollable { @@ -336,16 +364,12 @@ &-content-media &-month { &-items { width: 100%; - padding: 7.5px; + padding-top: 1px; display: grid; grid-template-columns: repeat(3,1fr); grid-auto-rows: 1fr; - grid-gap: 3.5px; - - @include respond-to(handhelds) { - padding: 7.5px 7.5px 7.5px 6.5px; - } + grid-gap: 1px; } } diff --git a/src/scss/partials/_ripple.scss b/src/scss/partials/_ripple.scss index 051ce168..6ec422fe 100644 --- a/src/scss/partials/_ripple.scss +++ b/src/scss/partials/_ripple.scss @@ -116,4 +116,4 @@ to { transform: scale(2); } -} \ No newline at end of file +} diff --git a/src/scss/partials/_row.scss b/src/scss/partials/_row.scss new file mode 100644 index 00000000..5f5325c2 --- /dev/null +++ b/src/scss/partials/_row.scss @@ -0,0 +1,110 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +.row { + min-height: 3.5rem; + position: relative; + padding: .6875rem 1rem; + display: flex; + flex-direction: column; + justify-content: center; + + a { + position: relative; + z-index: 1; + } + + &-title-row { + display: flex; + justify-content: space-between; + align-items: center; + order: 0; + + .row-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 1 auto; + } + } + + &-title { + color: var(--primary-text-color); + line-height: var(--line-height); + order: 0; + + @include text-overflow(false); + + &-right { + flex: 0 0 auto !important; + margin-left: 1rem; + } + } + + &-midtitle { + font-size: .875rem; + order: 1; + } + + &-with-padding { + padding-left: 4.5rem; + + .row-title.tgico:before { + position: absolute; + left: 1rem; + font-size: 1.5rem; + color: var(--secondary-text-color); + pointer-events: none; + margin-top: -.125rem; + } + + .row-subtitle:not(:empty) + .row-title.tgico:before { + margin-top: .25rem; + } + } + + &-clickable { + cursor: pointer; + overflow: hidden; + + @include respond-to(not-handhelds) { + border-radius: $border-radius-medium; + } + } + + .radio-field-main, .checkbox-field { + padding-left: 3.375rem; + margin-left: -3.375rem; + } + + .checkbox-field { + margin-right: 0; + height: auto; + + .checkbox-caption { + padding-left: 0; + } + } + + .checkbox-field-toggle { + margin: 0; + margin-right: .125rem; + padding: 0; + } + + &-subtitle { + color: var(--secondary-text-color) !important; + font-size: .875rem !important; + line-height: var(--line-height); + margin-top: .125rem; + margin-bottom: .0625rem; + order: 1; + + &:empty { + display: none; + } + } +} diff --git a/src/scss/partials/_slider.scss b/src/scss/partials/_slider.scss index 90d2d41b..bfb1d4b1 100644 --- a/src/scss/partials/_slider.scss +++ b/src/scss/partials/_slider.scss @@ -4,6 +4,23 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +.menu-horizontal-scrollable { + &:after { + content: " "; + position: absolute; + height: 1px; + border-bottom: 1px solid var(--border-color); + bottom: 0; + left: 0; + right: 0; + z-index: -1; + } + + .menu-horizontal-div { + border-bottom: none; + } +} + .menu-horizontal-div { width: 100%; display: flex; @@ -64,7 +81,7 @@ i { position: absolute; - bottom: calc(-.625rem - 2px); + bottom: calc(-.625rem - 3px); left: 0; opacity: 0; background-color: var(--primary-color); @@ -72,8 +89,8 @@ width: 100%; border-radius: .1875rem .1875rem 0 0; pointer-events: none; - padding-right: .5rem; - margin-left: -.25rem; + /* padding-right: .5rem; + margin-left: -.25rem; */ box-sizing: content-box; transform-origin: left; z-index: 1; diff --git a/src/scss/partials/pages/_pages.scss b/src/scss/partials/pages/_pages.scss index f5c8c509..327d7a25 100644 --- a/src/scss/partials/pages/_pages.scss +++ b/src/scss/partials/pages/_pages.scss @@ -43,8 +43,7 @@ flex-direction: column; position: relative; - &:before, &:after { - content: " "; + .auth-placeholder { flex: 1; min-height: 3rem; /* height: 105px; */ @@ -52,7 +51,7 @@ } @media screen and (max-height: 810px) { - &:after { + .auth-placeholder:last-child { display: none; } } diff --git a/src/scss/partials/popups/_mediaAttacher.scss b/src/scss/partials/popups/_mediaAttacher.scss index f20eb26a..dbe3b0c0 100644 --- a/src/scss/partials/popups/_mediaAttacher.scss +++ b/src/scss/partials/popups/_mediaAttacher.scss @@ -64,13 +64,14 @@ position: relative; .btn-primary { - width: 79px; + width: auto; height: 36px; font-size: 14px; font-weight: normal; - padding: 0; + padding: 0 1.375rem; margin-top: -3px; border-radius: $border-radius-medium; + text-transform: uppercase; } } @@ -81,7 +82,7 @@ &-title { flex: 1; - padding: 0 2rem 0 1.5rem; + padding-left: 1.5rem; margin: 0; margin-top: -3px; font-size: 1.25rem; diff --git a/src/scss/style.scss b/src/scss/style.scss index ae700c96..c6355ae8 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -118,7 +118,7 @@ $chat-padding-handhelds: .5rem; :root { // * Day theme --body-background-color: #fff; - //--background-color: #f4f4f5; + --background-color-true: #f4f4f5; --background-color: #fff; --border-color: #dfe1e5; --surface-color: #fff; @@ -136,6 +136,8 @@ $chat-padding-handhelds: .5rem; @include splitColor(danger-color, #df3f40, true, false); --avatar-online-color: #0ac630; + --avatar-color-top: var(--peer-avatar-blue-top); + --avatar-color-bottom: var(--peer-avatar-blue-bottom); --chatlist-status-color: var(--avatar-online-color); --chatlist-pinned-color: #a2abb2; --badge-text-color: #fff; @@ -159,7 +161,7 @@ html.night { //:root { // * Night theme --body-background-color: #181818; - //--background-color: #181818; + --background-color-true: #181818; --background-color: #212121; --border-color: #0f0f0f; --surface-color: #212121; @@ -177,6 +179,8 @@ html.night { @include splitColor(danger-color, #ff595a, true, false); --avatar-online-color: #0ac630; + --avatar-color-top: var(--peer-avatar-violet-top); + --avatar-color-bottom: var(--peer-avatar-violet-bottom); --chatlist-status-color: var(--primary-color); --chatlist-pinned-color: var(--secondary-color); --badge-text-color: #fff; @@ -231,6 +235,7 @@ html.night { @import "partials/peerTyping"; @import "partials/poll"; @import "partials/transition"; +@import "partials/row"; @import "partials/popups/popup"; @import "partials/popups/editAvatar"; @@ -1009,94 +1014,6 @@ middle-ellipsis-element { } } -.row { - min-height: 3.5rem; - position: relative; - padding: .6875rem 1rem; - display: flex; - flex-direction: column; - justify-content: center; - - a { - position: relative; - z-index: 1; - } - - &-title-row { - display: flex; - justify-content: space-between; - align-items: center; - - .row-title { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1 1 auto; - } - } - - &-title { - color: var(--primary-text-color); - line-height: var(--line-height); - - @include text-overflow(false); - - &-right { - flex: 0 0 auto !important; - margin-left: 1rem; - } - } - - &-midtitle { - font-size: .875rem; - } - - &-with-padding { - padding-left: 4.5rem; - - .row-title.tgico:before { - position: absolute; - left: 1rem; - font-size: 1.5rem; - margin-top: .25rem; - color: var(--secondary-text-color); - pointer-events: none; - } - } - - &-clickable { - cursor: pointer; - border-radius: $border-radius-medium; - overflow: hidden; - } - - .radio-field-main, .checkbox-field { - padding-left: 3.375rem; - margin-left: -3.375rem; - } - - .checkbox-field { - margin-right: 0; - height: auto; - - .checkbox-caption { - padding-left: 0; - } - } - - &-subtitle { - color: var(--secondary-text-color) !important; - font-size: .875rem !important; - line-height: var(--line-height); - margin-top: .125rem; - margin-bottom: .0625rem; - - &:empty { - display: none; - } - } -} - .hover-effect { @include hover-background-effect(); } @@ -1193,3 +1110,21 @@ middle-ellipsis-element { } } } + +.gradient-delimiter { + width: 100%; + height: .75rem; + display: flex; + background-color: var(--background-color-true); + position: relative; + + &:before { + content: " "; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: linear-gradient(180deg, rgba(0, 0, 0, .06) 0%, rgba(0, 0, 0, 0) 20%, rgba(0, 0, 0, 0) 94%, rgba(0, 0, 0, .06) 100%); + } +}