20 | {message.inlineButtons!.map((row) => (
21 |
22 | {row.map((button) => (
23 |
32 | ))}
33 |
34 | ))}
35 |
36 | );
37 | };
38 |
39 | export default InlineButtons;
40 |
--------------------------------------------------------------------------------
/src/components/middle/message/Invoice.scss:
--------------------------------------------------------------------------------
1 | .Invoice .title{color:var(--accent-color);font-weight:500}.Invoice .description{position:relative}.Invoice .description.has-image .invoice-image{max-width:100%;height:20rem}@media (max-width: 600px){.Invoice .description.has-image .invoice-image{height:10rem}}.Invoice .description.has-image .description-text{position:absolute;top:0;padding:.25rem .5rem;margin:.25rem;background-color:rgba(90,110,70,0.6);border-radius:var(--border-radius-messages-small);color:var(--color-text);font-weight:500}
2 |
--------------------------------------------------------------------------------
/src/components/middle/message/MessageContextMenu.scss:
--------------------------------------------------------------------------------
1 | .MessageContextMenu{position:absolute;font-size:1rem}.MessageContextMenu .bubble{transform:scale(0.5);transition:opacity 0.15s cubic-bezier(0.2, 0, 0.2, 1),transform 0.15s cubic-bezier(0.2, 0, 0.2, 1) !important}.MessageContextMenu .backdrop{position:absolute;touch-action:none}
2 |
--------------------------------------------------------------------------------
/src/components/middle/message/RoundVideo.scss:
--------------------------------------------------------------------------------
1 | .RoundVideo{position:relative;width:200px;height:200px;cursor:pointer}.RoundVideo .thumbnail-wrapper{width:200px;height:200px;border-radius:50%;overflow:hidden}.RoundVideo .video-wrapper{position:absolute;left:0;top:0;border-radius:50%;overflow:hidden}.RoundVideo .progress{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.RoundVideo .progress-circle{stroke:white;fill:transparent;stroke-width:4;stroke-opacity:.35;stroke-linecap:round}.RoundVideo video::-internal-media-controls-cast-button,.RoundVideo video::-webkit-media-controls,.RoundVideo video::-webkit-media-controls-start-playback-button{display:none}
2 |
--------------------------------------------------------------------------------
/src/components/middle/message/Sticker.scss:
--------------------------------------------------------------------------------
1 | .Sticker:not(.inactive){cursor:pointer}.Sticker.inactive{pointer-events:none}
2 |
--------------------------------------------------------------------------------
/src/components/middle/message/helpers/calculateAuthorWidth.ts:
--------------------------------------------------------------------------------
1 | import { IS_IOS } from '../../../../util/environment';
2 |
3 | let element: HTMLSpanElement | undefined;
4 |
5 | export default function calculateAuthorWidth(text: string) {
6 | if (!element) {
7 | element = document.createElement('span');
8 | // eslint-disable-next-line max-len
9 | element.style.font = IS_IOS
10 | // eslint-disable-next-line max-len
11 | ? '400 12px system-ui, -apple-system, BlinkMacSystemFont, "Roboto", "Apple Color Emoji", "Helvetica Neue", sans-serif'
12 | : '400 12px "Roboto", -apple-system, "Apple Color Emoji", BlinkMacSystemFont, "Helvetica Neue", sans-serif';
13 | element.style.whiteSpace = 'nowrap';
14 | element.style.position = 'absolute';
15 | element.style.left = '-999px';
16 | element.style.opacity = '.01';
17 | document.body.appendChild(element);
18 | }
19 |
20 | element.innerHTML = text;
21 |
22 | return element.offsetWidth;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/middle/message/helpers/getCustomAppendixBg.ts:
--------------------------------------------------------------------------------
1 | const SELECTED_APPENDIX_BACKGROUND = Promise.resolve('rgba(255,255,255,1)');
2 |
3 | export default function getCustomAppendixBg(src: string, isOwn: boolean, inSelectMode?: boolean, isSelected?: boolean) {
4 | return isSelected ? SELECTED_APPENDIX_BACKGROUND : getAppendixColorFromImage(src, isOwn);
5 | }
6 |
7 | async function getAppendixColorFromImage(src: string, isOwn: boolean) {
8 | const img = new Image();
9 | img.src = src;
10 |
11 | if (!img.width) {
12 | await new Promise((resolve) => {
13 | img.onload = resolve;
14 | });
15 | }
16 |
17 | const canvas = document.createElement('canvas');
18 | const ctx = canvas.getContext('2d')!;
19 |
20 | canvas.width = img.width;
21 | canvas.height = img.height;
22 |
23 | ctx.drawImage(img, 0, 0, img.width, img.height);
24 |
25 | const x = isOwn ? img.width - 1 : 0;
26 | const y = img.height - 1;
27 |
28 | const pixel = Array.from(ctx.getImageData(x, y, 1, 1).data);
29 | return `rgba(${pixel.join(',')})`;
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/middle/message/hooks/useBlurredMediaThumbRef.ts:
--------------------------------------------------------------------------------
1 | import { ApiMessage } from '../../../../api/types';
2 |
3 | import { IS_CANVAS_FILTER_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT } from '../../../../util/environment';
4 | import { getMessageMediaThumbDataUri } from '../../../../modules/helpers';
5 | import useCanvasBlur from '../../../../hooks/useCanvasBlur';
6 |
7 | export default function useBlurredMediaThumbRef(message: ApiMessage, fullMediaData?: string) {
8 | return useCanvasBlur(
9 | getMessageMediaThumbDataUri(message),
10 | Boolean(fullMediaData),
11 | IS_SINGLE_COLUMN_LAYOUT && !IS_CANVAS_FILTER_SUPPORTED,
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/middle/message/hooks/useFocusMessage.ts:
--------------------------------------------------------------------------------
1 | import { FocusDirection } from '../../../../types';
2 |
3 | import { useLayoutEffect } from '../../../../lib/teact/teact';
4 | import fastSmoothScroll from '../../../../util/fastSmoothScroll';
5 |
6 | // This is used when the viewport was replaced.
7 | const RELOCATED_FOCUS_OFFSET = 1000;
8 | const FOCUS_MARGIN = 20;
9 |
10 | export default function useFocusMessage(
11 | elementRef: { current: HTMLDivElement | null },
12 | chatId: number,
13 | isFocused?: boolean,
14 | focusDirection?: FocusDirection,
15 | noFocusHighlight?: boolean,
16 | ) {
17 | useLayoutEffect(() => {
18 | if (isFocused && elementRef.current) {
19 | const messagesContainer = elementRef.current.closest