Browse Source

Restrict forwarding outgoing message

Verify attach menu buttons availability
Fix peer typing flickering
Colored replies
master
Eduard Kuzmenko 2 years ago
parent
commit
02f0c9db0f
  1. 24
      src/components/chat/contextMenu.ts
  2. 33
      src/components/chat/input.ts
  3. 2
      src/components/chat/selection.ts
  4. 2
      src/components/chat/sendAs.ts
  5. 2
      src/components/poll.ts
  6. 8
      src/components/wrappers/reply.ts
  7. 5
      src/index.ts
  8. 28
      src/lib/appManagers/appImManager.ts
  9. 1
      src/lib/appManagers/appManagersManager.ts
  10. 6
      src/lib/idb.ts
  11. 5
      src/lib/mtproto/mtproto.worker.ts
  12. 1
      src/lib/mtproto/mtprotoMessagePort.ts
  13. 52
      src/lib/mtproto/mtprotoworker.ts
  14. 4
      src/lib/richTextProcessor/wrapRichText.ts
  15. 5
      src/scss/partials/_avatar.scss
  16. 2
      src/scss/partials/_button.scss
  17. 14
      src/scss/partials/_chatBubble.scss
  18. 5
      src/scss/partials/_scrollable.scss

24
src/components/chat/contextMenu.ts

@ -38,6 +38,7 @@ import contextMenuController from "../../helpers/contextMenuController"; @@ -38,6 +38,7 @@ import contextMenuController from "../../helpers/contextMenuController";
import { attachContextMenuListener } from "../../helpers/dom/attachContextMenuListener";
import filterAsync from "../../helpers/array/filterAsync";
import appDownloadManager from "../../lib/appManagers/appDownloadManager";
import { SERVICE_PEER_ID } from "../../lib/mtproto/mtproto_config";
export default class ChatContextMenu {
private buttons: (ButtonMenuItemOptions & {verify: () => boolean | Promise<boolean>, notDirect?: () => boolean, withSelection?: true, isSponsored?: true})[];
@ -178,6 +179,10 @@ export default class ChatContextMenu { @@ -178,6 +179,10 @@ export default class ChatContextMenu {
this.canOpenReactedList = undefined;
const initResult = await this.init();
if(!initResult) {
return;
}
element = initResult.element;
const {cleanup, destroy, menuPadding, reactionsMenu, reactionsMenuPosition} = initResult;
let isReactionsMenuVisible = false;
@ -214,7 +219,7 @@ export default class ChatContextMenu { @@ -214,7 +219,7 @@ export default class ChatContextMenu {
if(isReactionsMenuVisible) void reactionsMenu.container.offsetLeft; // reflow
}
openBtnMenu(element, () => {
contextMenuController.openBtnMenu(element, () => {
if(reactionsMenu) {
reactionsMenu.container.classList.remove('is-visible');
}
@ -224,6 +229,7 @@ export default class ChatContextMenu { @@ -224,6 +229,7 @@ export default class ChatContextMenu {
this.target = null;
this.viewerPeerId = undefined;
this.canOpenReactedList = undefined;
cleanup();
setTimeout(() => {
destroy();
@ -235,6 +241,9 @@ export default class ChatContextMenu { @@ -235,6 +241,9 @@ export default class ChatContextMenu {
}
};
r();
};
public cleanup() {
this.listenerSetter.removeAll();
this.reactionsMenu && this.reactionsMenu.cleanup();
@ -425,7 +434,7 @@ export default class ChatContextMenu { @@ -425,7 +434,7 @@ export default class ChatContextMenu {
icon: 'forward',
text: 'Forward',
onClick: this.onForwardClick, // let forward the message if it's outgoing but not ours (like a changelog)
verify: () => !this.noForwards && this.chat.type !== 'scheduled' && (!this.message.pFlags.is_outgoing || !this.message.pFlags.out) && this.message._ !== 'messageService'
verify: () => !this.noForwards && this.chat.type !== 'scheduled' && (!this.message.pFlags.is_outgoing || this.message.fromId === SERVICE_PEER_ID) && this.message._ !== 'messageService'
}, {
icon: 'forward',
text: 'Message.Context.Selection.Forward',
@ -500,6 +509,10 @@ export default class ChatContextMenu { @@ -500,6 +509,10 @@ export default class ChatContextMenu {
this.setButtons();
const filteredButtons = await this.filterButtons(this.buttons);
if(!filteredButtons.length) {
return;
}
const element = this.element = ButtonMenu(filteredButtons, this.listenerSetter);
element.id = 'bubble-contextmenu';
element.classList.add('contextmenu');
@ -540,8 +553,9 @@ export default class ChatContextMenu { @@ -540,8 +553,9 @@ export default class ChatContextMenu {
fakeText.classList.add('btn-menu-item-text-fake');
viewsButton.element.append(fakeText);
const AVATAR_SIZE = 22;
const MAX_AVATARS = 3;
const PADDING_PER_AVATAR = .875;
const PADDING_PER_AVATAR = 1.125;
i18nElem.element.style.visibility = 'hidden';
i18nElem.element.style.paddingRight = isViewingReactions ? PADDING_PER_AVATAR * Math.min(MAX_AVATARS, recentReactions.length) + 'rem' : '1rem';
const middleware = this.middleware.get();
@ -595,7 +609,7 @@ export default class ChatContextMenu { @@ -595,7 +609,7 @@ export default class ChatContextMenu {
}
if(reactions.length) {
const avatars = new StackedAvatars({avatarSize: 24});
const avatars = new StackedAvatars({avatarSize: AVATAR_SIZE});
avatars.render(recentReactions ? recentReactions.map((r) => getPeerId(r.peer_id)) : reactions.map((reaction) => reaction.peerId));
viewsButton.element.append(avatars.container);
@ -645,7 +659,7 @@ export default class ChatContextMenu { @@ -645,7 +659,7 @@ export default class ChatContextMenu {
},
destroy: () => {
element.remove();
reactionsMenu.widthContainer.remove();
reactionsMenu && reactionsMenu.widthContainer.remove();
},
menuPadding,
reactionsMenu,

33
src/components/chat/input.ts

@ -92,6 +92,7 @@ import contextMenuController from "../../helpers/contextMenuController"; @@ -92,6 +92,7 @@ import contextMenuController from "../../helpers/contextMenuController";
import { emojiFromCodePoints } from "../../vendor/emoji";
import { modifyAckedPromise } from "../../helpers/modifyAckedResult";
import ChatSendAs from "./sendAs";
import filterAsync from "../../helpers/array/filterAsync";
const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
@ -1211,7 +1212,17 @@ export default class ChatInput { @@ -1211,7 +1212,17 @@ export default class ChatInput {
const previousSendAs = this.sendAs;
const sendAs = this.createSendAs();
const [isBroadcast, canPinMessage, isBot, canSend, neededFakeContainer, ackedPeerFull, ackedScheduledMids, setSendAsCallback] = await Promise.all([
const [
isBroadcast,
canPinMessage,
isBot,
canSend,
neededFakeContainer,
ackedPeerFull,
ackedScheduledMids,
setSendAsCallback,
filteredAttachMenuButtons
] = await Promise.all([
this.managers.appPeersManager.isBroadcast(peerId),
this.managers.appPeersManager.canPinMessage(peerId),
this.managers.appPeersManager.isBot(peerId),
@ -1219,7 +1230,8 @@ export default class ChatInput { @@ -1219,7 +1230,8 @@ export default class ChatInput {
this.getNeededFakeContainer(),
modifyAckedPromise(this.managers.acknowledged.appProfileManager.getProfileByPeerId(peerId)),
btnScheduled ? modifyAckedPromise(this.managers.acknowledged.appMessagesManager.getScheduledMessages(peerId)) : undefined,
sendAs ? (sendAs.setPeerId(this.chat.peerId), sendAs.updateManual(true)) : undefined
sendAs ? (sendAs.setPeerId(this.chat.peerId), sendAs.updateManual(true)) : undefined,
this.filterAttachMenuButtons()
]);
const placeholderKey = this.messageInput ? await this.getPlaceholderKey() : undefined;
@ -1291,7 +1303,7 @@ export default class ChatInput { @@ -1291,7 +1303,7 @@ export default class ChatInput {
}
if(this.messageInput) {
this.updateMessageInput(canSend, placeholderKey);
this.updateMessageInput(canSend, placeholderKey, filteredAttachMenuButtons);
} else if(this.pinnedControlBtn) {
this.pinnedControlBtn.append(i18n(canPinMessage ? 'Chat.Input.UnpinAll' : 'Chat.Pinned.DontShow'));
}
@ -1373,7 +1385,14 @@ export default class ChatInput { @@ -1373,7 +1385,14 @@ export default class ChatInput {
i.compareAndUpdate({key});
}
public updateMessageInput(canSend: boolean, placeholderKey: LangPackKey) {
private filterAttachMenuButtons() {
const {peerId, threadId} = this.chat;
return filterAsync(this.attachMenuButtons, (button) => {
return button.verify(peerId, threadId);
});
}
public updateMessageInput(canSend: boolean, placeholderKey: LangPackKey, visible: ChatInput['attachMenuButtons']) {
const {chatInput, attachMenu, messageInput} = this;
const {peerId, threadId} = this.chat;
const isHidden = chatInput.classList.contains('is-hidden');
@ -1387,10 +1406,8 @@ export default class ChatInput { @@ -1387,10 +1406,8 @@ export default class ChatInput {
this.updateMessageInputPlaceholder(placeholderKey);
const visible = this.attachMenuButtons.filter((button) => {
const good = button.verify(peerId, threadId);
button.element.classList.toggle('hide', !good);
return good;
this.attachMenuButtons.forEach((button) => {
button.element.classList.toggle('hide', !visible.includes(button));
});
if(!canSend) {

2
src/components/chat/selection.ts

@ -333,6 +333,7 @@ class AppSelection extends EventListenerBase<{ @@ -333,6 +333,7 @@ class AppSelection extends EventListenerBase<{
this.appendCheckbox(element, checkboxField);
} else if(hasCheckbox) {
this.getCheckboxInputFromElement(element).parentElement.remove();
SetTransition(element, 'is-selected', false, 200);
}
return true;
@ -976,6 +977,7 @@ export default class ChatSelection extends AppSelection { @@ -976,6 +977,7 @@ export default class ChatSelection extends AppSelection {
};
protected onCancelSelection = async() => {
return;
const promises: Promise<HTMLElement>[] = [];
for(const [peerId, mids] of this.selectedMids) {
for(const mid of mids) {

2
src/components/chat/sendAs.ts

@ -141,7 +141,7 @@ export default class ChatSendAs { @@ -141,7 +141,7 @@ export default class ChatSendAs {
buttons.forEach((button, idx) => {
const peerId = peerIds[idx];
const avatar = new AvatarElement();
avatar.classList.add('avatar-32', 'btn-menu-item-icon');
avatar.classList.add('avatar-26', 'btn-menu-item-icon');
avatar.updateWithOptions({peerId});
if(!idx) {

2
src/components/poll.ts

@ -294,7 +294,7 @@ export default class PollElement extends HTMLElement { @@ -294,7 +294,7 @@ export default class PollElement extends HTMLElement {
setInnerHTML(this.firstElementChild, wrapEmojiText(poll.question));
Array.from(this.querySelectorAll('.poll-answer-text')).forEach((el, idx) => {
setInnerHTML(el, RichTextProcessor.wrapEmojiText(poll.answers[idx].text));
setInnerHTML(el, wrapEmojiText(poll.answers[idx].text));
});
this.descDiv = this.firstElementChild.nextElementSibling as HTMLElement;

8
src/components/wrappers/reply.ts

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { hexToRgb } from "../../helpers/color";
import { Message } from "../../layer";
import getPeerColorById from "../../lib/appManagers/utils/peers/getPeerColorById";
import ReplyContainer from "../chat/replyContainer";
@ -19,8 +20,11 @@ export default function wrapReply( @@ -19,8 +20,11 @@ export default function wrapReply(
if(setColorPeerId) {
const hex = getPeerColorById(setColorPeerId, false);
replyContainer.border.style.backgroundColor = hex;
replyContainer.title.style.color = hex;
const [r, g, b] = hexToRgb(hex);
replyContainer.container.style.setProperty('--override-color', `${r}, ${g}, ${b}`);
replyContainer.container.classList.add('is-overriding-color');
// replyContainer.border.style.backgroundColor = hex;
// replyContainer.title.style.color = hex;
}
return {container: replyContainer.container, fillPromise};

5
src/index.ts

@ -22,7 +22,6 @@ import I18n from './lib/langPack'; @@ -22,7 +22,6 @@ import I18n from './lib/langPack';
import './helpers/peerIdPolyfill';
import './lib/polyfill';
import apiManagerProxy from './lib/mtproto/mtprotoworker';
import loadState from './lib/appManagers/utils/state/loadState';
import getProxiedManagers from './lib/appManagers/getProxiedManagers';
import themeController from './helpers/themeController';
import overlayCounter from './helpers/overlayCounter';
@ -32,7 +31,6 @@ document.addEventListener('DOMContentLoaded', async() => { @@ -32,7 +31,6 @@ document.addEventListener('DOMContentLoaded', async() => {
toggleAttributePolyfill();
rootScope.managers = getProxiedManagers();
apiManagerProxy;
singleInstance.start();
@ -193,7 +191,8 @@ document.addEventListener('DOMContentLoaded', async() => { @@ -193,7 +191,8 @@ document.addEventListener('DOMContentLoaded', async() => {
const langPromise = I18n.getCacheLangPack();
const [stateResult, langPack] = await Promise.all([
loadState(),
// loadState(),
apiManagerProxy.sendState().then(([stateResult]) => stateResult),
langPromise
]);
I18n.setTimeFormat(stateResult.state.settings.timeFormat);

28
src/lib/appManagers/appImManager.ts

@ -1842,6 +1842,19 @@ export class AppImManager extends EventListenerBase<{ @@ -1842,6 +1842,19 @@ export class AppImManager extends EventListenerBase<{
return;
}
let peerTitlePromise: Promise<any>;
let args: any[];
if(peerId.isAnyChat()) {
const peerTitle = new PeerTitle();
peerTitlePromise = peerTitle.update({peerId: typing.userId.toPeerId(false), onlyFirstName: true});
args = [
peerTitle.element,
typings.length - 1
];
await peerTitlePromise;
}
if(!container) {
container = document.createElement('span');
container.classList.add('online', 'peer-typing-container');
@ -1859,17 +1872,6 @@ export class AppImManager extends EventListenerBase<{ @@ -1859,17 +1872,6 @@ export class AppImManager extends EventListenerBase<{
}
}
let peerTitlePromise: Promise<any>;
let args: any[];
if(peerId.isAnyChat()) {
const peerTitle = new PeerTitle();
peerTitlePromise = peerTitle.update({peerId: typing.userId.toPeerId(false), onlyFirstName: true});
args = [
peerTitle.element,
typings.length - 1
];
}
if(action._ === 'sendMessageEmojiInteractionSeen') {
if(args) {
args.pop();
@ -1887,10 +1889,6 @@ export class AppImManager extends EventListenerBase<{ @@ -1887,10 +1889,6 @@ export class AppImManager extends EventListenerBase<{
if(container.childElementCount > 1) container.lastElementChild.replaceWith(descriptionElement);
else container.append(descriptionElement);
if(peerTitlePromise) {
await peerTitlePromise;
}
// log('returning typing');
return container;
}

1
src/lib/appManagers/appManagersManager.ts

@ -54,6 +54,7 @@ export class AppManagersManager { @@ -54,6 +54,7 @@ export class AppManagersManager {
const appStoragesManager = new AppStoragesManager();
await Promise.all([
// new Promise(() => {}),
appStoragesManager.loadStorages(),
this.cryptoPortPromise
]);

6
src/lib/idb.ts

@ -369,6 +369,10 @@ export default class IDBStorage<T extends Database<any>, StoreName extends strin @@ -369,6 +369,10 @@ export default class IDBStorage<T extends Database<any>, StoreName extends strin
entryName = [].concat(entryName);
}
if(!entryName.length) {
return Promise.resolve([]) as any;
}
return this.getObjectStore<T>('readonly', (objectStore) => {
return (entryName as string[]).map((entryName) => objectStore.get(entryName));
}, DEBUG ? 'get: ' + entryName.join(', ') : '', storeName);
@ -421,7 +425,7 @@ export default class IDBStorage<T extends Database<any>, StoreName extends strin @@ -421,7 +425,7 @@ export default class IDBStorage<T extends Database<any>, StoreName extends strin
// transaction.oncomplete = () => onComplete('transaction');
const timeout = setTimeout(() => {
this.log.error('transaction not finished', transaction);
this.log.error('transaction not finished', transaction, log);
}, 10000);
/* transaction.addEventListener('abort', (e) => {

5
src/lib/mtproto/mtproto.worker.ts

@ -31,11 +31,6 @@ port.addMultipleEventsListeners({ @@ -31,11 +31,6 @@ port.addMultipleEventsListeners({
transportController.waitForWebSocket();
},
// windowSize: ({width, height}) => {
// windowSize.width = width;
// windowSize.height = height;
// },
crypto: ({method, args}) => {
return cryptoWorker.invokeCrypto(method as any, ...args as any);
},

1
src/lib/mtproto/mtprotoMessagePort.ts

@ -21,7 +21,6 @@ type MTProtoBroadcastEvent = { @@ -21,7 +21,6 @@ type MTProtoBroadcastEvent = {
export default class MTProtoMessagePort<Master extends boolean = true> extends SuperMessagePort<{
environment: (environment: ReturnType<typeof getEnvironment>) => void,
// windowSize: (payload: {width: number, height: number}) => void,
crypto: (payload: {method: string, args: any[]}) => Promise<any>,
state: (payload: {userId: UserId} & Awaited<ReturnType<typeof loadState>> & {storagesResults?: StoragesResults}) => void,
manager: (payload: MTProtoManagerTaskPayload) => any,

52
src/lib/mtproto/mtprotoworker.ts

@ -172,10 +172,7 @@ class ApiManagerProxy extends MTProtoMessagePort { @@ -172,10 +172,7 @@ class ApiManagerProxy extends MTProtoMessagePort {
this.log('Passing environment:', ENVIRONMENT);
this.invoke('environment', ENVIRONMENT);
this.sendState();
// setTimeout(() => {
// this.getConfig();
// }, 5000);
// this.sendState();
}
private registerServiceWorker() {
@ -318,61 +315,38 @@ class ApiManagerProxy extends MTProtoMessagePort { @@ -318,61 +315,38 @@ class ApiManagerProxy extends MTProtoMessagePort {
}
}
// protected s() {
// const originalPostMessage = this.postMessage;
// const postQueue: any[] = [];
// this.postMessage = (source, task) => {
// if(task.type === 'invoke' && task.payload.type === 'state') {
// this.postMessage = originalPostMessage;
// postQueue.unshift(task);
// postQueue.forEach((task) => this.postMessage(undefined, task));
// postQueue.length = 0;
// } else {
// postQueue.push(task);
// }
// };
// }
private onWorkerFirstMessage(worker: any) {
this.log('set webWorker');
// return;
this.worker = worker;
/// #if MTPROTO_SW
this.attachSendPort(worker);
/// #else
this.attachWorkerToPort(worker, this, 'mtproto');
// this.s();
// port.addEventListener('message', () => {
// this.registerServiceWorker();
// }, {once: true});
/// #endif
// this.startSendingWindowSize();
}
public addServiceWorkerTaskListener(name: keyof ApiManagerProxy['taskListenersSW'], callback: ApiManagerProxy['taskListenersSW'][typeof name]) {
this.taskListenersSW[name] = callback;
}
// private startSendingWindowSize() {
// const sendWindowSize = () => {
// this.invoke('windowSize', {width: windowSize.width, height: windowSize.height});
// };
// mediaSizes.addEventListener('resize', sendWindowSize);
// sendWindowSize();
// }
private sendState() {
private loadState() {
return Promise.all([
loadState(),
// loadStorages(createStorages()),
]).then(([stateResult/* , storagesResults */]) => {
loadState().then((stateResult) => {
this.newVersion = stateResult.newVersion;
this.oldVersion = stateResult.oldVersion;
this.mirrors['state'] = stateResult.state;
return stateResult;
}),
// loadStorages(createStorages()),
]);
}
public sendState() {
return this.loadState().then((result) => {
const [stateResult] = result;
this.invoke('state', {...stateResult, userId: rootScope.myId.toUserId()});
return result;
});
}

4
src/lib/richTextProcessor/wrapRichText.ts

@ -304,8 +304,8 @@ export default function wrapRichText(text: string, options: Partial<{ @@ -304,8 +304,8 @@ export default function wrapRichText(text: string, options: Partial<{
}
const href = (currentContext || typeof electronHelpers === 'undefined')
? encodeEntities(url)
: `javascript:electronHelpers.openExternal('${encodeEntities(url)}');`;
? url
: `javascript:electronHelpers.openExternal('${url}');`;
element = document.createElement('a');
element.className = 'anchor-url';

5
src/scss/partials/_avatar.scss

@ -219,6 +219,11 @@ avatar-element { @@ -219,6 +219,11 @@ avatar-element {
--multiplier: 2.25;
}
&.avatar-22 {
--size: 22px;
--multiplier: 2.454545;
}
&.avatar-18 {
--size: 18px;
--multiplier: 3;

2
src/scss/partials/_button.scss

@ -296,7 +296,7 @@ @@ -296,7 +296,7 @@
.stacked-avatars {
--margin-right: -.6875rem;
flex: 0 0 auto;
right: 1rem;
right: .5rem;
// margin-right: -1.5rem;
// margin-left: 1rem;
position: absolute;

14
src/scss/partials/_chatBubble.scss

@ -2359,6 +2359,20 @@ $bubble-beside-button-width: 38px; @@ -2359,6 +2359,20 @@ $bubble-beside-button-width: 38px;
code {
color: var(--monospace-text-color);
}
.reply.is-overriding-color {
.reply-border {
background-color: rgb(var(--override-color));
}
.reply-title {
color: rgb(var(--override-color));
}
@include hover() {
background-color: rgba(var(--override-color), #{$hover-alpha});
}
}
}
.bubble.is-out {

5
src/scss/partials/_scrollable.scss

@ -94,10 +94,13 @@ html:not(.is-safari):not(.is-ios) { @@ -94,10 +94,13 @@ html:not(.is-safari):not(.is-ios) {
overflow-y: overlay;
scrollbar-width: thin; // Firefox only
scrollbar-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
transition: scrollbar-color .3s ease;
-ms-overflow-style: none;
transform: translateZ(0);
// html.is-firefox & {
// transition: scrollbar-color .3s ease;
// }
/* html.is-safari & {
overflow-y: scroll;
} */

Loading…
Cancel
Save