Albums almost done

This commit is contained in:
morethanwords 2020-04-29 22:35:27 +03:00
parent c35fb83125
commit 901f8cf99b
6 changed files with 602 additions and 21 deletions

View File

@ -0,0 +1,442 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
type Size = {w: number, h: number};
export type GroupMediaLayout = {
geometry: {
x: number,
y: number,
width: number,
height: number
},
sides: number
};
type Attempt = {
lineCounts: number[],
heights: number[]
};
export const RectPart = {
None: 0,
Top: 1,
Right: 2,
Bottom: 4,
Left: 8
};
let accumulate = (arr: number[], initialValue: number) => arr.reduce((acc, value) => acc + value, initialValue);
// https://github.com/telegramdesktop/tdesktop/blob/74d848311b31ef0eb6d2c43a4d30ade8f1d2d9fb/Telegram/SourceFiles/core/utils.h#L128
function snap<T>(v: T, _min: T, _max: T): T {
return (v < _min) ? _min : ((v > _max) ? _max : v);
}
// https://github.com/telegramdesktop/tdesktop/blob/4669c07dc5335cbf4795bbbe5b0ab7c007b9aee2/Telegram/SourceFiles/ui/grouped_layout.cpp
export class Layouter {
private count: number;
private ratios: number[];
private proportions: string;
private averageRatio: number;
private maxSizeRatio: number;
constructor(private sizes: Size[], private maxWidth: number, private minWidth: number, private spacing: number, private maxHeight = maxWidth) {
this.count = sizes.length;
this.ratios = Layouter.countRatios(sizes);
this.proportions = Layouter.countProportions(this.ratios);
this.averageRatio = accumulate(this.ratios, 1) / this.count; // warn
this.maxSizeRatio = maxWidth / this.maxHeight;
}
public layout(): GroupMediaLayout[] {
if(!this.count) return [];
//else if(this.count == 1) return this.layoutOne();
if(this.count >= 5 || this.ratios.find(r => r > 2)) {
return new ComplexLayouter(this.ratios, this.averageRatio, this.maxWidth, this.minWidth, this.spacing).layout();
}
if(this.count == 2) return this.layoutTwo();
else if(this.count == 3) return this.layoutThree();
return this.layoutFour();
}
private layoutTwo(): ReturnType<Layouter['layout']> {
if((this.proportions == "ww")
&& (this.averageRatio > 1.4 * this.maxSizeRatio)
&& (this.ratios[1] - this.ratios[0] < 0.2)) {
return this.layoutTwoTopBottom();
} else if(this.proportions == "ww" || this.proportions == "qq") {
return this.layoutTwoLeftRightEqual();
}
return this.layoutTwoLeftRight();
}
private layoutThree(): ReturnType<Layouter['layout']> {
console.log('layoutThree:', this);
if(this.proportions[0] == 'n') {
return this.layoutThreeLeftAndOther();
}
return this.layoutThreeTopAndOther();
}
private layoutFour(): ReturnType<Layouter['layout']> {
if(this.proportions[0] == 'w') {
return this.layoutFourTopAndOther();
}
return this.layoutFourLeftAndOther();
}
private layoutTwoTopBottom(): ReturnType<Layouter['layout']> {
const width = this.maxWidth;
const height = Math.round(Math.min(
width / this.ratios[0],
Math.min(
width / this.ratios[1],
(this.maxHeight - this.spacing) / 2)));
return [
{
geometry: {x: 0, y: 0, width, height},
sides: RectPart.Left | RectPart.Top | RectPart.Right
},
{
geometry: {x: 0, y: height + this.spacing, width, height},
sides: RectPart.Left | RectPart.Bottom | RectPart.Right
},
];
}
private layoutTwoLeftRightEqual(): ReturnType<Layouter['layout']> {
const width = (this.maxWidth - this.spacing) / 2;
const height = Math.round(Math.min(
width / this.ratios[0],
Math.min(width / this.ratios[1], this.maxHeight * 1)));
return [
{
geometry: {x: 0, y: 0, width, height},
sides: RectPart.Top | RectPart.Left | RectPart.Bottom
},
{
geometry: {x: width + this.spacing, y: 0, width, height},
sides: RectPart.Top | RectPart.Right | RectPart.Bottom
},
];
}
private layoutTwoLeftRight(): ReturnType<Layouter['layout']> {
const minimalWidth = Math.round(this.minWidth * 1.5);
const secondWidth = Math.min(
Math.round(Math.max(
0.4 * (this.maxWidth - this.spacing),
(this.maxWidth - this.spacing) / this.ratios[0]
/ (1 / this.ratios[0] + 1 / this.ratios[1]))),
this.maxWidth - this.spacing - minimalWidth);
const firstWidth = this.maxWidth
- secondWidth
- this.spacing;
const height = Math.min(
this.maxHeight,
Math.round(Math.min(
firstWidth / this.ratios[0],
secondWidth / this.ratios[1])));
return [
{
geometry: {x: 0, y: 0, width: firstWidth, height},
sides: RectPart.Top | RectPart.Left | RectPart.Bottom
},
{
geometry: {x: firstWidth + this.spacing, y: 0, width: secondWidth, height},
sides: RectPart.Top | RectPart.Right | RectPart.Bottom
},
];
}
private layoutThreeLeftAndOther(): ReturnType<Layouter['layout']> {
const firstHeight = this.maxHeight;
const thirdHeight = Math.round(Math.min(
(this.maxHeight - this.spacing) / 2.,
(this.ratios[1] * (this.maxWidth - this.spacing)
/ (this.ratios[2] + this.ratios[1]))));
const secondHeight = firstHeight
- thirdHeight
- this.spacing;
const rightWidth = Math.max(
this.minWidth,
Math.round(Math.min(
(this.maxWidth - this.spacing) / 2.,
Math.min(
thirdHeight * this.ratios[2],
secondHeight * this.ratios[1]))));
const leftWidth = Math.min(
Math.round(firstHeight * this.ratios[0]),
this.maxWidth - this.spacing - rightWidth);
return [
{
geometry: {x: 0, y: 0, width: leftWidth, height: firstHeight},
sides: RectPart.Top | RectPart.Left | RectPart.Bottom
},
{
geometry: {x: leftWidth + this.spacing, y: 0, width: rightWidth, height: secondHeight},
sides: RectPart.Top | RectPart.Right
},
{
geometry: {x: leftWidth + this.spacing, y: secondHeight + this.spacing, width: rightWidth, height: thirdHeight},
sides: RectPart.Bottom | RectPart.Right
},
];
}
private layoutThreeTopAndOther(): ReturnType<Layouter['layout']> {
const firstWidth = this.maxWidth;
const firstHeight = Math.round(Math.min(
firstWidth / this.ratios[0],
(this.maxHeight - this.spacing) * 0.66));
const secondWidth = (this.maxWidth - this.spacing) / 2;
const secondHeight = Math.min(
this.maxHeight - firstHeight - this.spacing,
Math.round(Math.min(
secondWidth / this.ratios[1],
secondWidth / this.ratios[2])));
const thirdWidth = firstWidth - secondWidth - this.spacing;
return [
{
geometry: {x: 0, y: 0, width: firstWidth, height: firstHeight},
sides: RectPart.Left | RectPart.Top | RectPart.Right
},
{
geometry: {x: 0, y: firstHeight + this.spacing, width: secondWidth, height: secondHeight},
sides: RectPart.Bottom | RectPart.Left
},
{
geometry: {x: secondWidth + this.spacing, y: firstHeight + this.spacing, width: thirdWidth, height: secondHeight},
sides: RectPart.Bottom | RectPart.Right
},
];
}
private layoutFourTopAndOther(): ReturnType<Layouter['layout']> {
const w = this.maxWidth;
const h0 = Math.round(Math.min(
w / this.ratios[0],
(this.maxHeight - this.spacing) * 0.66));
const h = Math.round(
(this.maxWidth - 2 * this.spacing)
/ (this.ratios[1] + this.ratios[2] + this.ratios[3]));
const w0 = Math.max(
this.minWidth,
Math.round(Math.min(
(this.maxWidth - 2 * this.spacing) * 0.4,
h * this.ratios[1])));
const w2 = Math.round(Math.max(
Math.max(
this.minWidth * 1.,
(this.maxWidth - 2 * this.spacing) * 0.33),
h * this.ratios[3]));
const w1 = w - w0 - w2 - 2 * this.spacing;
const h1 = Math.min(
this.maxHeight - h0 - this.spacing,
h);
return [
{
geometry: {x: 0, y: 0, width: w, height: h0},
sides: RectPart.Left | RectPart.Top | RectPart.Right
},
{
geometry: {x: 0, y: h0 + this.spacing, width: w0, height: h1},
sides: RectPart.Bottom | RectPart.Left
},
{
geometry: {x: w0 + this.spacing, y: h0 + this.spacing, width: w1, height: h1},
sides: RectPart.Bottom,
},
{
geometry: {x: w0 + this.spacing + w1 + this.spacing, y: h0 + this.spacing, width: w2, height: h1},
sides: RectPart.Right | RectPart.Bottom
},
];
}
private layoutFourLeftAndOther(): ReturnType<Layouter['layout']> {
const h = this.maxHeight;
const w0 = Math.round(Math.min(
h * this.ratios[0],
(this.maxWidth - this.spacing) * 0.6));
const w = Math.round(
(this.maxHeight - 2 * this.spacing)
/ (1. / this.ratios[1] + 1. / this.ratios[2] + 1. / this.ratios[3])
);
const h0 = Math.round(w / this.ratios[1]);
const h1 = Math.round(w / this.ratios[2]);
const h2 = h - h0 - h1 - 2 * this.spacing;
const w1 = Math.max(
this.minWidth,
Math.min(this.maxWidth - w0 - this.spacing, w));
return [
{
geometry: {x: 0, y: 0, width: w0, height: h},
sides: RectPart.Top | RectPart.Left | RectPart.Bottom
},
{
geometry: {x: w0 + this.spacing, y: 0, width: w1, height: h0},
sides: RectPart.Top | RectPart.Right
},
{
geometry: {x: w0 + this.spacing, y: h0 + this.spacing, width: w1, height: h1},
sides: RectPart.Right
},
{
geometry: {x: w0 + this.spacing, y: h0 + h1 + 2 * this.spacing, width: w1, height: h2},
sides: RectPart.Bottom | RectPart.Right
},
];
}
private static countRatios(sizes: Size[]) {
return sizes.map(size => size.w / size.h);
}
private static countProportions(ratios: number[]) {
return ratios.map(ratio => (ratio > 1.2) ? 'w' : (ratio < 0.8) ? 'n' : 'q').join('');
}
}
class ComplexLayouter {
private ratios: number[];
private count: number;
constructor(ratios: number[], private averageRatio: number, private maxWidth: number, private minWidth: number, private spacing: number, private maxHeight = maxWidth * 4 / 3) {
this.ratios = ComplexLayouter.cropRatios(ratios, averageRatio);
this.count = ratios.length;
}
private static cropRatios(ratios: number[], averageRatio: number) {
const kMaxRatio = 2.75;
const kMinRatio = 0.6667;
return ratios.map(ratio => {
return averageRatio > 1.1
? snap(ratio, 1., kMaxRatio)
: snap(ratio, kMinRatio, 1.);
});
}
public layout(): GroupMediaLayout[] {
let result = new Array<GroupMediaLayout>(this.count);
let attempts: Attempt[] = [];
const multiHeight = (offset: number, count: number) => {
const ratios = this.ratios.slice(offset, offset + count); // warn
const sum = accumulate(ratios, 0);
return (this.maxWidth - (count - 1) * this.spacing) / sum;
};
const pushAttempt = (lineCounts: number[]) => {
let heights: number[] = [];
let offset = 0;
for(let count of lineCounts) {
heights.push(multiHeight(offset, count));
offset += count;
}
attempts.push({lineCounts, heights}); // warn
};
for(let first = 1; first != this.count; ++first) {
const second = this.count - first;
if(first > 3 || second > 3) {
continue;
}
pushAttempt([first, second]);
}
for(let first = 1; first != this.count - 1; ++first) {
for(let second = 1; second != this.count - first; ++second) {
const third = this.count - first - second;
if((first > 3)
|| (second > ((this.averageRatio < 0.85) ? 4 : 3))
|| (third > 3)) {
continue;
}
pushAttempt([first, second, third]);
}
}
for(let first = 1; first != this.count - 1; ++first) {
for(let second = 1; second != this.count - first; ++second) {
for(let third = 1; third != this.count - first - second; ++third) {
const fourth = this.count - first - second - third;
if(first > 3 || second > 3 || third > 3 || fourth > 3) {
continue;
}
pushAttempt([first, second, third, fourth]);
}
}
}
let optimalAttempt: Attempt = null;
let optimalDiff = 0;
for(const attempt of attempts) {
const {heights, lineCounts: counts} = attempt;
const lineCount = counts.length;
const totalHeight = accumulate(heights, 0)
+ this.spacing * (lineCount - 1);
const minLineHeight = Math.min(...heights);
const maxLineHeight = Math.max(...heights);
const bad1 = (minLineHeight < this.minWidth) ? 1.5 : 1;
const bad2 = (() => {
for(let line = 1; line != lineCount; ++line) {
if(counts[line - 1] > counts[line]) {
return 1.5;
}
}
return 1.;
})();
const diff = Math.abs(totalHeight - this.maxHeight) * bad1 * bad2;
if(!optimalAttempt || diff < optimalDiff) {
optimalAttempt = attempt;
optimalDiff = diff;
}
}
const optimalCounts = optimalAttempt.lineCounts;
const optimalHeights = optimalAttempt.heights;
const rowCount = optimalCounts.length;
let index = 0;
let y = 0;
for(let row = 0; row != rowCount; ++row) {
const colCount = optimalCounts[row];
const lineHeight = optimalHeights[row];
const height = Math.round(lineHeight);
let x = 0;
for(let col = 0; col != colCount; ++col) {
const sides = RectPart.None
| (row == 0 ? RectPart.Top : RectPart.None)
| (row == rowCount - 1 ? RectPart.Bottom : RectPart.None)
| (col == 0 ? RectPart.Left : RectPart.None)
| (col == colCount - 1 ? RectPart.Right : RectPart.None);
const ratio = this.ratios[index];
const width = (col == colCount - 1)
? (this.maxWidth - x)
: Math.round(ratio * lineHeight);
result[index] = {
geometry: {x, y, width, height},
sides
};
x += width + this.spacing;
++index;
}
y += height + this.spacing;
}
return result;
}
}

View File

@ -17,14 +17,15 @@ import appSidebarLeft from "./appSidebarLeft";
import appChatsManager from "./appChatsManager";
import appMessagesIDsManager from "./appMessagesIDsManager";
import apiUpdatesManager from './apiUpdatesManager';
import { wrapDocument, wrapPhoto, wrapVideo, wrapSticker, wrapReply } from '../../components/wrappers';
import { wrapDocument, wrapPhoto, wrapVideo, wrapSticker, wrapReply, MTPhotoSize } from '../../components/wrappers';
import ProgressivePreloader from '../../components/preloader';
import { openBtnMenu } from '../../components/misc';
import { openBtnMenu, renderImageFromUrl } from '../../components/misc';
import { ChatInput } from '../../components/chatInput';
import Scrollable from '../../components/scrollable';
import BubbleGroups from '../../components/bubbleGroups';
import LazyLoadQueue from '../../components/lazyLoadQueue';
import appDocsManager from './appDocsManager';
import { Layouter, RectPart } from '../../components/groupedLayout';
console.log('appImManager included!');
@ -1195,6 +1196,13 @@ export class AppImManager {
public renderMessage(message: any, reverse = false, multipleRender = false, bubble: HTMLDivElement = null, updatePosition = true) {
this.log('message to render:', message);
if(message.deleted) return;
else if(message.grouped_id) { // will render only last album's message
let storage = appMessagesManager.groupedMessagesStorage[message.grouped_id];
let maxID = Math.max(...Object.keys(storage).map(i => +i));
if(message.mid < maxID) {
return;
}
}
let peerID = this.peerID;
let our = message.fromID == this.myID;
@ -1409,9 +1417,110 @@ export class AppImManager {
bubble.classList.add('hide-name', 'photo');
wrapPhoto(photo.id, message, attachmentDiv, undefined, undefined, true, our, this.lazyLoadQueue, () => {
return this.peerID == peerID;
});
if(message.grouped_id) {
bubble.classList.add('is-album');
let items: {size: MTPhotoSize, media: any}[] = [];
// higher msgID will be the last in album
let storage = appMessagesManager.groupedMessagesStorage[message.grouped_id];
for(let mid in storage) {
let m = appMessagesManager.getMessage(+mid);
let media = m.media.photo || m.media.document;
let size = appPhotosManager.choosePhotoSize(media, 380, 380);
items.push({size, media});
}
let spacing = 2;
let layouter = new Layouter(items.map(i => ({w: i.size.w, h: i.size.h})), 451, 100, spacing);
let layout = layouter.layout();
this.log('layout:', layout);
/* let borderRadius = window.getComputedStyle(realParent).getPropertyValue('border-radius');
let brSplitted = fillPropertyValue(borderRadius); */
for(let {geometry, sides} of layout) {
let {size, media} = items.shift();
let div = document.createElement('div');
div.classList.add('album-item');
div.style.width = geometry.width + 'px';
div.style.height = geometry.height + 'px';
div.style.top = geometry.y + 'px';
div.style.left = geometry.x + 'px';
if(sides & RectPart.Right) {
attachmentDiv.style.width = geometry.width + geometry.x + 'px';
}
if(sides & RectPart.Bottom) {
attachmentDiv.style.height = geometry.height + geometry.y + 'px';
}
if(sides & RectPart.Left && sides & RectPart.Top) {
div.style.borderTopLeftRadius = 'inherit';
}
if(sides & RectPart.Left && sides & RectPart.Bottom) {
div.style.borderBottomLeftRadius = 'inherit';
}
if(sides & RectPart.Right && sides & RectPart.Top) {
div.style.borderTopRightRadius = 'inherit';
}
if(sides & RectPart.Right && sides & RectPart.Bottom) {
div.style.borderBottomRightRadius = 'inherit';
}
/* if(geometry.y != 0) {
div.style.marginTop = spacing + 'px';
}
if(geometry.x != 0) {
div.style.marginLeft = spacing + 'px';
} */
let preloader = new ProgressivePreloader(div);
let load = () => appPhotosManager.preloadPhoto(media._ == 'photo' ? media.id : media, size)
.then((blob) => {
if(this.peerID != peerID) {
this.log.warn('peer changed');
return;
}
preloader.detach();
if(media && media.url) {
renderImageFromUrl(div, media.url);
}/* else {
let url = URL.createObjectURL(blob);
this.urlsToRevoke.push(url);
let img = new Image();
img.src = url;
img.onload = () => {
div.style.backgroundImage = 'url(' + url + ')';
};
} */
//div.style.backgroundImage = 'url(' + url + ')';
});
load();
// @ts-ignore
//div.style.backgroundColor = '#' + Math.floor(Math.random() * (2 ** 24 - 1)).toString(16).padStart(6, '0');
attachmentDiv.append(div);
}
} else {
wrapPhoto(photo.id, message, attachmentDiv, undefined, undefined, true, our, this.lazyLoadQueue, () => {
return this.peerID == peerID;
});
}
break;
}

View File

@ -4,7 +4,7 @@ import appMessagesManager from "./appMessagesManager";
import { RichTextProcessor } from "../richtextprocessor";
import { logger } from "../polyfill";
import ProgressivePreloader from "../../components/preloader";
import { findUpClassName, $rootScope, generatePathData } from "../utils";
import { findUpClassName, $rootScope, generatePathData, fillPropertyValue } from "../utils";
import appDocsManager from "./appDocsManager";
import VideoPlayer from "../mediaPlayer";
import { renderImageFromUrl } from "../../components/misc";
@ -213,13 +213,7 @@ export class AppMediaViewer {
}
let borderRadius = window.getComputedStyle(realParent).getPropertyValue('border-radius');
let brSplitted = borderRadius.split(' ');
if(brSplitted.length != 4) {
if(!brSplitted[0]) brSplitted[0] = '0px';
for(let i = brSplitted.length; i < 4; ++i) {
brSplitted[i] = brSplitted[i % 2] || brSplitted[0] || '0px';
}
}
let brSplitted = fillPropertyValue(borderRadius) as string[];
borderRadius = brSplitted.map(r => (parseInt(r) / scaleX) + 'px').join(' ');
if(!wasActive) {
mover.style.borderRadius = borderRadius;

View File

@ -632,6 +632,18 @@ export function encodeEntities (value) {
}).replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
export function fillPropertyValue(str) {
let splitted = str.split(' ');
if(splitted.length != 4) {
if(!splitted[0]) splitted[0] = '0px';
for(let i = splitted.length; i < 4; ++i) {
splitted[i] = splitted[i % 2] || splitted[0] || '0px';
}
}
return splitted;
}
export function calcImageInBox (imageW, imageH, boxW, boxH, noZooom) {
if(imageW < boxW && imageH < boxH) {
return {w: imageW, h: imageH};

View File

@ -555,6 +555,23 @@ $chat-max-width: 696px;
}
}
&.is-album {
.attachment {
max-width: 451px;
max-height: none;
> div {
background-color: #000;
background-size: cover;
background-position: center center;
/* flex: 1 0 auto; */
max-width: 100%;
cursor: pointer;
position: absolute;
}
}
}
//&.video {
//.attachment {
//max-height: fit-content;

View File

@ -59,15 +59,16 @@
}
li {
padding: 2px 0;
//padding: 0 0 2px 0;
padding-bottom: 4px;
//overflow: hidden;
background-color: #fff;
}
li > .rp {
height: 70px;
max-height: 70px;
border-radius: $border-radius;
height: 72px;
max-height: 72px;
border-radius: $border-radius-medium;
//align-items: center;
/* display: grid;
grid-template-columns: 64px calc(100% - 64px - 6.5px); */
@ -76,8 +77,8 @@
flex-direction: row;
position: relative;
cursor: pointer;
padding: 7px 8.5px;
margin: 0px 8px 2px 7px;
padding: 9px 8.5px;
margin: 0px 8px 0px 7px;
overflow: hidden;
&:hover {
@ -91,7 +92,7 @@
.pinned-delimiter {
display: flex;
padding: 6px 0 6px;
padding: 8px 0 4px;
span {
margin: 0;
@ -271,6 +272,12 @@
&-contacts {
padding: 16px 0 7px;
li {
//margin-bottom: 2px;
padding-bottom: 4px;
padding-top: 2px;
}
li > .rp {
padding: 9px 11.5px !important;
height: 66px;