Browse Source

Added chat placeholders

master
Eduard Kuzmenko 3 years ago
parent
commit
7f85c08a5e
  1. 7
      src/components/appSelectPeers.ts
  2. 224
      src/components/chat/bubbles.ts
  3. 3
      src/components/popups/forward.ts
  4. 4
      src/components/popups/pickUser.ts
  5. 16
      src/lang.ts
  6. 2
      src/layer.d.ts
  7. 14
      src/lib/appManagers/appMessagesManager.ts
  8. 43
      src/lib/appManagers/appStickersManager.ts
  9. 1
      src/lib/langPack.ts
  10. 6
      src/scripts/in/schema_additional_params.json
  11. 4
      src/scss/components/_global.scss
  12. 85
      src/scss/partials/_chatBubble.scss

7
src/components/appSelectPeers.ts

@ -69,6 +69,8 @@ export default class AppSelectPeers { @@ -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 { @@ -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 { @@ -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);
}

224
src/components/chat/bubbles.ts

@ -67,15 +67,22 @@ import replaceContent from "../../helpers/dom/replaceContent"; @@ -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<Message.messageService['action']['_']> = new Set([
'messageActionHistoryClear',
'messageActionChatCreate'
]);
const TEST_SCROLL_TIMES: number = undefined;
let TEST_SCROLL = TEST_SCROLL_TIMES;
let queueId = 0;
type GenerateLocalMessageType<IsService> = 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 { @@ -116,7 +123,7 @@ export default class ChatBubbles {
private loadedBottomTimes = 0;
private messagesQueuePromise: Promise<void> = null;
private messagesQueue: {message: any, bubble: HTMLDivElement, reverse: boolean, promises: Promise<void>[]}[] = [];
private messagesQueue: {message: any, bubble: HTMLElement, reverse: boolean, promises: Promise<void>[]}[] = [];
private messagesQueueOnRender: () => void = null;
private messagesQueueOnRenderAdditional: () => void = null;
@ -1825,7 +1832,7 @@ export default class ChatBubbles { @@ -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<any>[]) {
public renderMessagesQueue(message: any, bubble: HTMLElement, reverse: boolean, promises: Promise<any>[]) {
/* 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 { @@ -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 { @@ -2031,10 +2043,14 @@ export default class ChatBubbles {
const loadPromises: Promise<any>[] = [];
if(message._ === 'messageService') {
let action = message.action;
let _ = action._;
if(IGNORE_ACTIONS.includes(_) || (langPack.hasOwnProperty(_) && !langPack[_])) {
return bubble;
assumeType<Message.messageService>(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 { @@ -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 { @@ -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<any>[] = [];
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 { @@ -3080,6 +3219,29 @@ export default class ChatBubbles {
}
}
private generateLocalFirstMessage<T extends boolean>(service?: T, fill?: (message: GenerateLocalMessageType<T>) => void): GenerateLocalMessageType<T> {
const offset = this.appMessagesManager.generateMessageId(0);
const message: Omit<Message.message | Message.messageService, 'message'> & {message?: string} = {
_: service ? 'messageService' : 'message',
date: 0,
id: -(this.peerId + offset),
peer_id: this.appPeersManager.getOutputPeer(this.peerId),
pFlags: {}
};
if(!service) {
message.message = '';
}
assumeType<GenerateLocalMessageType<T>>(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 { @@ -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 { @@ -3409,6 +3579,8 @@ export default class ChatBubbles {
delete this.dateMessages[i];
}
}
this.checkIfEmptyPlaceholderNeeded();
}
}

3
src/components/popups/forward.ts

@ -24,7 +24,8 @@ export default class PopupForward extends PopupPickUser { @@ -24,7 +24,8 @@ export default class PopupForward extends PopupPickUser {
},
onClose,
placeholder: 'ShareModal.Search.ForwardPlaceholder',
chatRightsAction: 'send_messages'
chatRightsAction: 'send_messages',
selfPresence: 'ChatYourSelf'
});
}
}

4
src/components/popups/pickUser.ts

@ -19,6 +19,7 @@ export default class PopupPickUser extends PopupElement { @@ -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 { @@ -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);

16
src/lang.ts

@ -494,6 +494,22 @@ const lang = { @@ -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",

2
src/layer.d.ts vendored

@ -833,7 +833,6 @@ export namespace Message { @@ -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 { @@ -875,6 +874,7 @@ export namespace Message {
legacy?: true,
unread?: true,
is_outgoing?: true,
is_single?: true,
}>,
id: number,
from_id?: Peer,

14
src/lib/appManagers/appMessagesManager.ts

@ -2788,12 +2788,22 @@ export class AppMessagesManager { @@ -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 { @@ -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 */,

43
src/lib/appManagers/appStickersManager.ts

@ -26,6 +26,10 @@ export class AppStickersManager { @@ -26,6 +26,10 @@ export class AppStickersManager {
private getStickerSetPromises: {[setId: string]: Promise<MessagesStickerSet>} = {};
private getStickersByEmoticonsPromises: {[emoticon: string]: Promise<Document[]>} = {};
private greetingStickers: Document.document[];
private getGreetingStickersTimeout: number;
private getGreetingStickersPromise: Promise<void>;
constructor() {
this.getStickerSet({id: 'emoji'}, {saveById: true});
@ -36,6 +40,37 @@ export class AppStickersManager { @@ -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 { @@ -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 { @@ -281,7 +316,7 @@ export class AppStickersManager {
}
};
iteratePacks(recentStickers.packs);
iteratePacks(recentStickers);
for(const set of installedSets) {
iteratePacks(set.packs);

1
src/lib/langPack.ts

@ -17,6 +17,7 @@ import rootScope from "./rootScope"; @@ -17,6 +17,7 @@ import rootScope from "./rootScope";
export const langPack: {[actionType: string]: LangPackKey} = {
"messageActionChatCreate": "ActionCreateGroup",
"messageActionChatCreateYou": "ActionYouCreateGroup",
"messageActionChatEditTitle": "ActionChangedTitle",
"messageActionChatEditPhoto": "ActionChangedPhoto",
"messageActionChatEditVideo": "ActionChangedVideo",

6
src/scripts/in/schema_additional_params.json

@ -51,8 +51,7 @@ @@ -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 @@ @@ -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",

4
src/scss/components/_global.scss

@ -111,6 +111,10 @@ Utility Classes @@ -111,6 +111,10 @@ Utility Classes
text-align: center;
}
/* .reset-align {
text-align: unset;
} */
.justify-start {
justify-content: flex-start!important;
}

85
src/scss/partials/_chatBubble.scss

@ -728,6 +728,81 @@ $bubble-margin: .25rem; @@ -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; @@ -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; @@ -1681,6 +1758,10 @@ $bubble-margin: .25rem;
align-self: center;
justify-content: center;
b {
color: inherit;
}
.bubble-content {
background-color: transparent;
border-radius: .875rem;

Loading…
Cancel
Save