Browse Source

Multiselect: forward and delete

Chat input helper is now clickable
Popup stickers lower resolution for handhelds
master
morethanwords 4 years ago
parent
commit
baf1f78618
  1. 16
      src/components/appSelectPeers.ts
  2. 84
      src/components/chat/contextMenu.ts
  3. 56
      src/components/chat/input.ts
  4. 76
      src/components/chat/selection.ts
  5. 15
      src/components/popup.ts
  6. 65
      src/components/popupDeleteMessages.ts
  7. 11
      src/components/popupForward.ts
  8. 18
      src/components/popupStickers.ts
  9. 4
      src/components/wrappers.ts
  10. 2
      src/helpers/touchSupport.ts
  11. 2
      src/index.hbs
  12. 56
      src/lib/appManagers/appImManager.ts
  13. 15
      src/lib/appManagers/appMessagesManager.ts
  14. 10
      src/lib/richtextprocessor.ts
  15. 11
      src/lib/utils.ts
  16. 8
      src/scss/components/_global.scss
  17. 168
      src/scss/partials/_chat.scss
  18. 34
      src/scss/partials/_leftSidebar.scss
  19. 4
      src/scss/partials/_selector.scss
  20. 6
      src/scss/partials/popups/_forward.scss
  21. 8
      src/scss/partials/popups/_stickers.scss
  22. 22
      src/scss/style.scss

16
src/components/appSelectPeers.ts

@ -37,12 +37,21 @@ export default class AppSelectPeers { @@ -37,12 +37,21 @@ export default class AppSelectPeers {
private loadedWhat: Partial<{[k in 'dialogs' | 'archived' | 'contacts']: true}> = {};
private renderedPeerIDs: Set<number> = new Set();
constructor(private appendTo: HTMLElement, private onChange?: (length: number) => void, private peerType: PeerType[] = ['dialogs'], onFirstRender?: () => void, private renderResultsFunc?: (peerIDs: number[]) => void, private chatRightsAction?: ChatRights, private multiSelect = true) {
this.container.classList.add('selector');
if(!this.renderResultsFunc) {
this.renderResultsFunc = this.renderResults;
}
const f = (renderResultsFunc || this.renderResults).bind(this);
this.renderResultsFunc = (peerIDs: number[]) => {
peerIDs = peerIDs.filter(peerID => {
const notRendered = !this.renderedPeerIDs.has(peerID);
if(notRendered) this.renderedPeerIDs.add(peerID);
return notRendered;
});
return f(peerIDs);
};
this.input = document.createElement('input');
this.input.classList.add('selector-search-input');
@ -130,6 +139,7 @@ export default class AppSelectPeers { @@ -130,6 +139,7 @@ export default class AppSelectPeers {
this.promise = null;
this.list.innerHTML = '';
this.query = value;
this.renderedPeerIDs.clear();
//console.log('selectPeers input:', this.query);
this.getMoreResults();

84
src/components/chat/contextMenu.ts

@ -1,13 +1,15 @@ @@ -1,13 +1,15 @@
import { isTouchSupported } from "../../helpers/touchSupport";
import appChatsManager from "../../lib/appManagers/appChatsManager";
import appImManager from "../../lib/appManagers/appImManager";
import appMessagesManager from "../../lib/appManagers/appMessagesManager";
import appPeersManager from "../../lib/appManagers/appPeersManager";
import appPollsManager, { Poll } from "../../lib/appManagers/appPollsManager";
import $rootScope from "../../lib/rootScope";
import { findUpClassName } from "../../lib/utils";
import { cancelEvent, cancelSelection, findUpClassName } from "../../lib/utils";
import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu";
import { attachContextMenuListener, openBtnMenu, positionMenu } from "../misc";
import { PopupButton } from "../popup";
import PopupDeleteMessages from "../popupDeleteMessages";
import PopupForward from "../popupForward";
import PopupPeer from "../popupPeer";
import appSidebarRight from "../sidebarRight";
@ -21,7 +23,7 @@ export default class ChatContextMenu { @@ -21,7 +23,7 @@ export default class ChatContextMenu {
public msgID: number;
constructor(private attachTo: HTMLElement) {
attachContextMenuListener(attachTo, (e) => {
const onContextMenu = (e: MouseEvent | Touch) => {
if(this.init) {
this.init();
this.init = null;
@ -69,7 +71,29 @@ export default class ChatContextMenu { @@ -69,7 +71,29 @@ export default class ChatContextMenu {
});
/////this.log('contextmenu', e, bubble, msgID, side);
};
if(isTouchSupported) {
attachTo.addEventListener('click', (e) => {
//const good = !!findUpClassName(e.target, 'message') || !!findUpClassName(e.target, 'bubble__container');
const className = (e.target as HTMLElement).className;
const good = ['bubble', 'bubble__container', 'message', 'time', 'inner'].find(c => className.includes(c));
if(good) {
onContextMenu(e);
}
});
attachContextMenuListener(attachTo, (e) => {
if(appImManager.chatSelection.isSelecting) return;
cancelSelection();
cancelEvent(e as any);
let bubble = findUpClassName(e.target, 'bubble');
if(bubble) {
appImManager.chatSelection.toggleByBubble(bubble);
}
});
} else attachContextMenuListener(attachTo, onContextMenu);
}
private init = () => {
@ -145,7 +169,7 @@ export default class ChatContextMenu { @@ -145,7 +169,7 @@ export default class ChatContextMenu {
icon: 'delete danger',
text: 'Delete',
onClick: this.onDeleteClick,
verify: () => this.peerID > 0 || appMessagesManager.getMessage(this.msgID).fromID == $rootScope.myID || appChatsManager.hasRights(-this.peerID, 'deleteRevoke')
verify: () => appMessagesManager.canDeleteMessage(this.msgID)
}];
this.element = ButtonMenu(this.buttons);
@ -217,58 +241,6 @@ export default class ChatContextMenu { @@ -217,58 +241,6 @@ export default class ChatContextMenu {
};
private onDeleteClick = () => {
const peerID = $rootScope.selectedPeerID;
const firstName = appPeersManager.getPeerTitle(peerID, false, true);
const msgID = this.msgID;
const callback = (revoke: boolean) => {
appMessagesManager.deleteMessages([msgID], revoke);
};
let title: string, description: string, buttons: PopupButton[];
title = 'Delete Message?';
description = `Are you sure you want to delete this message?`;
if(peerID == $rootScope.myID) {
buttons = [{
text: 'DELETE',
isDanger: true,
callback: () => callback(false)
}];
} else {
buttons = [{
text: 'DELETE JUST FOR ME',
isDanger: true,
callback: () => callback(false)
}];
if(peerID > 0) {
buttons.push({
text: 'DELETE FOR ME AND ' + firstName,
isDanger: true,
callback: () => callback(true)
});
} else if(appChatsManager.hasRights(-peerID, 'deleteRevoke')) {
buttons.push({
text: 'DELETE FOR ALL',
isDanger: true,
callback: () => callback(true)
});
}
}
buttons.push({
text: 'CANCEL',
isCancel: true
});
const popup = new PopupPeer('popup-delete-chat', {
peerID: peerID,
title: title,
description: description,
buttons: buttons
});
popup.show();
new PopupDeleteMessages([this.msgID]);
};
}

56
src/components/chat/input.ts

@ -11,10 +11,11 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; @@ -11,10 +11,11 @@ import apiManager from "../../lib/mtproto/mtprotoworker";
import opusDecodeController from "../../lib/opusDecodeController";
import { RichTextProcessor } from "../../lib/richtextprocessor";
import $rootScope from '../../lib/rootScope';
import { cancelEvent, getRichValue } from "../../lib/utils";
import { cancelEvent, findUpClassName, getRichValue } from "../../lib/utils";
import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu';
import emoticonsDropdown from "../emoticonsDropdown";
import PopupCreatePoll from "../popupCreatePoll";
import PopupForward from '../popupForward';
import PopupNewMedia from '../popupNewMedia';
import { ripple } from '../ripple';
import Scrollable from "../scrollable";
@ -205,7 +206,7 @@ export class ChatInput { @@ -205,7 +206,7 @@ export class ChatInput {
if(this.lastUrl != url) return;
//console.log('got webpage: ', webpage);
this.setTopInfo('webpage', () => {}, webpage.site_name || webpage.title, webpage.description || webpage.url);
this.setTopInfo('webpage', () => {}, webpage.site_name || webpage.title || 'Webpage', webpage.description || webpage.url || '');
delete this.noWebPage;
this.willSendWebPage = webpage;
@ -267,7 +268,7 @@ export class ChatInput { @@ -267,7 +268,7 @@ export class ChatInput {
//console.log('messageInput paste', text, entities);
entities = entities.filter(e => e._ == 'messageEntityEmoji' || e._ == 'messageEntityLinebreak');
//text = RichTextProcessor.wrapEmojiText(text);
text = RichTextProcessor.wrapRichText(text, {entities});
text = RichTextProcessor.wrapRichText(text, {entities, noLinks: true});
// console.log('messageInput paste after', text);
@ -453,7 +454,7 @@ export class ChatInput { @@ -453,7 +454,7 @@ export class ChatInput {
return; */
let perf = performance.now();
//let perf = performance.now();
opusDecodeController.decode(typedArray, true).then(result => {
//console.log('WAVEFORM!:', /* waveform, */performance.now() - perf);
@ -509,6 +510,33 @@ export class ChatInput { @@ -509,6 +510,33 @@ export class ChatInput {
this.clearHelper();
this.updateSendBtn();
});
let d = false;
this.replyElements.container.addEventListener('click', (e) => {
if(!findUpClassName(e.target, 'reply-wrapper')) return;
if(this.helperType == 'forward') {
if(d) return;
d = true;
const mids = this.forwardingMids.slice();
const helperFunc = this.helperFunc;
this.clearHelper();
let selected = false;
new PopupForward(mids, () => {
selected = true;
}, () => {
d = false;
if(!selected) {
helperFunc();
}
});
} else if(this.helperType == 'reply') {
appImManager.setPeer($rootScope.selectedPeerID, this.replyToMsgID);
} else if(this.helperType == 'edit') {
appImManager.setPeer($rootScope.selectedPeerID, this.editMsgID);
}
});
}
private isInputEmpty() {
@ -579,8 +607,12 @@ export class ChatInput { @@ -579,8 +607,12 @@ export class ChatInput {
});
}
// * wait for sendText set messageID for invokeAfterMsg
if(this.forwardingMids.length) {
appMessagesManager.forwardMessages(appImManager.peerID, this.forwardingMids);
const mids = this.forwardingMids.slice();
setTimeout(() => {
appMessagesManager.forwardMessages(appImManager.peerID, mids);
}, 0);
}
this.onMessageSent();
@ -629,7 +661,7 @@ export class ChatInput { @@ -629,7 +661,7 @@ export class ChatInput {
this.setTopInfo('forward', f, title, mids.length + ' forwarded messages');
}
this.forwardingMids = mids;
this.forwardingMids = mids.slice();
};
f();
@ -640,6 +672,12 @@ export class ChatInput { @@ -640,6 +672,12 @@ export class ChatInput {
this.messageInput.innerText = '';
}
if(type) {
this.lastUrl = '';
delete this.noWebPage;
this.willSendWebPage = null;
}
this.replyToMsgID = 0;
this.forwardingMids.length = 0;
this.editMsgID = 0;
@ -660,9 +698,13 @@ export class ChatInput { @@ -660,9 +698,13 @@ export class ChatInput {
}
this.chatInput.parentElement.classList.add('is-helper-active');
/* const scroll = appImManager.scrollable;
if(scroll.isScrolledDown && !scroll.scrollLocked && !appImManager.messagesQueuePromise && !appImManager.setPeerPromise) {
scroll.scrollTo(scroll.scrollHeight, 'top', true, true, 200);
} */
if(input !== undefined) {
this.messageInput.innerHTML = input ? RichTextProcessor.wrapRichText(input) : '';
this.messageInput.innerHTML = input ? RichTextProcessor.wrapRichText(input, {noLinks: true}) : '';
}
setTimeout(() => {

76
src/components/chat/selection.ts

@ -1,10 +1,11 @@ @@ -1,10 +1,11 @@
import { isTouchSupported } from "../../helpers/touchSupport";
import type { AppImManager } from "../../lib/appManagers/appImManager";
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import { cancelEvent, cancelSelection, findUpClassName } from "../../lib/utils";
import { cancelEvent, cancelSelection, findUpClassName, getSelectedText } from "../../lib/utils";
import Button from "../button";
import ButtonIcon from "../buttonIcon";
import CheckboxField from "../checkbox";
import PopupDeleteMessages from "../popupDeleteMessages";
import PopupForward from "../popupForward";
import { toast } from "../toast";
@ -34,7 +35,7 @@ const SetTransition = (element: HTMLElement, className: string, forwards: boolea @@ -34,7 +35,7 @@ const SetTransition = (element: HTMLElement, className: string, forwards: boolea
};
const MAX_SELECTION_LENGTH = 100;
const MIN_CLICK_MOVE = 32; // minimum bubble height
//const MIN_CLICK_MOVE = 32; // minimum bubble height
export default class ChatSelection {
public selectedMids: Set<number> = new Set();
@ -42,14 +43,25 @@ export default class ChatSelection { @@ -42,14 +43,25 @@ export default class ChatSelection {
private selectionContainer: HTMLElement;
private selectionCountEl: HTMLElement;
private selectionForwardBtn: HTMLElement;
private selectionDeleteBtn: HTMLElement;
constructor(private appImManager: AppImManager, private appMessagesManager: AppMessagesManager) {
if(isTouchSupported) return;
public selectedText: string;
constructor(private appImManager: AppImManager, private appMessagesManager: AppMessagesManager) {
const bubblesContainer = appImManager.bubblesContainer;
if(isTouchSupported) {
bubblesContainer.addEventListener('touchend', (e) => {
if(!this.isSelecting) return;
this.selectedText = getSelectedText();
});
return;
}
bubblesContainer.addEventListener('mousedown', (e) => {
//console.log('selection mousedown', e);
if(e.button != 0) { // LEFT BUTTON
if(e.button != 0 || (!this.selectedMids.size && !(e.target as HTMLElement).classList.contains('bubble'))) { // LEFT BUTTON
return;
}
@ -169,6 +181,28 @@ export default class ChatSelection { @@ -169,6 +181,28 @@ export default class ChatSelection {
if(!this.selectedMids.size) return;
this.selectionCountEl.innerText = this.selectedMids.size + ' Message' + (this.selectedMids.size == 1 ? '' : 's');
let cantForward = false, cantDelete = false;
for(const mid of this.selectedMids.values()) {
const message = this.appMessagesManager.getMessage(mid);
if(!cantForward) {
if(message.action) {
cantForward = true;
}
}
if(!cantDelete) {
const canDelete = this.appMessagesManager.canDeleteMessage(mid);
if(!canDelete) {
cantDelete = true;
}
}
if(cantForward && cantDelete) break;
}
this.selectionForwardBtn.toggleAttribute('disabled', cantForward);
this.selectionDeleteBtn.toggleAttribute('disabled', cantDelete);
}
public toggleSelection(toggleCheckboxes = true) {
@ -180,11 +214,23 @@ export default class ChatSelection { @@ -180,11 +214,23 @@ export default class ChatSelection {
const bubblesContainer = this.appImManager.bubblesContainer;
//bubblesContainer.classList.toggle('is-selecting', !!this.selectedMids.size);
/* if(bubblesContainer.classList.contains('is-chat-input-hidden')) {
const scrollable = this.appImManager.scrollable;
if(scrollable.isScrolledDown) {
scrollable.scrollTo(scrollable.scrollHeight, 'top', true, true, 200);
}
} */
SetTransition(bubblesContainer, 'is-selecting', !!this.selectedMids.size, 200, () => {
if(!this.isSelecting) {
this.selectionContainer.remove();
this.selectionContainer = null;
this.selectionContainer = this.selectionForwardBtn = this.selectionDeleteBtn = null;
this.selectedText = undefined;
}
window.requestAnimationFrame(() => {
this.appImManager.onScroll();
});
});
//const chatInput = this.appImManager.chatInput;
@ -201,19 +247,23 @@ export default class ChatSelection { @@ -201,19 +247,23 @@ export default class ChatSelection {
this.selectionCountEl = document.createElement('div');
this.selectionCountEl.classList.add('selection-container-count');
const btnForward = Button('btn-primary btn-transparent', {icon: 'forward'});
btnForward.append('Forward');
btnForward.addEventListener('click', () => {
this.selectionForwardBtn = Button('btn-primary btn-transparent selection-container-forward', {icon: 'forward'});
this.selectionForwardBtn.append('Forward');
this.selectionForwardBtn.addEventListener('click', () => {
new PopupForward([...this.selectedMids], () => {
this.cancelSelection();
});
});
const btnDelete = Button('btn-primary btn-transparent danger', {icon: 'delete'});
btnDelete.append('Delete');
this.selectionDeleteBtn = Button('btn-primary btn-transparent danger selection-container-delete', {icon: 'delete'});
this.selectionDeleteBtn.append('Delete');
this.selectionDeleteBtn.addEventListener('click', () => {
new PopupDeleteMessages([...this.selectedMids], () => {
this.cancelSelection();
});
});
this.selectionContainer.append(btnCancel, this.selectionCountEl, btnForward, btnDelete);
this.selectionContainer.append(btnCancel, this.selectionCountEl, this.selectionForwardBtn, this.selectionDeleteBtn);
inputMessageDiv.append(this.selectionContainer);
}

15
src/components/popup.ts

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import $rootScope from "../lib/rootScope";
import { cancelEvent } from "../lib/utils";
import AvatarElement from "./avatar";
import { cancelEvent, findUpClassName } from "../lib/utils";
import { ripple } from "./ripple";
export class PopupElement {
@ -16,7 +15,7 @@ export class PopupElement { @@ -16,7 +15,7 @@ export class PopupElement {
protected onCloseAfterTimeout: () => void;
protected onEscape: () => boolean = () => true;
constructor(className: string, buttons?: Array<PopupButton>, options: Partial<{closable: boolean, withConfirm: string, body: boolean}> = {}) {
constructor(className: string, buttons?: Array<PopupButton>, options: Partial<{closable: true, overlayClosable: true, withConfirm: string, body: true}> = {}) {
this.element.classList.add('popup');
this.element.className = 'popup' + (className ? ' ' + className : '');
this.container.classList.add('popup-container', 'z-depth-1');
@ -33,6 +32,16 @@ export class PopupElement { @@ -33,6 +32,16 @@ export class PopupElement {
this.header.prepend(this.closeBtn);
this.closeBtn.addEventListener('click', this.destroy, {once: true});
if(options.overlayClosable) {
const onOverlayClick = (e: MouseEvent) => {
if(!findUpClassName(e.target, 'popup-container')) {
this.closeBtn.click();
}
};
this.element.addEventListener('click', onOverlayClick, {once: true});
}
}
window.addEventListener('keydown', this._onKeyDown, {capture: true});

65
src/components/popupDeleteMessages.ts

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
import appChatsManager from "../lib/appManagers/appChatsManager";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appPeersManager from "../lib/appManagers/appPeersManager";
import $rootScope from "../lib/rootScope";
import { PopupButton } from "./popup";
import PopupPeer from "./popupPeer";
export default class PopupDeleteMessages {
constructor(mids: number[], onConfirm?: () => void) {
const peerID = $rootScope.selectedPeerID;
const firstName = appPeersManager.getPeerTitle(peerID, false, true);
mids = mids.slice();
const callback = (revoke: boolean) => {
onConfirm && onConfirm();
appMessagesManager.deleteMessages(mids, revoke);
};
let title: string, description: string, buttons: PopupButton[];
title = `Delete Message${mids.length == 1 ? '' : 's'}?`;
description = `Are you sure you want to delete ${mids.length == 1 ? 'this message' : 'these messages'}?`;
if(peerID == $rootScope.myID) {
buttons = [{
text: 'DELETE',
isDanger: true,
callback: () => callback(false)
}];
} else {
buttons = [{
text: 'DELETE JUST FOR ME',
isDanger: true,
callback: () => callback(false)
}];
if(peerID > 0) {
buttons.push({
text: 'DELETE FOR ME AND ' + firstName,
isDanger: true,
callback: () => callback(true)
});
} else if(appChatsManager.hasRights(-peerID, 'deleteRevoke')) {
buttons.push({
text: 'DELETE FOR ALL',
isDanger: true,
callback: () => callback(true)
});
}
}
buttons.push({
text: 'CANCEL',
isCancel: true
});
const popup = new PopupPeer('popup-delete-chat', {
peerID: peerID,
title: title,
description: description,
buttons: buttons
});
popup.show();
}
}

11
src/components/popupForward.ts

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import { isTouchSupported } from "../helpers/touchSupport";
import appImManager from "../lib/appManagers/appImManager";
import AppSelectPeers from "./appSelectPeers";
import { PopupElement } from "./popup";
@ -6,8 +7,10 @@ export default class PopupForward extends PopupElement { @@ -6,8 +7,10 @@ export default class PopupForward extends PopupElement {
private selector: AppSelectPeers;
//private scrollable: Scrollable;
constructor(mids: number[], onSelect?: () => Promise<void> | void) {
super('popup-forward', null, {closable: true, body: true});
constructor(mids: number[], onSelect?: () => Promise<void> | void, onClose?: () => void) {
super('popup-forward', null, {closable: true, overlayClosable: true, body: true});
if(onClose) this.onClose = onClose;
this.selector = new AppSelectPeers(this.body, async() => {
const peerID = this.selector.getSelected()[0];
@ -21,6 +24,10 @@ export default class PopupForward extends PopupElement { @@ -21,6 +24,10 @@ export default class PopupForward extends PopupElement {
appImManager.chatInputC.initMessagesForward(mids.slice());
}, ['dialogs', 'contacts'], () => {
this.show();
if(!isTouchSupported) {
this.selector.input.focus();
}
}, null, 'send', false);
//this.scrollable = new Scrollable(this.body);

18
src/components/popupStickers.ts

@ -9,6 +9,7 @@ import animationIntersector from "./animationIntersector"; @@ -9,6 +9,7 @@ import animationIntersector from "./animationIntersector";
import { findUpClassName } from "../lib/utils";
import appImManager from "../lib/appManagers/appImManager";
import { StickerSet } from "../layer";
import mediaSizes from "../helpers/mediaSizes";
const ANIMATION_GROUP = 'STICKERS-POPUP';
@ -24,7 +25,7 @@ export default class PopupStickers extends PopupElement { @@ -24,7 +25,7 @@ export default class PopupStickers extends PopupElement {
id: string,
access_hash: string
}) {
super('popup-stickers', null, {closable: true, body: true});
super('popup-stickers', null, {closable: true, overlayClosable: true, body: true});
this.h6 = document.createElement('h6');
this.h6.innerText = 'Loading...';
@ -36,21 +37,12 @@ export default class PopupStickers extends PopupElement { @@ -36,21 +37,12 @@ export default class PopupStickers extends PopupElement {
animationIntersector.checkAnimations(false);
this.stickersFooter.removeEventListener('click', this.onFooterClick);
this.stickersDiv.removeEventListener('click', this.onStickersClick);
this.element.removeEventListener('click', onOverlayClick);
};
this.onCloseAfterTimeout = () => {
animationIntersector.checkAnimations(undefined, ANIMATION_GROUP);
};
const onOverlayClick = (e: MouseEvent) => {
if(!findUpClassName(e.target, 'popup-container')) {
this.closeBtn.click();
}
};
this.element.addEventListener('click', onOverlayClick);
const div = document.createElement('div');
div.classList.add('sticker-set');
@ -130,6 +122,8 @@ export default class PopupStickers extends PopupElement { @@ -130,6 +122,8 @@ export default class PopupStickers extends PopupElement {
const div = document.createElement('div');
div.classList.add('sticker-set-sticker');
const size = mediaSizes.active.esgSticker.width;
wrapSticker({
doc,
div,
@ -137,8 +131,8 @@ export default class PopupStickers extends PopupElement { @@ -137,8 +131,8 @@ export default class PopupStickers extends PopupElement {
group: ANIMATION_GROUP,
play: true,
loop: true,
width: 80,
height: 80
width: size,
height: size
});
this.stickersDiv.append(div);

4
src/components/wrappers.ts

@ -10,7 +10,7 @@ import appMessagesManager from '../lib/appManagers/appMessagesManager'; @@ -10,7 +10,7 @@ import appMessagesManager from '../lib/appManagers/appMessagesManager';
import appPhotosManager, { MyPhoto } from '../lib/appManagers/appPhotosManager';
import LottieLoader from '../lib/lottieLoader';
import VideoPlayer from '../lib/mediaPlayer';
import { formatBytes, getEmojiToneIndex, isInDOM } from "../lib/utils";
import { cancelEvent, formatBytes, getEmojiToneIndex, isInDOM } from "../lib/utils";
import webpWorkerController from '../lib/webp/webpWorkerController';
import animationIntersector from './animationIntersector';
import appMediaPlaybackController from './appMediaPlaybackController';
@ -353,7 +353,7 @@ export function wrapDocument(doc: MyDocument, withTime = false, uploading = fals @@ -353,7 +353,7 @@ export function wrapDocument(doc: MyDocument, withTime = false, uploading = fals
let preloader: ProgressivePreloader;
let download: DownloadBlob;
docDiv.addEventListener('click', () => {
docDiv.addEventListener('click', (e) => {
if(!download) {
if(downloadDiv.classList.contains('downloading')) {
return; // means not ready yet

2
src/helpers/touchSupport.ts

@ -1,2 +1,2 @@ @@ -1,2 +1,2 @@
// @ts-ignore
export const isTouchSupported = ('ontouchstart' in window) || (window.DocumentTouch && document instanceof DocumentTouch);
export const isTouchSupported = ('ontouchstart' in window) || (window.DocumentTouch && document instanceof DocumentTouch)/* || true */;

2
src/index.hbs

@ -440,8 +440,10 @@ @@ -440,8 +440,10 @@
</div>
</div>
<div id="bubbles" class="scrolled-down">
{{!-- <div id="bubbles-transform-helper"> --}}
<div id="bubbles-inner"></div>
<div id="bubbles-go-down" class="tgico-down btn-corner z-depth-1 rp hide"></div>
{{!-- </div> --}}
</div>
<div id="chat-input" style="display: none;">
<div class="chat-input-container">

56
src/lib/appManagers/appImManager.ts

@ -63,6 +63,7 @@ const ANIMATION_GROUP = 'chat'; @@ -63,6 +63,7 @@ const ANIMATION_GROUP = 'chat';
export class AppImManager {
public columnEl = document.getElementById('column-center') as HTMLDivElement;
public backgroundEl = this.columnEl.firstElementChild as HTMLDivElement;
public btnJoin = this.columnEl.querySelector('.chat-join') as HTMLButtonElement;
public btnMute = this.columnEl.querySelector('.chat-mute-button') as HTMLButtonElement;
public avatarEl = document.getElementById('im-avatar') as AvatarElement;
@ -123,7 +124,7 @@ export class AppImManager { @@ -123,7 +124,7 @@ export class AppImManager {
public contextMenu = new ChatContextMenu(this.bubblesContainer);
private setPeerPromise: Promise<boolean> = null;
public setPeerPromise: Promise<boolean> = null;
public bubbleGroups = new BubbleGroups();
@ -137,7 +138,7 @@ export class AppImManager { @@ -137,7 +138,7 @@ export class AppImManager {
private loadedTopTimes = 0;
private loadedBottomTimes = 0;
private messagesQueuePromise: Promise<void> = null;
public messagesQueuePromise: Promise<void> = null;
private messagesQueue: {message: any, bubble: HTMLDivElement, reverse: boolean, promises: Promise<void>[]}[] = [];
private messagesQueueOnRender: () => void = null;
@ -437,9 +438,19 @@ export class AppImManager { @@ -437,9 +438,19 @@ export class AppImManager {
}
// ! Trusted - due to audio autoclick
if(this.chatSelection.isSelecting && !bubble.classList.contains('service') && e.isTrusted) {
if(this.chatSelection.isSelecting && e.isTrusted) {
if(bubble.classList.contains('service') && bubble.dataset.mid === undefined) {
return;
}
cancelEvent(e);
//console.log('bubble click', e);
if(isTouchSupported && this.chatSelection.selectedText) {
this.chatSelection.selectedText = undefined;
return;
}
this.chatSelection.toggleByBubble(bubble);
return;
}
@ -526,6 +537,7 @@ export class AppImManager { @@ -526,6 +537,7 @@ export class AppImManager {
new AppMediaViewer().openMedia(message, targets[idx].element, true,
targets.slice(0, idx), targets.slice(idx + 1)/* , !message.grouped_id */);
cancelEvent(e);
//appMediaViewer.openMedia(message, target as HTMLImageElement);
return;
}
@ -583,7 +595,7 @@ export class AppImManager { @@ -583,7 +595,7 @@ export class AppImManager {
}
//console.log('chatInner click', e);
});
}, {capture: true, passive: false});
this.closeBtn.addEventListener('click', (e) => {
cancelEvent(e);
@ -831,7 +843,7 @@ export class AppImManager { @@ -831,7 +843,7 @@ export class AppImManager {
}
}
public onScroll(e: Event) {
public onScroll() {
if(this.onScrollRAF) window.cancelAnimationFrame(this.onScrollRAF);
// * В таком случае, кнопка не будет моргать если чат в самом низу, и правильно отработает случай написания нового сообщения и проскролла вниз
@ -854,10 +866,10 @@ export class AppImManager { @@ -854,10 +866,10 @@ export class AppImManager {
}
if(this.scrollable.isScrolledDown) {
this.scroll.parentElement.classList.add('scrolled-down');
this.bubblesContainer.classList.add('scrolled-down');
this.scrolledDown = true;
} else if(this.scroll.parentElement.classList.contains('scrolled-down')) {
this.scroll.parentElement.classList.remove('scrolled-down');
} else if(this.bubblesContainer.classList.contains('scrolled-down')) {
this.bubblesContainer.classList.remove('scrolled-down');
this.scrolledDown = false;
}
@ -866,7 +878,7 @@ export class AppImManager { @@ -866,7 +878,7 @@ export class AppImManager {
}
public setScroll() {
this.scrollable = new Scrollable(this.bubblesContainer, 'IM', this.chatInner, 300);
this.scrollable = new Scrollable(this.bubblesContainer/* .firstElementChild */ as HTMLElement, 'IM', this.chatInner, 300);
/* const getScrollOffset = () => {
//return Math.round(Math.max(300, appPhotosManager.windowH / 1.5));
@ -880,14 +892,14 @@ export class AppImManager { @@ -880,14 +892,14 @@ export class AppImManager {
this.scrollable = new Scrollable(this.bubblesContainer, 'y', 'IM', this.chatInner, getScrollOffset()); */
this.scroll = this.scrollable.container;
this.bubblesContainer.append(this.goDownBtn);
this.bubblesContainer/* .firstElementChild */.append(this.goDownBtn);
this.scrollable.onScrolledTop = () => this.loadMoreHistory(true);
this.scrollable.onScrolledBottom = () => this.loadMoreHistory(false);
//this.scrollable.attachSentinels(undefined, 300);
this.scroll.addEventListener('scroll', this.onScroll.bind(this));
this.scroll.parentElement.classList.add('scrolled-down');
this.bubblesContainer.classList.add('scrolled-down');
if(isTouchSupported) {
this.scroll.addEventListener('touchmove', () => {
@ -1289,9 +1301,18 @@ export class AppImManager { @@ -1289,9 +1301,18 @@ export class AppImManager {
this.chatInner.classList.toggle('has-rights', hasRights);
const canWrite = (!isChannel || hasRights) && (peerID < 0 || appUsersManager.canSendToUser(peerID));
this.chatInput.style.display = canWrite ? '' : 'none';
this.chatInner.classList.toggle('is-chat-input-hidden', !canWrite);
//const needToChangeInputDisplay = !(!this.chatInput.classList.contains('is-hidden') && canWrite);
//this.chatInput.style.display = needToChangeInputDisplay ? 'none' : '';
this.chatInput.style.display = '';
this.chatInput.classList.toggle('is-hidden', !canWrite);
this.bubblesContainer.classList.toggle('is-chat-input-hidden', !canWrite);
// const noTransition = [this.columnEl/* appSidebarRight.sidebarEl, this.backgroundEl,
// this.bubblesContainer, this.chatInput, this.chatInner,
// this.chatInputC.replyElements.container */];
// noTransition.forEach(el => {
// el.classList.add('no-transition-all');
// });
this.topbar.classList.remove('is-pinned-shown');
this.topbar.style.display = '';
@ -1308,6 +1329,13 @@ export class AppImManager { @@ -1308,6 +1329,13 @@ export class AppImManager {
this.setPinnedMessage();
window.requestAnimationFrame(() => {
/* noTransition.forEach(el => {
el.classList.remove('no-transition-all');
}); */
/* if(needToChangeInputDisplay) {
this.chatInput.style.display = '';
} */
let title = '';
if(this.peerID == this.myID) title = 'Saved Messages';
else title = appPeersManager.getPeerTitle(this.peerID);

15
src/lib/appManagers/appMessagesManager.ts

@ -2018,7 +2018,7 @@ export class AppMessagesManager { @@ -2018,7 +2018,7 @@ export class AppMessagesManager {
withMyScore: true
}> = {}) {
peerID = appPeersManager.getPeerMigratedTo(peerID) || peerID;
mids = mids.sort((a, b) => a - b);
mids = mids.slice().sort((a, b) => a - b);
const splitted = appMessagesIDsManager.splitMessageIDsByChannels(mids);
const promises: Promise<void>[] = [];
@ -2442,6 +2442,9 @@ export class AppMessagesManager { @@ -2442,6 +2442,9 @@ export class AppMessagesManager {
case 'messageMediaPhoto':
messageText += '<i>Photo' + (message.message ? ', ' : '') + '</i>';
break;
case 'messageMediaDice':
messageText += RichTextProcessor.wrapEmojiText(message.media.emoticon);
break;
case 'messageMediaGeo':
messageText += '<i>Geolocation</i>';
break;
@ -2471,6 +2474,7 @@ export class AppMessagesManager { @@ -2471,6 +2474,7 @@ export class AppMessagesManager {
break;
default:
//messageText += message.media._;
///////this.log.warn('Got unknown message.media type!', message);
break;
}
@ -2660,6 +2664,15 @@ export class AppMessagesManager { @@ -2660,6 +2664,15 @@ export class AppMessagesManager {
return true;
}
public canDeleteMessage(messageID: number) {
const message = this.messagesStorage[messageID];
if(message) {
return message.peerID > 0 || message.fromID == $rootScope.myID || appChatsManager.hasRights(message.peerID, 'deleteRevoke');
} else {
return false;
}
}
public applyConversations(dialogsResult: MessagesPeerDialogs.messagesPeerDialogs) {
// * В эту функцию попадут только те диалоги, в которых есть read_inbox_max_id и read_outbox_max_id, в отличие от тех, что будут в getTopMessages

10
src/lib/richtextprocessor.ts

@ -349,12 +349,12 @@ namespace RichTextProcessor { @@ -349,12 +349,12 @@ namespace RichTextProcessor {
entities: MessageEntity[],
contextSite: string,
highlightUsername: string,
noLinks: boolean,
noLinebreaks: boolean,
noCommands: boolean,
noLinks: true,
noLinebreaks: true,
noCommands: true,
fromBot: boolean,
noTextFormat: boolean,
nested?: boolean,
noTextFormat: true,
nested?: true,
contextHashtag?: string
}> = {}) {
if(!text || !text.length) {

11
src/lib/utils.ts

@ -607,4 +607,15 @@ export function cancelSelection() { @@ -607,4 +607,15 @@ export function cancelSelection() {
}
}
export function getSelectedText() {
if(window.getSelection) {
return window.getSelection().toString();
// @ts-ignore
} else if(document.selection) {
// @ts-ignore
return document.selection.createRange().text;
}
return '';
}
//(window as any).splitStringByLength = splitStringByLength;

8
src/scss/components/_global.scss

@ -88,6 +88,14 @@ Utility Classes @@ -88,6 +88,14 @@ Utility Classes
user-select: none;
}
/* .no-transition {
transition: none !important;
&-all, &-all * {
transition: none !important;
}
} */
.center-align, .text-center {
text-align: center;
}

168
src/scss/partials/_chat.scss

@ -214,6 +214,7 @@ $chat-helper-size: 39px; @@ -214,6 +214,7 @@ $chat-helper-size: 39px;
}
#chat-input {
--translateY: 0;
display: flex;
width: 100%;
max-width: 100%;
@ -222,6 +223,8 @@ $chat-helper-size: 39px; @@ -222,6 +223,8 @@ $chat-helper-size: 39px;
flex: 0 0 auto; /* Forces side columns to stay same width */
position: relative;
//overflow: hidden;
transition: transform var(--layer-transition);
transform: translateY(var(--translateY));
/* // * for no ESG top
flex: 1 1 auto;
@ -235,10 +238,10 @@ $chat-helper-size: 39px; @@ -235,10 +238,10 @@ $chat-helper-size: 39px;
@include respond-to(medium-screens) {
width: calc(100% - var(--right-column-width));
transition: transform var(--layer-transition);
//transition: transform var(--layer-transition);
body.is-right-column-shown & {
transform: translate3d(calc(var(--right-column-width) / -2), 0, 0);
transform: translate3d(calc(var(--right-column-width) / -2), var(--translateY), 0);
}
body.animation-level-0 & {
@ -246,20 +249,31 @@ $chat-helper-size: 39px; @@ -246,20 +249,31 @@ $chat-helper-size: 39px;
}
}
&.is-hidden {
--translateY: 100%;
transform: translate3d(0, var(--translateY), 0);
position: absolute;
bottom: 0;
#bubbles.is-selecting:not(.backwards) ~ & {
--translateY: 0;
}
}
.chat-input-container {
--padding-horizontal: #{$chat-padding-handhelds};
display: flex;
align-items: center;
justify-content: space-between;
max-width: var(--messages-container-width);
margin: 0 auto;
width: 100%;
padding: 0 $chat-padding-handhelds;
padding: 0 var(--padding-horizontal);
flex: 0 0 auto;
position: relative;
@include respond-to(not-handhelds) {
padding-left: $chat-padding;
padding-right: $chat-padding;
--padding-horizontal: #{$chat-padding};
padding-bottom: 21px;
}
@ -327,17 +341,16 @@ $chat-helper-size: 39px; @@ -327,17 +341,16 @@ $chat-helper-size: 39px;
top: 0;
// here percents can be used since there are no other transforms
transform: translateX(calc(-100% + #{-1rem + -$btn-send-margin}));
transform: translateX(calc(-100% + var(--padding-horizontal) * -1 + #{-$btn-send-margin}));
@include respond-to(handhelds) {
/* @include respond-to(handhelds) {
transform: translateX(calc(-100% + #{-.5rem + -$btn-send-margin}));
}
} */
}
.btn-send-container {
flex: 0 0 auto;
position: relative;
align-self: flex-end;
position: absolute;
right: var(--padding-horizontal);
z-index: 2;
}
@ -625,6 +638,10 @@ $chat-helper-size: 39px; @@ -625,6 +638,10 @@ $chat-helper-size: 39px;
.chat-background {
overflow: hidden;
&.no-transition:before {
transition: none !important;
}
&, &:before {
position: absolute !important;
top: 0;
@ -884,7 +901,7 @@ $chat-helper-size: 39px; @@ -884,7 +901,7 @@ $chat-helper-size: 39px;
align-items: center;
flex-direction: column;
width: calc(100% - #{$chat-input-size + $btn-send-margin});
max-width: 100%;
max-width: calc(100% - #{$chat-input-size + $btn-send-margin});
justify-content: center;
background-color: #fff;
border-radius: 12px;
@ -920,9 +937,14 @@ $chat-helper-size: 39px; @@ -920,9 +937,14 @@ $chat-helper-size: 39px;
@include respond-to(handhelds) {
--padding: .5px .5rem;
width: calc(100% - #{$chat-input-handhelds-size + $btn-send-margin});
max-width: calc(100% - #{$chat-input-handhelds-size + $btn-send-margin});
min-height: $chat-input-handhelds-size;
}
@media only screen and (max-width: 420px) {
max-width: 100%;
}
@include respond-to(esg-bottom) {
--padding: .5px .5rem;
min-height: $chat-input-handhelds-size;
@ -1031,21 +1053,54 @@ $chat-helper-size: 39px; @@ -1031,21 +1053,54 @@ $chat-helper-size: 39px;
border-radius: inherit;
padding: inherit;
user-select: none;
font-size: 15px;
&-count {
color: #000;
font-weight: 500;
flex-grow: 1;
padding-left: .5rem;
white-space: nowrap;
//padding-left: .5rem;
}
.btn-icon {
margin-left: 6px;
maRgin-top: 6px;
color: #3f454a;
height: 42px;
width: 42px;
}
.btn-primary {
height: 2.5rem;
width: 7.5rem;
width: auto;
@include respond-to(handhelds) {
padding: 0 .5rem;
}
@media only screen and (max-width: 380px) {
font-size: 0;
&:before {
margin: 0;
}
}
}
&-forward {
&:before {
margin-right: 14px;
}
}
&-delete {
margin-right: .625rem;
margin-left: .375rem;
.btn-primary + .btn-primary {
margin-left: .75rem;
&:before {
margin-right: 10px;
}
}
}
@ -1086,6 +1141,7 @@ $chat-helper-size: 39px; @@ -1086,6 +1141,7 @@ $chat-helper-size: 39px;
flex: 1 1 auto; /* Lets middle column shrink/grow to available width */
//overflow: hidden;
position: relative;
transform: translateY(var(--translateY));
transition: transform var(--layer-transition);
@ -1096,8 +1152,53 @@ $chat-helper-size: 39px; @@ -1096,8 +1152,53 @@ $chat-helper-size: 39px;
.chat-container.is-helper-active & {
&:not(.is-selecting), &.is-selecting.backwards {
--translateY: -#{$chat-helper-size};
#bubbles-inner {
transform: translateY(calc(var(--translateY) * -1));
//margin-top: $chat-helper-size;
//transition: none;
}
}
}
&.is-chat-input-hidden.is-selecting:not(.backwards) {
--translateY: -79px;
#bubbles-inner {
transform: translateY(calc(var(--translateY) * -1));
//margin-top: $chat-helper-size;
//transition: none;
}
}
/* #bubbles-transform-helper {
width: 100%;
height: 100%;
max-height: 100%;
position: relative;
transform: translateY(var(--translateY));
transition: transform var(--layer-transition); */
> .scrollable {
height: auto;
/* position: absolute;
bottom: 0;
left: 0; */
//position: relative; // неизвестно зачем это было
//display: flex; // for end
//flex-direction: unset;
display: block;
/* display: flex;
flex-direction: column;
justify-content: flex-end; */
// * scrollbar takes some width, don't need to set padding for iOS
html.is-safari:not(.is-ios) & {
padding-left: 6px;
}
}
//}
// ! WARNING, НЕЛЬЗЯ СТАВИТЬ ТРАНСФОРМ КРОМЕ TRANSLATEZ(0) НА БЛОК С OVERFLOW, ОН БУДЕТ ПРЫГАТЬ ВВЕРХ ПРИ ВКЛЮЧЕННОМ ПРАВИЛЕ И ЭТО НЕ ИСПРАВИТЬ JS'ОМ!
@include respond-to(medium-screens) {
@ -1113,9 +1214,6 @@ $chat-helper-size: 39px; @@ -1113,9 +1214,6 @@ $chat-helper-size: 39px;
}
&.is-selecting {
cursor: default !important;
user-select: none;
&:not(.backwards) .is-in .bubble__container {
transform: translateX(2.5rem);
}
@ -1133,27 +1231,9 @@ $chat-helper-size: 39px; @@ -1133,27 +1231,9 @@ $chat-helper-size: 39px;
display: none;
} */
> .scrollable {
height: auto;
/* position: absolute;
bottom: 0;
left: 0; */
//position: relative; // неизвестно зачем это было
//display: flex; // for end
//flex-direction: unset;
display: block;
/* display: flex;
flex-direction: column;
justify-content: flex-end; */
// * scrollbar takes some width, don't need to set padding for iOS
html.is-safari:not(.is-ios) & {
padding-left: 6px;
}
}
&:not(.scrolled-down):not(.search-results-active) {
//> #bubbles-transform-helper {
// ! these lines will blur messages if chat input helper is active
> .scrollable {
-webkit-mask-image: -webkit-linear-gradient(bottom, transparent, #000 20px);
mask-image: linear-gradient(0deg, transparent 0, #000 20px);
@ -1168,6 +1248,7 @@ $chat-helper-size: 39px; @@ -1168,6 +1248,7 @@ $chat-helper-size: 39px;
--translateY: 79px !important;
} */
}
//}
}
.preloader {
@ -1201,6 +1282,11 @@ $chat-helper-size: 39px; @@ -1201,6 +1282,11 @@ $chat-helper-size: 39px;
padding: 0 1rem;
max-width: var(--messages-container-width);
transition: transform var(--layer-transition);
transform: translateY(0);
/* transition: margin-top var(--layer-transition);
transition-delay: .2s; */
@include respond-to(medium-screens) {
width: calc(100% - var(--right-column-width));
}
@ -1237,9 +1323,9 @@ $chat-helper-size: 39px; @@ -1237,9 +1323,9 @@ $chat-helper-size: 39px;
}
}
&.is-chat-input-hidden {
/* #bubbles.is-chat-input-hidden & {
padding-bottom: 55px;
}
} */
&:not(.is-channel), &.is-chat {
.message {

34
src/scss/partials/_leftSidebar.scss

@ -466,7 +466,6 @@ @@ -466,7 +466,6 @@
}
.tgico-add:before {
content: "\e903";
font-size: 24px;
margin-right: 6px;
}
@ -619,17 +618,7 @@ @@ -619,17 +618,7 @@
color: #50a2e9;
}
.included-chats-container {
.sidebar-left-h2 {
color: #707579;
font-size: 15px;
font-weight: 500;
padding: 6px 24px 8px 24px;
@include respond-to(handhelds) {
padding: 6px 16px 8px 16px;
}
}
.popup-forward, .included-chats-container {
.selector {
ul {
li > .rp {
@ -647,10 +636,6 @@ @@ -647,10 +636,6 @@
height: 46px;
}
span.user-title {
font-weight: 500;
}
.user-caption {
padding: 0px 0px 0 14px;
margin-top: -2px;
@ -660,7 +645,24 @@ @@ -660,7 +645,24 @@
font-size: 15px;
margin-top: 2px;
}
}
}
}
.included-chats-container {
.sidebar-left-h2 {
color: #707579;
font-size: 15px;
font-weight: 500;
padding: 6px 24px 8px 24px;
@include respond-to(handhelds) {
padding: 6px 16px 8px 16px;
}
}
.selector {
ul {
.checkbox {
margin-top: 10px;
}

4
src/scss/partials/_selector.scss

@ -130,10 +130,6 @@ @@ -130,10 +130,6 @@
height: 24px;
}
span.user-title {
font-weight: normal;
}
span.user-last-message {
font-size: 14px;
}

6
src/scss/partials/popups/_forward.scss

@ -6,15 +6,15 @@ @@ -6,15 +6,15 @@
width: 420px;
max-width: 420px;
//padding: 12px 20px 32.5px;
padding: .75rem 0 0 0;
padding: 9px 0 0 0;
max-height: unquote('min(40.625rem, 100%)');
height: 40.625rem;
}
&-header {
flex: 0 0 auto;
margin-bottom: 9px;
padding: 0 .75rem;
margin-bottom: 4px;
padding: 0 1rem;
}
&-title {

8
src/scss/partials/popups/_stickers.scss

@ -60,12 +60,16 @@ @@ -60,12 +60,16 @@
}
&-sticker {
width: 80px;
height: 80px;
width: var(--esg-sticker-size);
height: var(--esg-sticker-size);
margin-bottom: 2px;
justify-self: center;
cursor: pointer;
@include respond-to(handhelds) {
margin-bottom: 8px;
}
&:hover {
border-radius: 12px;
background-color: var(--color-gray-hover);

22
src/scss/style.scss

@ -840,9 +840,10 @@ input:focus, button:focus { @@ -840,9 +840,10 @@ input:focus, button:focus {
overflow: hidden;
position: relative;
padding: 0; // new
transition: .2s opacity;
html.no-touch &:hover {
transition: .2s background-color;
transition: .2s background-color, .2s opacity;
background: darken($color-blue, 8%);
}
@ -852,24 +853,31 @@ input:focus, button:focus { @@ -852,24 +853,31 @@ input:focus, button:focus {
left: auto;
}
// * tgico
&:before {
color: #707579;
font-size: 1.5rem;
margin-right: 1rem;
&:disabled {
pointer-events: none !important;
opacity: .25;
}
}
// ! example: multiselect input
.btn-transparent {
color: #000;
background-color: transparent;
display: flex;
align-items: center;
justify-content: center;
padding: 0 .875rem;
//width: auto;
html.no-touch &:hover {
background-color: var(--color-gray-hover);
}
// * tgico
&:before {
color: #707579;
font-size: 1.5rem;
margin-right: 1rem;
}
}
.btn-primary.btn-circle {

Loading…
Cancel
Save