Eduard Kuzmenko
3 years ago
52 changed files with 2358 additions and 315 deletions
@ -0,0 +1,185 @@
@@ -0,0 +1,185 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/ |
||||
|
||||
import callbackify from "../../helpers/callbackify"; |
||||
import { formatNumber } from "../../helpers/number"; |
||||
import { MessageUserReaction, ReactionCount } from "../../layer"; |
||||
import appReactionsManager from "../../lib/appManagers/appReactionsManager"; |
||||
import RLottiePlayer from "../../lib/rlottie/rlottiePlayer"; |
||||
import SetTransition from "../singleTransition"; |
||||
import StackedAvatars from "../stackedAvatars"; |
||||
import { wrapSticker, wrapStickerAnimation } from "../wrappers"; |
||||
|
||||
const CLASS_NAME = 'reaction'; |
||||
const TAG_NAME = CLASS_NAME + '-element'; |
||||
const REACTION_INLINE_SIZE = 14; |
||||
const REACTION_BLOCK_SIZE = 16; |
||||
|
||||
export const REACTION_DISPLAY_INLINE_COUNTER_AT = 2; |
||||
export const REACTION_DISPLAY_BLOCK_COUNTER_AT = 4; |
||||
|
||||
export type ReactionLayoutType = 'block' | 'inline'; |
||||
|
||||
export default class ReactionElement extends HTMLElement { |
||||
private type: ReactionLayoutType; |
||||
private counter: HTMLElement; |
||||
private stickerContainer: HTMLElement; |
||||
private stackedAvatars: StackedAvatars; |
||||
private canRenderAvatars: boolean; |
||||
private _reactionCount: ReactionCount; |
||||
|
||||
constructor() { |
||||
super(); |
||||
this.classList.add(CLASS_NAME); |
||||
} |
||||
|
||||
public get reactionCount() { |
||||
return this._reactionCount; |
||||
} |
||||
|
||||
public set reactionCount(reactionCount: ReactionCount) { |
||||
this._reactionCount = reactionCount; |
||||
} |
||||
|
||||
public get count() { |
||||
return this.reactionCount.count; |
||||
} |
||||
|
||||
public init(type: ReactionLayoutType) { |
||||
this.type = type; |
||||
this.classList.add(CLASS_NAME + '-' + type); |
||||
} |
||||
|
||||
public setCanRenderAvatars(canRenderAvatars: boolean) { |
||||
this.canRenderAvatars = canRenderAvatars; |
||||
} |
||||
|
||||
public render(doNotRenderSticker?: boolean) { |
||||
const hadStickerContainer = !!this.stickerContainer; |
||||
if(!hadStickerContainer) { |
||||
this.stickerContainer = document.createElement('div'); |
||||
this.stickerContainer.classList.add(CLASS_NAME + '-sticker'); |
||||
this.append(this.stickerContainer); |
||||
} |
||||
|
||||
const reactionCount = this.reactionCount; |
||||
if(!doNotRenderSticker && !hadStickerContainer) { |
||||
const availableReaction = appReactionsManager.getReaction(reactionCount.reaction); |
||||
callbackify(availableReaction, (availableReaction) => { |
||||
const size = this.type === 'inline' ? REACTION_INLINE_SIZE : REACTION_BLOCK_SIZE; |
||||
wrapSticker({ |
||||
div: this.stickerContainer, |
||||
doc: availableReaction.static_icon, |
||||
width: size, |
||||
height: size |
||||
}); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
public renderCounter() { |
||||
const reactionCount = this.reactionCount; |
||||
const displayOn = this.type === 'inline' ? REACTION_DISPLAY_INLINE_COUNTER_AT : REACTION_DISPLAY_BLOCK_COUNTER_AT; |
||||
if(reactionCount.count >= displayOn || (this.type === 'block' && !this.canRenderAvatars)) { |
||||
if(!this.counter) { |
||||
this.counter = document.createElement(this.type === 'inline' ? 'i' : 'span'); |
||||
this.counter.classList.add(CLASS_NAME + '-counter'); |
||||
} |
||||
|
||||
const formatted = formatNumber(reactionCount.count); |
||||
if(this.counter.textContent !== formatted) { |
||||
this.counter.textContent = formatted; |
||||
} |
||||
|
||||
if(!this.counter.parentElement) { |
||||
this.append(this.counter); |
||||
} |
||||
} else if(this.counter?.parentElement) { |
||||
this.counter.remove(); |
||||
this.counter = undefined; |
||||
} |
||||
} |
||||
|
||||
public renderAvatars(recentReactions: MessageUserReaction[]) { |
||||
if(this.type === 'inline') { |
||||
return; |
||||
} |
||||
|
||||
if(this.reactionCount.count >= REACTION_DISPLAY_BLOCK_COUNTER_AT || !this.canRenderAvatars) { |
||||
if(this.stackedAvatars) { |
||||
this.stackedAvatars.container.remove(); |
||||
this.stackedAvatars = undefined; |
||||
} |
||||
|
||||
return; |
||||
} |
||||
|
||||
if(!this.stackedAvatars) { |
||||
this.stackedAvatars = new StackedAvatars({ |
||||
avatarSize: 16 |
||||
}); |
||||
|
||||
this.append(this.stackedAvatars.container); |
||||
} |
||||
|
||||
this.stackedAvatars.render(recentReactions.map(reaction => reaction.user_id.toPeerId())); |
||||
} |
||||
|
||||
public setIsChosen(isChosen = !!this.reactionCount.pFlags.chosen) { |
||||
if(this.type === 'inline') return; |
||||
const wasChosen = this.classList.contains('is-chosen') && !this.classList.contains('backwards'); |
||||
if(wasChosen !== isChosen) { |
||||
SetTransition(this, 'is-chosen', isChosen, this.isConnected ? 300 : 0); |
||||
} |
||||
} |
||||
|
||||
public fireAroundAnimation() { |
||||
callbackify(appReactionsManager.getReaction(this.reactionCount.reaction), (availableReaction) => { |
||||
const size = (this.type === 'inline' ? REACTION_INLINE_SIZE : REACTION_BLOCK_SIZE) + 14; |
||||
const div = document.createElement('div'); |
||||
div.classList.add(CLASS_NAME + '-sticker-activate'); |
||||
|
||||
Promise.all([ |
||||
wrapSticker({ |
||||
div: div, |
||||
doc: availableReaction.center_icon, |
||||
width: size, |
||||
height: size, |
||||
withThumb: false, |
||||
needUpscale: true, |
||||
play: false, |
||||
skipRatio: 1, |
||||
group: 'none', |
||||
needFadeIn: false |
||||
}) as Promise<RLottiePlayer>, |
||||
|
||||
wrapStickerAnimation({ |
||||
doc: availableReaction.around_animation, |
||||
size: 80, |
||||
target: this.stickerContainer, |
||||
side: 'center', |
||||
skipRatio: 1, |
||||
play: false |
||||
}).stickerPromise |
||||
]).then(([activatePlayer, aroundPlayer]) => { |
||||
activatePlayer.addEventListener('enterFrame', (frameNo) => { |
||||
if(frameNo === activatePlayer.maxFrame) { |
||||
activatePlayer.remove(); |
||||
div.remove(); |
||||
} |
||||
}); |
||||
|
||||
activatePlayer.addEventListener('firstFrame', () => { |
||||
this.stickerContainer.prepend(div); |
||||
activatePlayer.play(); |
||||
aroundPlayer.play(); |
||||
}, {once: true}); |
||||
}); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
customElements.define(TAG_NAME, ReactionElement); |
@ -0,0 +1,198 @@
@@ -0,0 +1,198 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/ |
||||
|
||||
import { forEachReverse } from "../../helpers/array"; |
||||
import positionElementByIndex from "../../helpers/dom/positionElementByIndex"; |
||||
import { Message, ReactionCount } from "../../layer"; |
||||
import appReactionsManager from "../../lib/appManagers/appReactionsManager"; |
||||
import rootScope from "../../lib/rootScope"; |
||||
import ReactionElement, { ReactionLayoutType, REACTION_DISPLAY_BLOCK_COUNTER_AT } from "./reaction"; |
||||
|
||||
const CLASS_NAME = 'reactions'; |
||||
const TAG_NAME = CLASS_NAME + '-element'; |
||||
|
||||
const elements: Map<string, Set<ReactionsElement>> = new Map(); |
||||
rootScope.addEventListener('message_reactions', ({message, changedResults}) => { |
||||
const key = message.peerId + '_' + message.mid; |
||||
const set = elements.get(key); |
||||
if(!set) { |
||||
rootScope.dispatchEvent('missed_reactions_element', {message, changedResults}); |
||||
return; |
||||
} |
||||
|
||||
for(const element of set) { |
||||
element.update(message, changedResults); |
||||
} |
||||
}); |
||||
|
||||
export default class ReactionsElement extends HTMLElement { |
||||
private message: Message.message; |
||||
private key: string; |
||||
private isPlaceholder: boolean; |
||||
private type: ReactionLayoutType; |
||||
private sorted: ReactionElement[]; |
||||
private onConnectCallback: () => void; |
||||
|
||||
constructor() { |
||||
super(); |
||||
this.classList.add(CLASS_NAME); |
||||
this.sorted = []; |
||||
} |
||||
|
||||
connectedCallback() { |
||||
let set = elements.get(this.key); |
||||
if(!set) { |
||||
elements.set(this.key, set = new Set()); |
||||
} |
||||
|
||||
set.add(this); |
||||
|
||||
if(this.onConnectCallback && this.isConnected) { |
||||
this.onConnectCallback(); |
||||
this.onConnectCallback = undefined; |
||||
} |
||||
} |
||||
|
||||
disconnectedCallback() { |
||||
const set = elements.get(this.key); |
||||
set.delete(this); |
||||
if(!set.size) { |
||||
elements.delete(this.key); |
||||
} |
||||
} |
||||
|
||||
public getReactionCount(reactionElement: ReactionElement) { |
||||
return this.sorted[this.sorted.indexOf(reactionElement)].reactionCount; |
||||
} |
||||
|
||||
public getMessage() { |
||||
return this.message; |
||||
} |
||||
|
||||
public init(message: Message.message, type: ReactionLayoutType, isPlaceholder?: boolean) { |
||||
if(this.key !== undefined) { |
||||
this.disconnectedCallback(); |
||||
} |
||||
|
||||
this.message = message; |
||||
this.key = this.message.peerId + '_' + this.message.mid; |
||||
this.isPlaceholder = isPlaceholder; |
||||
|
||||
if(this.type !== type) { |
||||
this.type = type; |
||||
this.classList.add(CLASS_NAME + '-' + type); |
||||
} |
||||
|
||||
this.connectedCallback(); |
||||
} |
||||
|
||||
public changeMessage(message: Message.message) { |
||||
return this.init(message, this.type, this.isPlaceholder); |
||||
} |
||||
|
||||
public update(message: Message.message, changedResults?: ReactionCount[]) { |
||||
this.message = message; |
||||
this.render(changedResults); |
||||
} |
||||
|
||||
public render(changedResults?: ReactionCount[]) { |
||||
const reactions = this.message.reactions; |
||||
const hasReactions = !!(reactions && reactions.results.length); |
||||
this.classList.toggle('has-no-reactions', !hasReactions); |
||||
if(!hasReactions && !this.sorted.length) return; |
||||
|
||||
const availableReactionsResult = appReactionsManager.getAvailableReactions(); |
||||
// callbackify(availableReactionsResult, () => {
|
||||
const counts = hasReactions ? ( |
||||
availableReactionsResult instanceof Promise ? |
||||
reactions.results : |
||||
reactions.results.filter(reactionCount => { |
||||
return appReactionsManager.isReactionActive(reactionCount.reaction); |
||||
}) |
||||
) : []; |
||||
|
||||
forEachReverse(this.sorted, (reactionElement, idx, arr) => { |
||||
const reaction = reactionElement.reactionCount.reaction; |
||||
const found = counts.some(reactionCount => reactionCount.reaction === reaction); |
||||
if(!found) { |
||||
arr.splice(idx, 1); |
||||
reactionElement.remove(); |
||||
} |
||||
}); |
||||
|
||||
const totalReactions = counts.reduce((acc, c) => acc + c.count, 0); |
||||
const canRenderAvatars = reactions && !!reactions.pFlags.can_see_list && totalReactions < REACTION_DISPLAY_BLOCK_COUNTER_AT; |
||||
this.sorted = counts.map((reactionCount, idx) => { |
||||
const reactionElementIdx = this.sorted.findIndex(reactionElement => reactionElement.reactionCount.reaction === reactionCount.reaction); |
||||
let reactionElement = reactionElementIdx !== -1 && this.sorted[reactionElementIdx]; |
||||
if(!reactionElement) { |
||||
reactionElement = new ReactionElement(); |
||||
reactionElement.init(this.type); |
||||
} |
||||
|
||||
positionElementByIndex(reactionElement, this, idx); |
||||
|
||||
const recentReactions = reactions.recent_reactions ? reactions.recent_reactions.filter(reaction => reaction.reaction === reactionCount.reaction) : []; |
||||
reactionElement.reactionCount = {...reactionCount}; |
||||
reactionElement.setCanRenderAvatars(canRenderAvatars); |
||||
reactionElement.render(this.isPlaceholder); |
||||
reactionElement.renderCounter(); |
||||
reactionElement.renderAvatars(recentReactions); |
||||
reactionElement.setIsChosen(); |
||||
|
||||
return reactionElement; |
||||
}); |
||||
|
||||
// this.sorted.forEach((reactionElement, idx) => {
|
||||
// /* if(this.type === 'block' && this.childElementCount !== this.sorted.length) { // because of appended time
|
||||
// idx += 1;
|
||||
// } */
|
||||
|
||||
// positionElementByIndex(reactionElement, this, idx);
|
||||
// });
|
||||
|
||||
if(!this.isPlaceholder && changedResults?.length) { |
||||
if(this.isConnected) { |
||||
this.handleChangedResults(changedResults); |
||||
} else { |
||||
this.onConnectCallback = () => { |
||||
this.handleChangedResults(changedResults); |
||||
}; |
||||
} |
||||
} |
||||
// });
|
||||
|
||||
// ! тут вообще не должно быть этого кода, но пока он побудет тут
|
||||
if(!this.sorted.length && this.type === 'block') { |
||||
const parentElement = this.parentElement; |
||||
this.remove(); |
||||
|
||||
if(parentElement.classList.contains('document-message') && !parentElement.childNodes.length) { |
||||
parentElement.remove(); |
||||
return; |
||||
} |
||||
|
||||
const timeSpan = this.querySelector('.time'); |
||||
if(timeSpan) { |
||||
parentElement.append(timeSpan); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private handleChangedResults(changedResults: ReactionCount[]) { |
||||
// ! temp
|
||||
if(this.message.peerId !== rootScope.peerId) return; |
||||
|
||||
changedResults.forEach(reactionCount => { |
||||
const reactionElement = this.sorted.find(reactionElement => reactionElement.reactionCount.reaction === reactionCount.reaction); |
||||
if(reactionElement) { |
||||
reactionElement.fireAroundAnimation(); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
customElements.define(TAG_NAME, ReactionsElement); |
@ -0,0 +1,220 @@
@@ -0,0 +1,220 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/ |
||||
|
||||
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; |
||||
import PopupElement from "."; |
||||
import { Message } from "../../layer"; |
||||
import { generateDelimiter, SettingSection } from "../sidebarLeft"; |
||||
import ReactionsElement from "../chat/reactions"; |
||||
import { horizontalMenu } from "../horizontalMenu"; |
||||
import Scrollable from "../scrollable"; |
||||
import ScrollableLoader from "../../helpers/scrollableLoader"; |
||||
import appDialogsManager from "../../lib/appManagers/appDialogsManager"; |
||||
import replaceContent from "../../helpers/dom/replaceContent"; |
||||
import appUsersManager from "../../lib/appManagers/appUsersManager"; |
||||
import appReactionsManager from "../../lib/appManagers/appReactionsManager"; |
||||
import { wrapSticker } from "../wrappers"; |
||||
import ReactionElement from "../chat/reaction"; |
||||
|
||||
export default class PopupReactedList extends PopupElement { |
||||
constructor( |
||||
private appMessagesManager: AppMessagesManager, |
||||
private message: Message.message |
||||
) { |
||||
super('popup-reacted-list', /* [{ |
||||
langKey: 'Close', |
||||
isCancel: true |
||||
}] */null, {closable: true, overlayClosable: true, body: true}); |
||||
|
||||
this.init(); |
||||
} |
||||
|
||||
private async init() { |
||||
const message = this.appMessagesManager.getGroupsFirstMessage(this.message); |
||||
|
||||
const canViewReadParticipants = this.appMessagesManager.canViewMessageReadParticipants(message); |
||||
|
||||
// this.body.append(generateDelimiter());
|
||||
|
||||
const reactionsElement = new ReactionsElement(); |
||||
const newMessage: Message.message = { |
||||
...message, |
||||
mid: 0, |
||||
id: 0, |
||||
reactions: { |
||||
_: 'messageReactions', |
||||
results: [], |
||||
|
||||
...message.reactions, |
||||
|
||||
pFlags: {}, |
||||
recent_reactions: [] |
||||
} |
||||
}; |
||||
|
||||
newMessage.reactions.results = newMessage.reactions.results.map(reactionCount => { |
||||
return { |
||||
...reactionCount, |
||||
pFlags: {} |
||||
}; |
||||
}); |
||||
|
||||
reactionsElement.init(newMessage, 'block'); |
||||
reactionsElement.render(); |
||||
reactionsElement.classList.add('no-stripe'); |
||||
reactionsElement.classList.remove('has-no-reactions'); |
||||
|
||||
reactionsElement.append(this.btnClose); |
||||
|
||||
this.header.append(reactionsElement); |
||||
|
||||
const tabsContainer = document.createElement('div'); |
||||
tabsContainer.classList.add('tabs-container'); |
||||
tabsContainer.dataset.animation = 'tabs'; |
||||
|
||||
const loaders: Map<HTMLElement, ScrollableLoader> = new Map(); |
||||
|
||||
let hasAllReactions = false; |
||||
if(newMessage.reactions.results.length) { |
||||
const reaction = this.createFakeReaction('reactions', newMessage.reactions.results.reduce((acc, r) => acc + r.count, 0)); |
||||
|
||||
reactionsElement.prepend(reaction); |
||||
newMessage.reactions.results.unshift(reaction.reactionCount); |
||||
hasAllReactions = true; |
||||
} |
||||
|
||||
let hasReadParticipants = false; |
||||
if(canViewReadParticipants) { |
||||
try { |
||||
const readUserIds = await this.appMessagesManager.getMessageReadParticipants(message.peerId, message.mid); |
||||
if(!readUserIds.length) { |
||||
throw ''; |
||||
} |
||||
|
||||
const reaction = this.createFakeReaction('checks', readUserIds.length); |
||||
|
||||
reactionsElement.prepend(reaction); |
||||
newMessage.reactions.results.unshift(reaction.reactionCount); |
||||
hasReadParticipants = true; |
||||
} catch(err) { |
||||
|
||||
} |
||||
} |
||||
|
||||
newMessage.reactions.results.forEach(reactionCount => { |
||||
const scrollable = new Scrollable(undefined); |
||||
scrollable.container.classList.add('tabs-tab'); |
||||
|
||||
const section = new SettingSection({ |
||||
noShadow: true, |
||||
noDelimiter: true |
||||
}); |
||||
|
||||
const chatlist = appDialogsManager.createChatList({ |
||||
dialogSize: 72 |
||||
}); |
||||
|
||||
appDialogsManager.setListClickListener(chatlist, () => { |
||||
this.hide(); |
||||
}, undefined, false, true); |
||||
|
||||
section.content.append(chatlist); |
||||
scrollable.container.append(section.container); |
||||
|
||||
const skipReadParticipants = reactionCount.reaction !== 'checks'; |
||||
const skipReactionsList = reactionCount.reaction === 'checks'; |
||||
if(['checks', 'reactions'].includes(reactionCount.reaction)) { |
||||
reactionCount.reaction = undefined; |
||||
} |
||||
|
||||
let nextOffset: string; |
||||
const loader = new ScrollableLoader({ |
||||
scrollable, |
||||
getPromise: async() => { |
||||
const result = await this.appMessagesManager.getMessageReactionsListAndReadParticipants(message, undefined, reactionCount.reaction, nextOffset, skipReadParticipants, skipReactionsList); |
||||
nextOffset = result.nextOffset; |
||||
|
||||
result.combined.forEach(({peerId, reaction}) => { |
||||
const {dom} = appDialogsManager.addDialogNew({ |
||||
dialog: peerId, |
||||
autonomous: true, |
||||
container: chatlist, |
||||
avatarSize: 54, |
||||
rippleEnabled: false, |
||||
meAsSaved: false, |
||||
drawStatus: false |
||||
}); |
||||
|
||||
if(reaction) { |
||||
const stickerContainer = document.createElement('div'); |
||||
stickerContainer.classList.add('reacted-list-reaction-icon'); |
||||
const availableReaction = appReactionsManager.getReactionCached(reaction); |
||||
|
||||
wrapSticker({ |
||||
doc: availableReaction.static_icon, |
||||
div: stickerContainer, |
||||
width: 24, |
||||
height: 24 |
||||
}); |
||||
|
||||
dom.listEl.append(stickerContainer); |
||||
} |
||||
|
||||
replaceContent(dom.lastMessageSpan, appUsersManager.getUserStatusString(peerId.toUserId())); |
||||
}); |
||||
|
||||
return !nextOffset; |
||||
} |
||||
}); |
||||
|
||||
loaders.set(scrollable.container, loader); |
||||
|
||||
tabsContainer.append(scrollable.container); |
||||
}); |
||||
|
||||
this.body.append(tabsContainer); |
||||
|
||||
const selectTab = horizontalMenu(reactionsElement, tabsContainer, (id, tabContent) => { |
||||
if(id === (reactionsElement.childElementCount - 1)) { |
||||
return false; |
||||
} |
||||
|
||||
const reaction = reactionsElement.children[id] as ReactionElement; |
||||
const prevId = selectTab.prevId(); |
||||
if(prevId !== -1) { |
||||
(reactionsElement.children[prevId] as ReactionElement).setIsChosen(false); |
||||
} |
||||
|
||||
reaction.setIsChosen(true); |
||||
|
||||
const loader = loaders.get(tabContent); |
||||
loader.load(); |
||||
}); |
||||
|
||||
// selectTab(hasAllReactions && hasReadParticipants ? 1 : 0, false);
|
||||
selectTab(0, false); |
||||
|
||||
this.show(); |
||||
} |
||||
|
||||
private createFakeReaction(icon: string, count: number) { |
||||
const reaction = new ReactionElement(); |
||||
reaction.init('block'); |
||||
reaction.reactionCount = { |
||||
_: 'reactionCount', |
||||
count: count, |
||||
reaction: icon |
||||
}; |
||||
reaction.setCanRenderAvatars(false); |
||||
reaction.renderCounter(); |
||||
|
||||
const allReactionsSticker = document.createElement('div'); |
||||
allReactionsSticker.classList.add('reaction-counter', 'reaction-sticker-icon', 'tgico-' + icon); |
||||
reaction.prepend(allReactionsSticker); |
||||
|
||||
return reaction; |
||||
} |
||||
} |
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/ |
||||
|
||||
import AvatarElement from "./avatar"; |
||||
import type { LazyLoadQueueIntersector } from "./lazyLoadQueue"; |
||||
|
||||
const CLASS_NAME = 'stacked-avatars'; |
||||
const AVATAR_CLASS_NAME = CLASS_NAME + '-avatar'; |
||||
const AVATAR_CONTAINER_CLASS_NAME = AVATAR_CLASS_NAME + '-container'; |
||||
|
||||
export default class StackedAvatars { |
||||
public container: HTMLElement; |
||||
private lazyLoadQueue: LazyLoadQueueIntersector; |
||||
private avatarSize: number; |
||||
|
||||
constructor(options: { |
||||
lazyLoadQueue?: LazyLoadQueueIntersector, |
||||
avatarSize: number |
||||
}) { |
||||
this.lazyLoadQueue = options.lazyLoadQueue; |
||||
this.avatarSize = options.avatarSize; |
||||
|
||||
this.container = document.createElement('div'); |
||||
this.container.classList.add(CLASS_NAME); |
||||
|
||||
this.container.style.setProperty('--avatar-size', options.avatarSize + 'px'); |
||||
} |
||||
|
||||
public render(peerIds: PeerId[], loadPromises?: Promise<any>[]) { |
||||
const children = this.container.children; |
||||
peerIds.slice().reverse().forEach((peerId, idx) => { |
||||
let avatarContainer = children[idx] as HTMLElement; |
||||
if(!avatarContainer) { |
||||
avatarContainer = document.createElement('div'); |
||||
avatarContainer.classList.add(AVATAR_CONTAINER_CLASS_NAME); |
||||
} |
||||
|
||||
let avatarElem = avatarContainer.firstElementChild as AvatarElement; |
||||
if(!avatarElem) { |
||||
avatarElem = new AvatarElement(); |
||||
avatarElem.setAttribute('dialog', '0'); |
||||
avatarElem.classList.add('avatar-' + this.avatarSize, AVATAR_CLASS_NAME); |
||||
avatarElem.lazyLoadQueue = this.lazyLoadQueue; |
||||
avatarElem.loadPromises = loadPromises; |
||||
} |
||||
|
||||
avatarElem.setAttribute('peer', '' + peerId); |
||||
|
||||
if(!avatarElem.parentNode) { |
||||
avatarContainer.append(avatarElem); |
||||
} |
||||
|
||||
if(!avatarContainer.parentNode) { |
||||
this.container.append(avatarContainer); |
||||
} |
||||
}); |
||||
|
||||
// if were 3 and became 2
|
||||
(Array.from(children) as HTMLElement[]).slice(peerIds.length).forEach(el => el.remove()); |
||||
} |
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/ |
||||
|
||||
import {Awaited} from '../types'; |
||||
|
||||
export default function callbackifyAll<T extends readonly unknown[] | [], R extends any>( |
||||
values: T, |
||||
callback: (result: { -readonly [P in keyof T]: Awaited<T[P]> }) => R |
||||
): PromiseLike<R> | R { |
||||
if(values.some(value => value instanceof Promise)) { |
||||
return Promise.all(values).then(callback as any); |
||||
} else { |
||||
return callback(values as any); |
||||
} |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
export default function getTimeFormat(): 'h12' | 'h23' { |
||||
// try {
|
||||
// const resolvedOptions = Intl.DateTimeFormat(navigator.language, {hour: 'numeric'}).resolvedOptions();
|
||||
// if('hourCycle' in resolvedOptions) {
|
||||
// return (resolvedOptions as any).hourCycle === 'h12' ? 'h12' : 'h23';
|
||||
// } else {
|
||||
// return resolvedOptions.hour12 ? 'h12' : 'h23';
|
||||
// }
|
||||
// } catch(err) {
|
||||
return new Date().toLocaleString().match(/\s(AM|PM)/) ? 'h12' : 'h23'; |
||||
// }
|
||||
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,143 @@
@@ -0,0 +1,143 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb |
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
||||
*/ |
||||
|
||||
/* @keyframes reaction-activate { |
||||
0% { |
||||
opacity: 1; |
||||
transform: scale(1.75); |
||||
} |
||||
|
||||
95% { |
||||
opacity: 1; |
||||
} |
||||
|
||||
to { |
||||
opacity: 0; |
||||
transform: scale(1.25); |
||||
} |
||||
} */ |
||||
|
||||
.reaction { |
||||
display: flex; |
||||
align-items: center; |
||||
|
||||
&-sticker { |
||||
position: relative; |
||||
width: var(--reaction-size); |
||||
height: var(--reaction-size); |
||||
|
||||
&-activate { |
||||
--offset: -.4375rem; |
||||
// --offset: 0; |
||||
position: absolute; |
||||
top: var(--offset); |
||||
right: var(--offset); |
||||
bottom: var(--offset); |
||||
left: var(--offset); |
||||
// animation: reaction-activate 2s linear forwards; |
||||
|
||||
& + .media-sticker { |
||||
opacity: 0; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&-inline { |
||||
--reaction-size: .875rem; |
||||
min-width: var(--reaction-size); |
||||
min-height: var(--reaction-size); |
||||
} |
||||
|
||||
&-inline &-counter { |
||||
font-size: inherit !important; |
||||
order: -1; |
||||
margin-right: .0625rem !important; |
||||
} |
||||
|
||||
&-block { |
||||
--additional-height: .625rem; |
||||
--margin: .25rem; |
||||
// --reaction-size: 1.0625rem; |
||||
--reaction-size: 1rem; |
||||
--background-color: var(--message-highlightning-color); |
||||
--chosen-background-color: var(--message-primary-color); |
||||
--counter-color: #fff; |
||||
--reaction-total-size: calc(var(--reaction-size) + var(--additional-height)); |
||||
height: var(--reaction-total-size); |
||||
border-radius: var(--reaction-total-size); |
||||
// padding: 0 .375rem 0 .625rem; |
||||
padding: 0 .5rem; |
||||
background-color: var(--background-color); |
||||
cursor: pointer; |
||||
position: relative; |
||||
margin-top: var(--margin); |
||||
margin-right: var(--margin); |
||||
color: var(--counter-color); |
||||
|
||||
&:last-child { |
||||
margin-right: 0; |
||||
} |
||||
|
||||
&:before { |
||||
content: " "; |
||||
position: absolute; |
||||
top: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
left: 0; |
||||
background-color: var(--chosen-background-color); |
||||
// visibility: hidden; |
||||
border-radius: inherit; |
||||
// transform: scale3d(.2, .2, .2); |
||||
transform: scale3d(0, 0, 0); |
||||
opacity: 0; |
||||
} |
||||
|
||||
&.is-chosen { |
||||
&:not(.backwards) { |
||||
&:before { |
||||
transform: scale3d(1, 1, 1); |
||||
opacity: 1; |
||||
// visibility: visible; |
||||
} |
||||
|
||||
.stacked-avatars { |
||||
--border-color: var(--chosen-background-color); |
||||
} |
||||
} |
||||
|
||||
&.animating { |
||||
&:before { |
||||
transition: transform var(--transition-standard-in), opacity var(--transition-standard-in); |
||||
// transition: transform var(--transition-standard-in); |
||||
} |
||||
|
||||
.reaction-counter { |
||||
transition: color var(--transition-standard-in); |
||||
// transition: color 1s linear; |
||||
} |
||||
|
||||
.stacked-avatars-avatar-container { |
||||
transition: border-color var(--transition-standard-in); |
||||
} |
||||
} |
||||
} |
||||
|
||||
.stacked-avatars { |
||||
--border-color: transparent; |
||||
margin-left: .25rem; |
||||
// margin-right: .0625rem; |
||||
} |
||||
} |
||||
|
||||
&-block &-counter { |
||||
font-size: .875rem !important; |
||||
font-weight: 500; |
||||
margin: 0 .125rem 0 .375rem; |
||||
line-height: 1; |
||||
position: relative; |
||||
} |
||||
} |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb |
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
||||
*/ |
||||
|
||||
.reactions { |
||||
&-block { |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
user-select: none; |
||||
|
||||
&.has-no-reactions { |
||||
display: unset; |
||||
} |
||||
} |
||||
|
||||
&-inline { |
||||
display: inline-flex; |
||||
|
||||
&:not(:empty) { |
||||
margin-right: .125rem; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb |
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
||||
*/ |
||||
|
||||
.stacked-avatars { |
||||
--border-color: var(--surface-color); |
||||
--border-size: 1px; |
||||
--margin-right: -.3125rem; |
||||
--avatar-size: 1rem; |
||||
--avatar-total-size: calc(var(--avatar-size) + var(--border-size) * 2); |
||||
display: flex; |
||||
flex-direction: row-reverse; |
||||
|
||||
&-avatar { |
||||
width: var(--avatar-size); |
||||
height: var(--avatar-size); |
||||
z-index: 0; // * fix border blinking |
||||
|
||||
&-container { |
||||
width: var(--avatar-total-size); |
||||
height: var(--avatar-total-size); |
||||
border: var(--border-size) solid var(--border-color); |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
border-radius: 50%; |
||||
position: relative; |
||||
|
||||
&:not(:first-child) { |
||||
margin-right: var(--margin-right); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
/* |
||||
* https://github.com/morethanwords/tweb |
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko |
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
||||
*/ |
||||
|
||||
.popup-reacted-list { |
||||
$parent: ".popup"; |
||||
|
||||
#{$parent} { |
||||
&-container { |
||||
width: 25rem; |
||||
height: 600px; |
||||
max-height: 600px; |
||||
padding: 0; |
||||
} |
||||
|
||||
&-header { |
||||
min-height: 3.5625rem; |
||||
margin: 0; |
||||
padding: .25rem .75rem .75rem; |
||||
border-bottom: 1px solid var(--border-color); |
||||
} |
||||
|
||||
&-close { |
||||
margin-top: .375rem; |
||||
margin-right: .5rem; |
||||
height: 40px; |
||||
order: -1; |
||||
} |
||||
} |
||||
|
||||
.reaction { |
||||
--additional-height: .75rem; |
||||
--reaction-size: 1.5rem; |
||||
--margin: .5rem; |
||||
// --background-color: var(--secondary-color); |
||||
--background-color: var(--light-filled-primary-color); |
||||
--counter-color: var(--primary-color); |
||||
|
||||
&.is-chosen { |
||||
&:not(.backwards) { |
||||
// --counter-color: var(--primary-text-color); |
||||
--counter-color: #fff; |
||||
} |
||||
} |
||||
|
||||
&-sticker-icon { |
||||
font-size: 1.25rem !important; |
||||
margin: 0; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
} |
||||
|
||||
.sidebar-left-section { |
||||
margin-bottom: 0 !important; |
||||
} |
||||
|
||||
/* .gradient-delimiter { |
||||
flex: 0 0 auto; |
||||
} */ |
||||
|
||||
.tabs-container { |
||||
flex: 1 1 auto; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.tabs-tab { |
||||
background-color: var(--surface-color); |
||||
} |
||||
|
||||
.reacted-list-reaction-icon { |
||||
width: 1.5rem; |
||||
height: 1.5rem; |
||||
margin: 0; |
||||
top: 50%; |
||||
transform: translateY(-50%); |
||||
} |
||||
} |
Loading…
Reference in new issue