diff --git a/src/components/appSelectPeers.ts b/src/components/appSelectPeers.ts index 921a87f9..ee4ee2ad 100644 --- a/src/components/appSelectPeers.ts +++ b/src/components/appSelectPeers.ts @@ -69,6 +69,8 @@ export default class AppSelectPeers { private peerId = 0; private placeholder: LangPackKey; + + private selfPresence: LangPackKey = 'Presence.YourChat'; constructor(options: { appendTo: AppSelectPeers['appendTo'], @@ -81,7 +83,8 @@ export default class AppSelectPeers { multiSelect?: AppSelectPeers['multiSelect'], rippleEnabled?: boolean, avatarSize?: AppSelectPeers['avatarSize'], - placeholder?: LangPackKey + placeholder?: LangPackKey, + selfPresence?: LangPackKey }) { safeAssign(this, options); @@ -449,7 +452,7 @@ export default class AppSelectPeers { if(peerId < 0) { subtitleEl = appProfileManager.getChatMembersString(-peerId); } else if(peerId === rootScope.myId) { - subtitleEl = i18n('Presence.YourChat'); + subtitleEl = i18n(this.selfPresence); } else { subtitleEl = appUsersManager.getUserStatusString(peerId); } diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 720c93a3..e7171d47 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -67,15 +67,22 @@ import replaceContent from "../../helpers/dom/replaceContent"; import setInnerHTML from "../../helpers/dom/setInnerHTML"; import whichChild from "../../helpers/dom/whichChild"; import { cancelAnimationByKey } from "../../helpers/animation"; +import assumeType from "../../helpers/assumeType"; +import { EmoticonsDropdown } from "../emoticonsDropdown"; const USE_MEDIA_TAILS = false; -const IGNORE_ACTIONS: Message.messageService['action']['_'][] = ['messageActionHistoryClear']; +const IGNORE_ACTIONS: Set = new Set([ + 'messageActionHistoryClear', + 'messageActionChatCreate' +]); const TEST_SCROLL_TIMES: number = undefined; let TEST_SCROLL = TEST_SCROLL_TIMES; let queueId = 0; +type GenerateLocalMessageType = IsService extends true ? Message.messageService : Message.message; + export default class ChatBubbles { public bubblesContainer: HTMLDivElement; private chatInner: HTMLDivElement; @@ -116,7 +123,7 @@ export default class ChatBubbles { private loadedBottomTimes = 0; private messagesQueuePromise: Promise = null; - private messagesQueue: {message: any, bubble: HTMLDivElement, reverse: boolean, promises: Promise[]}[] = []; + private messagesQueue: {message: any, bubble: HTMLElement, reverse: boolean, promises: Promise[]}[] = []; private messagesQueueOnRender: () => void = null; private messagesQueueOnRenderAdditional: () => void = null; @@ -1825,7 +1832,7 @@ export default class ChatBubbles { this.chatInner.classList.toggle('is-channel', isChannel); } - public renderMessagesQueue(message: any, bubble: HTMLDivElement, reverse: boolean, promises: Promise[]) { + public renderMessagesQueue(message: any, bubble: HTMLElement, reverse: boolean, promises: Promise[]) { /* let dateMessage = this.getDateContainerByMessage(message, reverse); if(reverse) dateMessage.container.insertBefore(bubble, dateMessage.div.nextSibling); else dateMessage.container.append(bubble); @@ -1891,6 +1898,11 @@ export default class ChatBubbles { } public setBubblePosition(bubble: HTMLElement, message: any, reverse: boolean) { + if(message.id < 0) { + this.chatInner.prepend(bubble); + return; + } + const dateMessage = this.getDateContainerByMessage(message, reverse); if(this.chat.type === 'scheduled' || this.chat.type === 'pinned'/* || true */) { // ! TEMP COMMENTED const offset = this.stickyIntersector ? 2 : 1; @@ -2031,10 +2043,14 @@ export default class ChatBubbles { const loadPromises: Promise[] = []; if(message._ === 'messageService') { - let action = message.action; - let _ = action._; - if(IGNORE_ACTIONS.includes(_) || (langPack.hasOwnProperty(_) && !langPack[_])) { - return bubble; + assumeType(message); + + const action = message.action; + if(action) { + const _ = action._; + if(IGNORE_ACTIONS.has(_) || (langPack.hasOwnProperty(_) && !langPack[_])) { + return bubble; + } } bubble.className = 'bubble service'; @@ -2042,7 +2058,9 @@ export default class ChatBubbles { bubbleContainer.innerHTML = ''; const s = document.createElement('div'); s.classList.add('service-msg'); - s.append(this.appMessagesManager.wrapMessageActionTextNew(message)); + if(action) { + s.append(this.appMessagesManager.wrapMessageActionTextNew(message)); + } bubbleContainer.append(s); if(updatePosition) { @@ -3055,15 +3073,136 @@ export default class ChatBubbles { return promise; } - private processLocalMessageRender(message: any) { - const bubble = this.renderMessage(message, false, false, undefined, false); + private renderEmptyPlaceholder(type: 'group' | 'saved' | 'noMessages' | 'noScheduledMessages' | 'greeting', bubble: HTMLElement, message: any, elements: (Node | string)[]) { + bubble.classList.add('empty-placeholder', 'empty-placeholder-' + type); + + let title: HTMLElement; + if(type === 'group') title = i18n('GroupEmptyTitle1'); + else if(type === 'saved') title = i18n('ChatYourSelfTitle'); + else if(type === 'noMessages' || type === 'greeting') title = i18n('NoMessages'); + else if(type === 'noScheduledMessages') title = i18n('NoScheduledMessages'); + title.classList.add('center', 'empty-placeholder-title'); + + elements.push(title); + + let listElements: HTMLElement[]; + if(type === 'group') { + elements.push(i18n('GroupEmptyTitle2')); + listElements = [ + i18n('GroupDescription1'), + i18n('GroupDescription2'), + i18n('GroupDescription3'), + i18n('GroupDescription4') + ]; + } else if(type === 'saved') { + listElements = [ + i18n('ChatYourSelfDescription1'), + i18n('ChatYourSelfDescription2'), + i18n('ChatYourSelfDescription3'), + i18n('ChatYourSelfDescription4') + ]; + } else if(type === 'greeting') { + const subtitle = i18n('NoMessagesGreetingsDescription'); + subtitle.classList.add('center', 'empty-placeholder-subtitle'); + + this.messagesQueue.findAndSplice(q => q.bubble === bubble); + + const stickerDiv = document.createElement('div'); + stickerDiv.classList.add('empty-placeholder-sticker'); + + const middleware = this.getMiddleware(); + const loadPromise = this.appStickersManager.getGreetingSticker().then(doc => { + if(!middleware()) return; + + const loadPromises: Promise[] = []; + wrapSticker({ + doc, + // doc: appDocsManager.getDoc("5431607541660389336"), // cubigator mockup + div: stickerDiv, + middleware, + lazyLoadQueue: this.lazyLoadQueue, + group: CHAT_ANIMATION_GROUP, + //play: !!message.pending || !multipleRender, + play: true, + loop: true, + withThumb: true, + loadPromises + }); + + attachClickEvent(stickerDiv, (e) => { + cancelEvent(e); + EmoticonsDropdown.onMediaClick({target: e.target}); + }); + + return Promise.all(loadPromises); + }); + + this.renderMessagesQueue(message, bubble, false, [loadPromise]); + + elements.push(subtitle, stickerDiv); + } + + if(listElements) { + elements.push( + ...listElements.map(elem => { + const span = document.createElement('span'); + span.classList.add('empty-placeholder-list-item'); + span.append(elem); + return span; + }) + ); + + if(type === 'group') { + listElements.forEach(elem => { + const i = document.createElement('span'); + i.classList.add('tgico-check'); + elem.prepend(i); + }); + } else if(type === 'saved') { + listElements.forEach(elem => { + const i = document.createElement('span'); + i.classList.add('empty-placeholder-list-bullet'); + i.innerText = '•'; + elem.prepend(i); + }); + } + } + + if(elements.length > 1) { + bubble.classList.add('has-description'); + } + + elements.forEach((element: any) => element.classList.add('empty-placeholder-line')); + } + + private processLocalMessageRender(message: Message) { + const bubble = this.renderMessage(message); bubble.classList.add('bubble-first', 'is-group-last', 'is-group-first'); bubble.classList.remove('can-have-tail', 'is-in'); - const messageDiv = bubble.querySelector('.message'); - const b = document.createElement('b'); - b.append(i18n('BotInfoTitle')); - messageDiv.prepend(b, '\n\n'); + const messageDiv = bubble.querySelector('.message, .service-msg'); + const elements: (Node | string)[] = []; + if(this.appPeersManager.isBot(this.peerId)) { + const b = document.createElement('b'); + b.append(i18n('BotInfoTitle')); + elements.push(b, '\n\n'); + } else if(this.appPeersManager.isAnyGroup(this.peerId) && this.appPeersManager.getPeer(this.peerId).pFlags.creator) { + this.renderEmptyPlaceholder('group', bubble, message, elements); + } else if(rootScope.myId === this.peerId) { + this.renderEmptyPlaceholder('saved', bubble, message, elements); + } else if(this.peerId > 0 && this.appMessagesManager.canWriteToPeer(this.peerId) && this.chat.type === 'chat') { + this.renderEmptyPlaceholder('greeting', bubble, message, elements); + } else if(this.chat.type === 'scheduled') { + this.renderEmptyPlaceholder('noScheduledMessages', bubble, message, elements); + } else { + this.renderEmptyPlaceholder('noMessages', bubble, message, elements); + } + + /* for(let i = 1; i < elements.length; i += 2) { + elements.splice(i, 0, '\n'); + } */ + + messageDiv.prepend(...elements); if(this.messagesQueueOnRenderAdditional) { this.onAnimateLadder = () => { @@ -3080,6 +3219,29 @@ export default class ChatBubbles { } } + private generateLocalFirstMessage(service?: T, fill?: (message: GenerateLocalMessageType) => void): GenerateLocalMessageType { + const offset = this.appMessagesManager.generateMessageId(0); + + const message: Omit & {message?: string} = { + _: service ? 'messageService' : 'message', + date: 0, + id: -(this.peerId + offset), + peer_id: this.appPeersManager.getOutputPeer(this.peerId), + pFlags: {} + }; + + if(!service) { + message.message = ''; + } + + assumeType>(message); + + fill && fill(message); + + this.appMessagesManager.saveMessages([message]); + return message; + } + private setLoaded(side: SliceSides, value: boolean) { const willChange = this.scrollable.loadedAll[side] !== value; if(!willChange) { @@ -3096,22 +3258,30 @@ export default class ChatBubbles { return; } - const offset = this.appMessagesManager.generateMessageId(0); - const message: Message.message = { - _: 'message', - date: 0, - id: -(this.peerId + offset), - message: userFull.bot_info.description, - peer_id: this.appPeersManager.getOutputPeer(this.peerId), - pFlags: { - bot_description: true - } - }; + const message = this.generateLocalFirstMessage(false, message => { + message.message = userFull.bot_info.description; + }); - this.appMessagesManager.saveMessages([message]); this.processLocalMessageRender(message); }); } + + this.checkIfEmptyPlaceholderNeeded(); + } + + public checkIfEmptyPlaceholderNeeded() { + if(this.scrollable.loadedAll.top && + this.scrollable.loadedAll.bottom && + !this.chatInner.childElementCount) { + this.log('inject empty peer placeholder'); + + const message = this.generateLocalFirstMessage(true); + this.processLocalMessageRender(message); + + return true; + } + + return false; } /** @@ -3409,6 +3579,8 @@ export default class ChatBubbles { delete this.dateMessages[i]; } } + + this.checkIfEmptyPlaceholderNeeded(); } } diff --git a/src/components/popups/forward.ts b/src/components/popups/forward.ts index e98b68dc..903494c6 100644 --- a/src/components/popups/forward.ts +++ b/src/components/popups/forward.ts @@ -24,7 +24,8 @@ export default class PopupForward extends PopupPickUser { }, onClose, placeholder: 'ShareModal.Search.ForwardPlaceholder', - chatRightsAction: 'send_messages' + chatRightsAction: 'send_messages', + selfPresence: 'ChatYourSelf' }); } } diff --git a/src/components/popups/pickUser.ts b/src/components/popups/pickUser.ts index 1d08d8da..26e3de93 100644 --- a/src/components/popups/pickUser.ts +++ b/src/components/popups/pickUser.ts @@ -19,6 +19,7 @@ export default class PopupPickUser extends PopupElement { placeholder: LangPackKey, chatRightsAction?: AppSelectPeers['chatRightsAction'], peerId?: number, + selfPresence?: LangPackKey }) { super('popup-forward', null, {closable: true, overlayClosable: true, body: true}); @@ -54,7 +55,8 @@ export default class PopupPickUser extends PopupElement { rippleEnabled: false, avatarSize: 46, peerId: options.peerId, - placeholder: options.placeholder + placeholder: options.placeholder, + selfPresence: options.selfPresence }); //this.scrollable = new Scrollable(this.body); diff --git a/src/lang.ts b/src/lang.ts index 40dcb7dd..c2eb49a5 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -494,6 +494,22 @@ const lang = { "AreYouSureClearDraftsTitle": "Delete cloud drafts", "AreYouSureClearDrafts": "Are you sure you want to delete all cloud drafts?", "BotInfoTitle": "What can this bot do?", + "ChatYourSelf": "forward here to save", + "GroupEmptyTitle1": "You have created a **group**.", + "GroupEmptyTitle2": "Groups can have:", + "GroupDescription1": "Up to 200,000 members", + "GroupDescription2": "Persistent chat history", + "GroupDescription3": "Public links such as t.me/title", + "GroupDescription4": "Admins with different rights", + "ChatYourSelfDescription1": "Forward messages here to save them", + "ChatYourSelfDescription2": "Send media and files to store them", + "ChatYourSelfDescription3": "Access this chat from any device", + "ChatYourSelfDescription4": "Use search to quickly find things", + "ChatYourSelfTitle": "Your cloud storage", + "ActionYouCreateGroup": "You created the group", + "NoMessages": "No messages here yet...", + "NoScheduledMessages": "No scheduled messages here yet...", + "NoMessagesGreetingsDescription": "Send a message or tap the greeting below.", // * macos "AccountSettings.Filters": "Chat Folders", diff --git a/src/layer.d.ts b/src/layer.d.ts index 5c54394f..11c6a686 100644 --- a/src/layer.d.ts +++ b/src/layer.d.ts @@ -833,7 +833,6 @@ export namespace Message { pinned?: true, unread?: true, is_outgoing?: true, - bot_description?: true, }>, id: number, from_id?: Peer, @@ -875,6 +874,7 @@ export namespace Message { legacy?: true, unread?: true, is_outgoing?: true, + is_single?: true, }>, id: number, from_id?: Peer, diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index ac06daba..db6e30b0 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -2788,12 +2788,22 @@ export class AppMessagesManager { break; } + case 'messageActionChatCreate': { + const myId = appUsersManager.getSelf().id; + if(message.fromId === myId) { + _ += 'You'; + } else { + args = [getNameDivHTML(message.fromId, plain)]; + } + + break; + } + case 'messageActionPinMessage': case 'messageActionContactSignUp': case 'messageActionChatReturn': case 'messageActionChatLeave': case 'messageActionChatJoined': - case 'messageActionChatCreate': case 'messageActionChatEditPhoto': case 'messageActionChatDeletePhoto': case 'messageActionChatEditVideo': @@ -3419,7 +3429,7 @@ export class AppMessagesManager { _: 'messageService', pFlags: { is_single: true - } as any, + }, id: this.generateMessageId(maxMessageId, true), date: message.date, from_id: {_: 'peerUser', user_id: 0}/* message.from_id */, diff --git a/src/lib/appManagers/appStickersManager.ts b/src/lib/appManagers/appStickersManager.ts index 16f62593..ac09d818 100644 --- a/src/lib/appManagers/appStickersManager.ts +++ b/src/lib/appManagers/appStickersManager.ts @@ -26,6 +26,10 @@ export class AppStickersManager { private getStickerSetPromises: {[setId: string]: Promise} = {}; private getStickersByEmoticonsPromises: {[emoticon: string]: Promise} = {}; + + private greetingStickers: Document.document[]; + private getGreetingStickersTimeout: number; + private getGreetingStickersPromise: Promise; constructor() { this.getStickerSet({id: 'emoji'}, {saveById: true}); @@ -36,6 +40,37 @@ export class AppStickersManager { rootScope.dispatchEvent('stickers_installed', update.stickerset.set); } }); + + this.getGreetingStickersTimeout = window.setTimeout(() => { + this.getGreetingStickersTimeout = undefined; + this.getGreetingSticker(true); + }, 5000); + } + + public getGreetingSticker(justPreload = false) { + if(this.getGreetingStickersTimeout) { + clearTimeout(this.getGreetingStickersTimeout); + this.getGreetingStickersTimeout = undefined; + } + + if(!this.getGreetingStickersPromise) { + this.getGreetingStickersPromise = this.getStickersByEmoticon('👋', false).then(docs => { + this.greetingStickers = docs.slice() as Document.document[]; + this.greetingStickers.sort((a, b) => Math.random() - Math.random()); + }); + } + + return this.getGreetingStickersPromise.then(() => { + let doc: Document.document; + if(!justPreload) { + doc = this.greetingStickers.shift(); + this.greetingStickers.push(doc); + } + + appDocsManager.downloadDoc(this.greetingStickers[0]); // preload next sticker + + return doc; + }); } public saveStickers(docs: Document[]) { @@ -255,15 +290,15 @@ export class AppStickersManager { }); } - public getStickersByEmoticon(emoticon: string) { + public getStickersByEmoticon(emoticon: string, includeOurStickers = true) { if(this.getStickersByEmoticonsPromises[emoticon]) return this.getStickersByEmoticonsPromises[emoticon]; return this.getStickersByEmoticonsPromises[emoticon] = Promise.all([ apiManager.invokeApiHashable('messages.getStickers', { emoticon }), - this.preloadStickerSets(), - this.getRecentStickers() + includeOurStickers ? this.preloadStickerSets() : [], + includeOurStickers ? this.getRecentStickers().then(res => res.packs) : [] ]).then(([messagesStickers, installedSets, recentStickers]) => { const foundStickers = (messagesStickers as MessagesStickers.messagesStickers).stickers.map(sticker => appDocsManager.saveDoc(sticker)); const cachedStickersAnimated: Document.document[] = [], cachedStickersStatic: Document.document[] = []; @@ -281,7 +316,7 @@ export class AppStickersManager { } }; - iteratePacks(recentStickers.packs); + iteratePacks(recentStickers); for(const set of installedSets) { iteratePacks(set.packs); diff --git a/src/lib/langPack.ts b/src/lib/langPack.ts index 75845952..6db5b3aa 100644 --- a/src/lib/langPack.ts +++ b/src/lib/langPack.ts @@ -17,6 +17,7 @@ import rootScope from "./rootScope"; export const langPack: {[actionType: string]: LangPackKey} = { "messageActionChatCreate": "ActionCreateGroup", + "messageActionChatCreateYou": "ActionYouCreateGroup", "messageActionChatEditTitle": "ActionChangedTitle", "messageActionChatEditPhoto": "ActionChangedPhoto", "messageActionChatEditVideo": "ActionChangedVideo", diff --git a/src/scripts/in/schema_additional_params.json b/src/scripts/in/schema_additional_params.json index 53c86644..77888d4c 100644 --- a/src/scripts/in/schema_additional_params.json +++ b/src/scripts/in/schema_additional_params.json @@ -51,8 +51,7 @@ {"name": "unread", "type": "true"}, {"name": "is_outgoing", "type": "true"}, {"name": "rReply", "type": "string"}, - {"name": "viaBotId", "type": "number"}, - {"name": "bot_description", "type": "true"} + {"name": "viaBotId", "type": "number"} ] }, { "predicate": "messageService", @@ -64,7 +63,8 @@ {"name": "unread", "type": "true"}, {"name": "is_outgoing", "type": "true"}, {"name": "rReply", "type": "string"}, - {"name": "viaBotId", "type": "number"} + {"name": "viaBotId", "type": "number"}, + {"name": "is_single", "type": "true"} ] }, { "predicate": "messageEmpty", diff --git a/src/scss/components/_global.scss b/src/scss/components/_global.scss index fd1803a2..6f13ca65 100644 --- a/src/scss/components/_global.scss +++ b/src/scss/components/_global.scss @@ -111,6 +111,10 @@ Utility Classes text-align: center; } +/* .reset-align { + text-align: unset; +} */ + .justify-start { justify-content: flex-start!important; } diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 02c2f4aa..de6bbb7f 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -728,6 +728,81 @@ $bubble-margin: .25rem; flex: 1 1 auto; align-items: center; + &.empty-placeholder { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + &.has-description { + .service-msg { + flex-direction: column; + align-items: flex-start !important; + padding: .75rem 1rem .875rem !important; + } + + .center { + align-self: center; + } + + .empty-placeholder-title { + font-weight: 500; + font-size: 1rem !important; + } + + .bubble-content { + border-radius: 1.5rem !important; + } + } + + .empty-placeholder-line + .empty-placeholder-line { + margin-top: .5rem; + } + + .tgico-check { + margin-right: .25rem; + font-size: 1.25rem; + vertical-align: bottom; + margin-left: -.1875rem; + } + + .empty-placeholder-list-bullet { + margin-right: .3125rem; + } + + &:not(:first-child:last-child) { + .bubble-content-wrapper { + transform: scale3d(.8, .8, 1) translateX(0); + //transform: scale(.8) translateX(0); + opacity: 0; + } + } + + &.empty-placeholder-group { + .empty-placeholder-list-item { + margin-top: .4375rem !important; + } + } + + &.empty-placeholder-greeting { + .service-msg { + max-width: 232px; + } + + .empty-placeholder-subtitle { + margin-top: .25rem !important; + } + } + + .empty-placeholder-sticker { + margin-top: .75rem !important; + position: relative; + width: 200px; + height: 200px; + cursor: pointer; + } + } + .time { display: none !important; } @@ -1641,8 +1716,10 @@ $bubble-margin: .25rem; opacity: 0; } - @include respond-to(not-handhelds) { - max-width: 85%; + .bubble:not(.service) & { + @include respond-to(not-handhelds) { + max-width: 85%; + } } @include respond-to(handhelds) { @@ -1681,6 +1758,10 @@ $bubble-margin: .25rem; align-self: center; justify-content: center; + b { + color: inherit; + } + .bubble-content { background-color: transparent; border-radius: .875rem;