From 0478be16036588c15c7ba7cefcc1c6f186c67c68 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Thu, 26 Aug 2021 01:05:29 +0300 Subject: [PATCH] Report messages --- src/components/chat/contextMenu.ts | 10 +++ src/components/inputField.ts | 2 +- src/components/popups/index.ts | 3 +- src/components/popups/peer.ts | 26 +++++--- src/components/popups/reportMessages.ts | 54 +++++++++++++++ .../popups/reportMessagesConfirm.ts | 66 +++++++++++++++++++ src/lang.ts | 12 ++++ src/lib/appManagers/appMessagesManager.ts | 13 +++- src/lib/appManagers/appStickersManager.ts | 15 ++++- .../partials/popups/_instanceDeactivated.scss | 6 ++ src/scss/partials/popups/_joinChatInvite.scss | 6 ++ src/scss/partials/popups/_reportMessages.scss | 28 ++++++++ src/scss/style.scss | 1 + 13 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 src/components/popups/reportMessages.ts create mode 100644 src/components/popups/reportMessagesConfirm.ts create mode 100644 src/scss/partials/popups/_reportMessages.scss diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index c12a0ee3..4c5d6343 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -26,6 +26,7 @@ import cancelSelection from "../../helpers/dom/cancelSelection"; import { attachClickEvent } from "../../helpers/dom/clickEvent"; import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty"; import { Message } from "../../layer"; +import PopupReportMessages from "../popups/reportMessages"; export default class ChatContextMenu { private buttons: (ButtonMenuItemOptions & {verify: () => boolean, notDirect?: () => boolean, withSelection?: true})[]; @@ -322,6 +323,15 @@ export default class ChatContextMenu { !this.chat.selection.selectionForwardBtn.hasAttribute('disabled'), notDirect: () => true, withSelection: true + }, { + icon: 'flag', + text: 'ReportChat', + onClick: () => { + new PopupReportMessages(this.peerId, [this.mid]); + }, + verify: () => !this.message.pFlags.out && !this.message.pFlags.is_outgoing && this.appPeersManager.isChannel(this.peerId), + notDirect: () => true, + withSelection: true }, { icon: 'select', text: 'Message.Context.Select', diff --git a/src/components/inputField.ts b/src/components/inputField.ts index 1a4e97ca..0efbeebe 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -95,7 +95,7 @@ class InputField { this.container.classList.add('input-field'); if(options.maxLength) { - options.showLengthOn = Math.round(options.maxLength / 3); + options.showLengthOn = Math.min(40, Math.round(options.maxLength / 3)); } const {placeholder, maxLength, showLengthOn, name, plainText} = options; diff --git a/src/components/popups/index.ts b/src/components/popups/index.ts index 26e24e99..71d9dc36 100644 --- a/src/components/popups/index.ts +++ b/src/components/popups/index.ts @@ -36,6 +36,7 @@ export default class PopupElement { protected btnClose: HTMLElement; protected btnConfirm: HTMLElement; protected body: HTMLElement; + protected buttons: HTMLElement; protected onClose: () => void; protected onCloseAfterTimeout: () => void; @@ -91,7 +92,7 @@ export default class PopupElement { } if(buttons && buttons.length) { - const buttonsDiv = document.createElement('div'); + const buttonsDiv = this.buttons = document.createElement('div'); buttonsDiv.classList.add('popup-buttons'); if(buttons.length === 2) { diff --git a/src/components/popups/peer.ts b/src/components/popups/peer.ts index ccb3cebd..3f065211 100644 --- a/src/components/popups/peer.ts +++ b/src/components/popups/peer.ts @@ -18,15 +18,16 @@ export type PopupPeerOptions = PopupOptions & Partial<{ title: string, titleLangKey?: LangPackKey, titleLangArgs?: any[], + noTitle?: boolean, description: string, descriptionLangKey?: LangPackKey, descriptionLangArgs?: any[], - buttons: Array & Partial<{callback: PopupPeerButtonCallback}>>, + buttons?: Array & Partial<{callback: PopupPeerButtonCallback}>>, checkboxes: Array }>; export default class PopupPeer extends PopupElement { constructor(private className: string, options: PopupPeerOptions = {}) { - super('popup-peer' + (className ? ' ' + className : ''), addCancelButton(options.buttons), {overlayClosable: true, ...options}); + super('popup-peer' + (className ? ' ' + className : ''), options.buttons && addCancelButton(options.buttons), {overlayClosable: true, ...options}); if(options.peerId) { let avatarEl = new AvatarElement(); @@ -36,16 +37,21 @@ export default class PopupPeer extends PopupElement { this.header.prepend(avatarEl); } - if(options.titleLangKey || !options.title) this.title.append(i18n(options.titleLangKey || 'AppName', options.titleLangArgs)); - else this.title.innerText = options.title || ''; - - let p = document.createElement('p'); - p.classList.add('popup-description'); - if(options.descriptionLangKey) p.append(i18n(options.descriptionLangKey, options.descriptionLangArgs)); - else p.innerHTML = options.description; + if(!options.noTitle) { + if(options.titleLangKey || !options.title) this.title.append(i18n(options.titleLangKey || 'AppName', options.titleLangArgs)); + else this.title.innerText = options.title || ''; + } const fragment = document.createDocumentFragment(); - fragment.append(p); + + if(options.descriptionLangKey || options.description) { + const p = document.createElement('p'); + p.classList.add('popup-description'); + if(options.descriptionLangKey) p.append(i18n(options.descriptionLangKey, options.descriptionLangArgs)); + else if(options.description) p.innerHTML = options.description; + + fragment.append(p); + } if(options.checkboxes) { this.container.classList.add('have-checkbox'); diff --git a/src/components/popups/reportMessages.ts b/src/components/popups/reportMessages.ts new file mode 100644 index 00000000..f9091b67 --- /dev/null +++ b/src/components/popups/reportMessages.ts @@ -0,0 +1,54 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import findUpClassName from "../../helpers/dom/findUpClassName"; +import whichChild from "../../helpers/dom/whichChild"; +import { ReportReason } from "../../layer"; +import appStickersManager from "../../lib/appManagers/appStickersManager"; +import { LangPackKey } from "../../lib/langPack"; +import Button from "../button"; +import PopupPeer from "./peer"; +import PopupReportMessagesConfirm from "./reportMessagesConfirm"; + +export default class PopupReportMessages extends PopupPeer { + constructor(peerId: number, mids: number[], onConfirm?: () => void) { + super('popup-report-messages', {titleLangKey: 'ChatTitle.ReportMessages', buttons: [], body: true}); + + mids = mids.slice(); + + const buttons: [LangPackKey, ReportReason['_']][] = [ + ['ReportChatSpam', 'inputReportReasonSpam'], + ['ReportChatViolence', 'inputReportReasonViolence'], + ['ReportChatChild', 'inputReportReasonChildAbuse'], + ['ReportChatPornography', 'inputReportReasonPornography'], + ['ReportChatOther', 'inputReportReasonOther'] + ]; + + const className = 'btn-primary btn-transparent'; + buttons.forEach(b => { + const button = Button(className, {/* icon: 'edit', */text: b[0]}); + this.body.append(button); + }); + + const preloadStickerPromise = appStickersManager.preloadAnimatedEmojiSticker(PopupReportMessagesConfirm.STICKER_EMOJI); + + this.body.addEventListener('click', (e) => { + const target = findUpClassName(e.target, 'btn-primary'); + const reason = buttons[whichChild(target)][1]; + + preloadStickerPromise.then(() => { + this.hide(); + + new PopupReportMessagesConfirm(peerId, mids, reason, onConfirm); + }); + }); + + this.body.style.margin = '0 -1rem'; + this.buttons.style.marginTop = '.5rem'; + + this.show(); + } +} diff --git a/src/components/popups/reportMessagesConfirm.ts b/src/components/popups/reportMessagesConfirm.ts new file mode 100644 index 00000000..25c1e515 --- /dev/null +++ b/src/components/popups/reportMessagesConfirm.ts @@ -0,0 +1,66 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import { ReportReason } from "../../layer"; +import appMessagesManager from "../../lib/appManagers/appMessagesManager"; +import appStickersManager from "../../lib/appManagers/appStickersManager"; +import InputField from "../inputField"; +import { toastNew } from "../toast"; +import { wrapSticker } from "../wrappers"; +import PopupPeer from "./peer"; + +export default class PopupReportMessagesConfirm extends PopupPeer { + public static STICKER_EMOJI = '👮‍♀️'; + constructor(peerId: number, mids: number[], reason: ReportReason['_'], onConfirm?: () => void) { + super('popup-report-messages-confirm', { + noTitle: true, + descriptionLangKey: 'ReportInfo', + buttons: [{ + langKey: 'ReportChat', + callback: () => { + if(!inputField.isValid()) { + return; + } + + onConfirm && onConfirm(); + appMessagesManager.reportMessages(peerId, mids, reason, inputField.value).then(bool => { + if(!bool) return; + + toastNew({ + langPackKey: 'ReportSentInfo' + }); + }); + } + }], + body: true + }); + + const div = document.createElement('div'); + const doc = appStickersManager.getAnimatedEmojiSticker(PopupReportMessagesConfirm.STICKER_EMOJI); + const size = 100; + wrapSticker({ + doc, + div, + emoji: PopupReportMessagesConfirm.STICKER_EMOJI, + width: size, + height: size, + loop: false, + play: true + }).finally(() => { + this.show(); + }); + + this.header.append(div); + + const inputField = new InputField({ + label: 'ReportHint', + maxLength: 512, + placeholder: 'ReportChatDescription' + }); + + this.body.append(inputField.container); + } +} diff --git a/src/lang.ts b/src/lang.ts index b959ad68..ee680939 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -549,6 +549,17 @@ const lang = { "Create": "Create", "ViewDiscussion": "View discussion", "MessageScheduledUntilOnline": "Scheduled until online", + "ReportChat": "Report", + "ReportChatSpam": "Spam", + // "ReportChatFakeAccount": "Fake Account", + "ReportChatViolence": "Violence", + "ReportChatPornography": "Pornography", + "ReportChatChild": "Child Abuse", + "ReportChatOther": "Other", + "ReportChatDescription": "Description", + "ReportInfo": "Please enter any additional details relevant to your report.", + "ReportSentInfo": "Telegram moderators will review your report.\nThank you for your cooperation!", + "ReportHint": "Additional details...", // * macos "AccountSettings.Filters": "Chat Folders", @@ -609,6 +620,7 @@ const lang = { "one_value": "%d Comment", "other_value": "%d Comments" }, + "ChatTitle.ReportMessages": "Report Messages", "Chat.Send.WithoutSound": "Send Without Sound", "Chat.Send.SetReminder": "Set a Reminder", "Chat.Send.ScheduledMessage": "Schedule Message", diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 6f86df8b..834e2f69 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -17,7 +17,7 @@ import { createPosterForVideo } from "../../helpers/files"; import { copy, 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, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint } from "../../layer"; +import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason } from "../../layer"; import { InvokeApiOptions } from "../../types"; import I18n, { i18n, join, langPack, LangPackKey, _i18n } from "../langPack"; import { logger, LogTypes } from "../logger"; @@ -2866,6 +2866,17 @@ export class AppMessagesManager { } } + public reportMessages(peerId: number, mids: number[], reason: ReportReason['_'], message?: string) { + return apiManager.invokeApiSingle('messages.report', { + peer: appPeersManager.getInputPeerById(peerId), + id: mids.map(mid => appMessagesIdsManager.getServerMessageId(mid)), + reason: { + _: reason + }, + message + }); + } + public startBot(botId: number, chatId: number, startParam: string) { const peerId = chatId ? -chatId : botId; if(startParam) { diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index 4d9dcf08..8bb9c921 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -32,7 +32,7 @@ export class AppStickersManager { private getGreetingStickersPromise: Promise; constructor() { - this.getStickerSet({id: 'emoji'}, {saveById: true}); + this.getAnimatedEmojiStickerSet(); rootScope.addMultipleEventsListeners({ updateNewStickerSet: (update) => { @@ -121,6 +121,10 @@ export class AppStickersManager { }); } + public getAnimatedEmojiStickerSet() { + return this.getStickerSet({id: 'emoji'}, {saveById: true}); + } + public async getRecentStickers(): Promise> { @@ -139,6 +143,15 @@ export class AppStickersManager { const pack = stickerSet.packs.find(p => p.emoticon === emoji); return pack ? appDocsManager.getDoc(pack.documents[0]) : undefined; } + + public preloadAnimatedEmojiSticker(emoji: string) { + return this.getAnimatedEmojiStickerSet().then(() => { + const doc = this.getAnimatedEmojiSticker(emoji); + if(doc) { + return appDocsManager.downloadDoc(doc); + } + }); + } public saveStickerSet(res: Omit, id: string) { //console.log('stickers save set', res);w diff --git a/src/scss/partials/popups/_instanceDeactivated.scss b/src/scss/partials/popups/_instanceDeactivated.scss index b2286e9f..b0cff2b6 100644 --- a/src/scss/partials/popups/_instanceDeactivated.scss +++ b/src/scss/partials/popups/_instanceDeactivated.scss @@ -1,3 +1,9 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + .popup-instance-deactivated { background-color: rgba(0, 0, 0, .6); diff --git a/src/scss/partials/popups/_joinChatInvite.scss b/src/scss/partials/popups/_joinChatInvite.scss index 24f42b56..a2239555 100644 --- a/src/scss/partials/popups/_joinChatInvite.scss +++ b/src/scss/partials/popups/_joinChatInvite.scss @@ -1,3 +1,9 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + .popup-join-chat-invite { user-select: none; diff --git a/src/scss/partials/popups/_reportMessages.scss b/src/scss/partials/popups/_reportMessages.scss new file mode 100644 index 00000000..421e48a9 --- /dev/null +++ b/src/scss/partials/popups/_reportMessages.scss @@ -0,0 +1,28 @@ +.popup-report-messages-confirm { + user-select: none; + + .media-sticker-wrapper { + width: 100px; + height: 100px; + position: relative; + margin: 0 auto; + } + + .popup-body { + margin: 1em -.5rem .375rem -.5rem; + overflow: unset; + } + + .popup-description { + font-size: .875rem; + text-align: center; + } + + .popup-buttons { + margin-top: .625rem; + } + + .input-field { + width: 100%; + } +} diff --git a/src/scss/style.scss b/src/scss/style.scss index 1def2547..1c2231ab 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -276,6 +276,7 @@ html.night { @import "partials/popups/forward"; @import "partials/popups/instanceDeactivated"; @import "partials/popups/joinChatInvite"; +@import "partials/popups/reportMessages"; @import "partials/pages/pages"; @import "partials/pages/authCode";