Message: Introduce Geo Locations (#1716)
This commit is contained in:
parent
39ae9189d2
commit
531c2de36c
@ -28,6 +28,7 @@ import {
|
|||||||
ApiAvailableReaction,
|
ApiAvailableReaction,
|
||||||
ApiSponsoredMessage,
|
ApiSponsoredMessage,
|
||||||
ApiUser,
|
ApiUser,
|
||||||
|
ApiLocation,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -173,7 +174,8 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
|
|||||||
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
|
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
|
||||||
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
|
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
|
||||||
...(forwardInfo && { forwardInfo }),
|
...(forwardInfo && { forwardInfo }),
|
||||||
...(isEdited && { isEdited, editDate: mtpMessage.editDate }),
|
...(isEdited && { isEdited }),
|
||||||
|
...(mtpMessage.editDate && { editDate: mtpMessage.editDate }),
|
||||||
...(isMediaUnread && { isMediaUnread }),
|
...(isMediaUnread && { isMediaUnread }),
|
||||||
...(mtpMessage.mentioned && isMediaUnread && { hasUnreadMention: true }),
|
...(mtpMessage.mentioned && isMediaUnread && { hasUnreadMention: true }),
|
||||||
...(mtpMessage.mentioned && { isMentioned: true }),
|
...(mtpMessage.mentioned && { isMentioned: true }),
|
||||||
@ -321,6 +323,9 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMes
|
|||||||
const invoice = buildInvoiceFromMedia(media);
|
const invoice = buildInvoiceFromMedia(media);
|
||||||
if (invoice) return { invoice };
|
if (invoice) return { invoice };
|
||||||
|
|
||||||
|
const location = buildLocationFromMedia(media);
|
||||||
|
if (location) return { location };
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -571,6 +576,63 @@ function buildInvoiceFromMedia(media: GramJs.TypeMessageMedia): ApiInvoice | und
|
|||||||
return buildInvoice(media);
|
return buildInvoice(media);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildLocationFromMedia(media: GramJs.TypeMessageMedia): ApiLocation | undefined {
|
||||||
|
if (media instanceof GramJs.MessageMediaGeo) {
|
||||||
|
return buildGeo(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media instanceof GramJs.MessageMediaVenue) {
|
||||||
|
return buildVenue(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media instanceof GramJs.MessageMediaGeoLive) {
|
||||||
|
return buildGeoLive(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGeo(media: GramJs.MessageMediaGeo): ApiLocation | undefined {
|
||||||
|
const point = buildGeoPoint(media.geo);
|
||||||
|
return point && { type: 'geo', geo: point };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVenue(media: GramJs.MessageMediaVenue): ApiLocation | undefined {
|
||||||
|
const { geo, title, provider, address, venueId, venueType } = media;
|
||||||
|
const point = buildGeoPoint(geo);
|
||||||
|
return point && {
|
||||||
|
type: 'venue',
|
||||||
|
geo: point,
|
||||||
|
title,
|
||||||
|
provider,
|
||||||
|
address,
|
||||||
|
venueId,
|
||||||
|
venueType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGeoLive(media: GramJs.MessageMediaGeoLive): ApiLocation | undefined {
|
||||||
|
const { geo, period, heading } = media;
|
||||||
|
const point = buildGeoPoint(geo);
|
||||||
|
return point && {
|
||||||
|
type: 'geoLive',
|
||||||
|
geo: point,
|
||||||
|
period,
|
||||||
|
heading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGeoPoint(geo: GramJs.TypeGeoPoint): ApiLocation['geo'] | undefined {
|
||||||
|
if (geo instanceof GramJs.GeoPointEmpty) return undefined;
|
||||||
|
const { long, lat, accuracyRadius, accessHash } = geo;
|
||||||
|
return {
|
||||||
|
long,
|
||||||
|
lat,
|
||||||
|
accessHash: accessHash.toString(),
|
||||||
|
accuracyRadius,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll {
|
export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll {
|
||||||
const { id, answers: rawAnswers } = poll;
|
const { id, answers: rawAnswers } = poll;
|
||||||
const answers = rawAnswers.map((answer) => ({
|
const answers = rawAnswers.map((answer) => ({
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import * as cacheApi from '../../../util/cacheApi';
|
|||||||
type EntityType = (
|
type EntityType = (
|
||||||
'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' |
|
'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' |
|
||||||
'document'
|
'document'
|
||||||
);
|
);
|
||||||
|
|
||||||
const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document']);
|
const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document']);
|
||||||
const TGS_MIME_TYPE = 'application/x-tgsticker';
|
const TGS_MIME_TYPE = 'application/x-tgsticker';
|
||||||
@ -74,11 +74,13 @@ async function download(
|
|||||||
mediaFormat?: ApiMediaFormat,
|
mediaFormat?: ApiMediaFormat,
|
||||||
isHtmlAllowed?: boolean,
|
isHtmlAllowed?: boolean,
|
||||||
) {
|
) {
|
||||||
const mediaMatch = url.startsWith('webDocument')
|
const mediaMatch = url.startsWith('staticMap')
|
||||||
? url.match(/(webDocument):(.+)/)
|
? url.match(/(staticMap):([0-9-]+)(\?.+)/)
|
||||||
: url.match(
|
: url.startsWith('webDocument')
|
||||||
/(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file|document)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/,
|
? url.match(/(webDocument):(.+)/)
|
||||||
);
|
: url.match(
|
||||||
|
/(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file|document)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/,
|
||||||
|
);
|
||||||
if (!mediaMatch) {
|
if (!mediaMatch) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -102,6 +104,25 @@ async function download(
|
|||||||
GramJs.Document | GramJs.StickerSet | GramJs.TypeWebDocument | undefined
|
GramJs.Document | GramJs.StickerSet | GramJs.TypeWebDocument | undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (mediaMatch[1] === 'staticMap') {
|
||||||
|
const accessHash = mediaMatch[2];
|
||||||
|
const params = mediaMatch[3];
|
||||||
|
const parsedParams = new URLSearchParams(params);
|
||||||
|
const long = parsedParams.get('long');
|
||||||
|
const lat = parsedParams.get('lat');
|
||||||
|
const w = parsedParams.get('w');
|
||||||
|
const h = parsedParams.get('h');
|
||||||
|
const zoom = parsedParams.get('zoom');
|
||||||
|
const scale = parsedParams.get('scale');
|
||||||
|
const accuracyRadius = parsedParams.get('accuracy_radius');
|
||||||
|
|
||||||
|
const data = await client.downloadStaticMap(accessHash, long, lat, w, h, zoom, scale, accuracyRadius);
|
||||||
|
return {
|
||||||
|
mimeType: 'image/png',
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaMatch[1] === 'avatar' || mediaMatch[1] === 'profile') {
|
if (mediaMatch[1] === 'avatar' || mediaMatch[1] === 'profile') {
|
||||||
entityType = getEntityTypeById(entityId);
|
entityType = getEntityTypeById(entityId);
|
||||||
} else {
|
} else {
|
||||||
@ -204,6 +225,12 @@ function getMessageMediaMimeType(message: GramJs.Message, sizeType?: string) {
|
|||||||
return 'image/jpeg';
|
return 'image/jpeg';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.media instanceof GramJs.MessageMediaGeo
|
||||||
|
|| message.media instanceof GramJs.MessageMediaVenue
|
||||||
|
|| message.media instanceof GramJs.MessageMediaGeoLive) {
|
||||||
|
return 'image/png';
|
||||||
|
}
|
||||||
|
|
||||||
if (message.media instanceof GramJs.MessageMediaDocument && message.media.document instanceof GramJs.Document) {
|
if (message.media instanceof GramJs.MessageMediaDocument && message.media.document instanceof GramJs.Document) {
|
||||||
if (sizeType) {
|
if (sizeType) {
|
||||||
return message.media.document!.attributes.some((a) => a instanceof GramJs.DocumentAttributeSticker)
|
return message.media.document!.attributes.some((a) => a instanceof GramJs.DocumentAttributeSticker)
|
||||||
|
|||||||
@ -143,6 +143,37 @@ export interface ApiInvoice {
|
|||||||
isTest?: boolean;
|
isTest?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiGeoPoint {
|
||||||
|
long: number;
|
||||||
|
lat: number;
|
||||||
|
accessHash: string;
|
||||||
|
accuracyRadius?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiGeo {
|
||||||
|
type: 'geo';
|
||||||
|
geo: ApiGeoPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiVenue {
|
||||||
|
type: 'venue';
|
||||||
|
geo: ApiGeoPoint;
|
||||||
|
title: string;
|
||||||
|
address: string;
|
||||||
|
provider: string;
|
||||||
|
venueId: string;
|
||||||
|
venueType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiGeoLive {
|
||||||
|
type: 'geoLive';
|
||||||
|
geo: ApiGeoPoint;
|
||||||
|
heading?: number;
|
||||||
|
period: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiLocation = ApiGeo | ApiVenue | ApiGeoLive;
|
||||||
|
|
||||||
export type ApiNewPoll = {
|
export type ApiNewPoll = {
|
||||||
summary: ApiPoll['summary'];
|
summary: ApiPoll['summary'];
|
||||||
quiz?: {
|
quiz?: {
|
||||||
@ -240,6 +271,7 @@ export interface ApiMessage {
|
|||||||
audio?: ApiAudio;
|
audio?: ApiAudio;
|
||||||
voice?: ApiVoice;
|
voice?: ApiVoice;
|
||||||
invoice?: ApiInvoice;
|
invoice?: ApiInvoice;
|
||||||
|
location?: ApiLocation;
|
||||||
};
|
};
|
||||||
date: number;
|
date: number;
|
||||||
isOutgoing: boolean;
|
isOutgoing: boolean;
|
||||||
|
|||||||
1
src/assets/map-pin.svg
Normal file
1
src/assets/map-pin.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="21.333" height="37.218" viewBox="0 0 20 34.892"><g transform="translate(-965.773 -331.784) scale(1.18559)"><path d="M817.112 282.971c-1.258 1.343-2.046 3.299-2.015 5.139.064 3.845 1.797 5.3 4.568 10.592.999 2.328 2.04 4.792 3.031 8.873.138.602.272 1.16.335 1.21.062.048.196-.513.334-1.115.99-4.081 2.033-6.543 3.031-8.871 2.771-5.292 4.504-6.748 4.568-10.592.031-1.84-.759-3.798-2.017-5.14-1.437-1.535-3.605-2.67-5.916-2.717-2.312-.048-4.481 1.087-5.919 2.621z" style="display:inline;opacity:1;fill:#ff4646;fill-opacity:1;stroke:#d73534;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/><circle r="3.035" cy="288.253" cx="823.031" style="display:inline;opacity:1;fill:#590000;fill-opacity:1;stroke-width:0"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 791 B |
196
src/components/middle/message/Location.scss
Normal file
196
src/components/middle/message/Location.scss
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
.Location {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
|
||||||
|
.location-accuracy {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
@keyframes accuracy-wave {
|
||||||
|
0% {
|
||||||
|
transform: translate(-50%, -50%) scale(0);
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animation: accuracy-wave 5s ease-out infinite forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map {
|
||||||
|
animation: fade-in 0.3s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -100%);
|
||||||
|
animation: fade-in 0.3s forwards;
|
||||||
|
|
||||||
|
--pin-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.geoLive {
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 50%);
|
||||||
|
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
|
||||||
|
background: var(--color-primary);
|
||||||
|
border: 2px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.round-pin {
|
||||||
|
bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.direction {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transition: transform 0.3s ease-out;
|
||||||
|
transform: translate(-50%, 0.3125rem) rotate(var(--direction));
|
||||||
|
transform-origin: bottom;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 2rem;
|
||||||
|
clip-path: polygon(50% 100%, 0 0, 100% 0);
|
||||||
|
background: radial-gradient(circle, var(--color-primary) -100%, transparent 100%);
|
||||||
|
border-radius: 40%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-avatar {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 1.3125rem;
|
||||||
|
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.venue-icon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -0.5rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.geo {
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.geoLive,
|
||||||
|
.venue {
|
||||||
|
filter: drop-shadow(0 0 2px var(--color-default-shadow));
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired {
|
||||||
|
--pin-color: white;
|
||||||
|
|
||||||
|
.location-avatar::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.round-pin {
|
||||||
|
fill: var(--pin-color);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
width: 5rem;
|
||||||
|
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.3125rem 0.5rem 0.375rem;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
font-weight: 500;
|
||||||
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-subtitle {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
grid-area: 2 / 1 / 2 / 2;
|
||||||
|
|
||||||
|
.Message.own & {
|
||||||
|
color: var(--color-message-meta-own);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.geo-countdown {
|
||||||
|
grid-area: 1 / 2 / 3 / 3;
|
||||||
|
position: relative;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.geo-countdown-text {
|
||||||
|
color: var(--accent-color);
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.geo-countdown-progress {
|
||||||
|
stroke: var(--accent-color);
|
||||||
|
fill: transparent;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
transition: stroke-dashoffset 2s, stroke 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
282
src/components/middle/message/Location.tsx
Normal file
282
src/components/middle/message/Location.tsx
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { ApiChat, ApiMessage, ApiUser } from '../../../api/types';
|
||||||
|
import { ISettings } from '../../../types';
|
||||||
|
|
||||||
|
import { CUSTOM_APPENDIX_ATTRIBUTE } from '../../../config';
|
||||||
|
import {
|
||||||
|
getMessageLocation,
|
||||||
|
buildStaticMapHash,
|
||||||
|
isGeoLiveExpired,
|
||||||
|
isOwnMessage,
|
||||||
|
isUserId,
|
||||||
|
} from '../../../modules/helpers';
|
||||||
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
import getCustomAppendixBg from './helpers/getCustomAppendixBg';
|
||||||
|
import { formatCountdownShort, formatLastUpdated } from '../../../util/dateFormat';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||||
|
import useTimeout from '../../../hooks/useTimeout';
|
||||||
|
import { getMetersPerPixel, getVenueColor, getVenueIconUrl, prepareMapUrl } from '../../../util/map';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
|
import useInterval from '../../../hooks/useInterval';
|
||||||
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
|
|
||||||
|
import Avatar from '../../common/Avatar';
|
||||||
|
import Skeleton from '../../ui/Skeleton';
|
||||||
|
|
||||||
|
import mapPin from '/src/assets/map-pin.svg';
|
||||||
|
import './Location.scss';
|
||||||
|
|
||||||
|
const MOVE_THRESHOLD = 0.0001; // ~11m
|
||||||
|
const DEFAULT_MAP_CONFIG = {
|
||||||
|
width: 400,
|
||||||
|
height: 300,
|
||||||
|
zoom: 16,
|
||||||
|
scale: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
const SVG_PIN = { __html: '<svg version="1.1" class="round-pin" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve"><g><circle cx="32" cy="32" r="24.5"/><path d="M32,8c13.23,0,24,10.77,24,24S45.23,56,32,56S8,45.23,8,32S18.77,8,32,8 M32,7C18.19,7,7,18.19,7,32s11.19,25,25,25 s25-11.19,25-25S45.81,7,32,7L32,7z"/></g><g><polygon points="29.38,57.67 27.4,56.08 30.42,54.42 32,51.54 33.58,54.42 36.6,56.08 34.69,57.61 32,60.73"/><path d="M32,52.58l1.07,1.95l0.14,0.26l0.26,0.14l2.24,1.22l-1.33,1.06l-0.07,0.06l-0.06,0.07L32,59.96l-2.24-2.61l-0.06-0.07 l-0.07-0.06l-1.33-1.06l2.24-1.22l0.26-0.14l0.14-0.26L32,52.58 M32,50.5l-1.94,3.56L26.5,56l2.5,2l3,3.5l3-3.5l2.5-2l-3.56-1.94 L32,50.5L32,50.5z"/></g></svg>' };
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
message: ApiMessage;
|
||||||
|
peer?: ApiUser | ApiChat;
|
||||||
|
lastSyncTime?: number;
|
||||||
|
isInSelectMode?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
theme: ISettings['theme'];
|
||||||
|
serverTimeOffset: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Location: FC<OwnProps> = ({
|
||||||
|
message,
|
||||||
|
peer,
|
||||||
|
lastSyncTime,
|
||||||
|
isInSelectMode,
|
||||||
|
isSelected,
|
||||||
|
theme,
|
||||||
|
serverTimeOffset,
|
||||||
|
}) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const countdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const lang = useLang();
|
||||||
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
|
const location = getMessageLocation(message)!;
|
||||||
|
const { type, geo } = location;
|
||||||
|
|
||||||
|
const serverTime = getServerTime(serverTimeOffset);
|
||||||
|
const isExpired = isGeoLiveExpired(message, serverTime);
|
||||||
|
const secondsBeforeEnd = (type === 'geoLive' && !isExpired) ? message.date + location.period - serverTime
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const [point, setPoint] = useState(geo);
|
||||||
|
|
||||||
|
const shouldRenderText = type === 'venue' || (type === 'geoLive' && !isExpired);
|
||||||
|
const { width, height, zoom, scale } = DEFAULT_MAP_CONFIG;
|
||||||
|
|
||||||
|
const mediaHash = Boolean(lastSyncTime) && buildStaticMapHash(point, width, height, zoom, scale);
|
||||||
|
const mediaBlobUrl = useMedia(mediaHash);
|
||||||
|
const prevMediaBlobUrl = usePrevious(mediaBlobUrl);
|
||||||
|
const mapBlobUrl = mediaBlobUrl || prevMediaBlobUrl;
|
||||||
|
|
||||||
|
const isPeerUser = peer && isUserId(peer.id);
|
||||||
|
const avatarUser = (peer && isPeerUser) ? peer as ApiUser : undefined;
|
||||||
|
const avatarChat = (peer && !isPeerUser) ? peer as ApiChat : undefined;
|
||||||
|
|
||||||
|
const isOwn = isOwnMessage(message);
|
||||||
|
|
||||||
|
const accuracyRadiusPx = useMemo(() => {
|
||||||
|
if (type !== 'geoLive' || !point.accuracyRadius) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lat, accuracyRadius } = point;
|
||||||
|
return accuracyRadius / getMetersPerPixel(lat, zoom);
|
||||||
|
}, [type, point, zoom]);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
const url = prepareMapUrl(point.lat, point.long, zoom);
|
||||||
|
window.open(url, '_blank')?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCountdown = useCallback((countdownEl: HTMLDivElement) => {
|
||||||
|
if (type !== 'geoLive') return;
|
||||||
|
const radius = 12;
|
||||||
|
const circumference = radius * 2 * Math.PI;
|
||||||
|
const svgEl = countdownEl.lastElementChild;
|
||||||
|
const timerEl = countdownEl.firstElementChild as SVGElement;
|
||||||
|
|
||||||
|
const timeLeft = message.date + location.period - getServerTime(serverTimeOffset);
|
||||||
|
const strokeDashOffset = (1 - timeLeft / location.period) * circumference;
|
||||||
|
const text = formatCountdownShort(lang, timeLeft * 1000);
|
||||||
|
|
||||||
|
if (!svgEl || !timerEl) {
|
||||||
|
countdownEl.innerHTML = `
|
||||||
|
<span class="geo-countdown-text">${text}</span>
|
||||||
|
<svg width="32px" height="32px">
|
||||||
|
<circle cx="16" cy="16" r="${radius}" class="geo-countdown-progress" transform="rotate(-90, 16, 16)"
|
||||||
|
stroke-dasharray="${circumference} ${circumference}"
|
||||||
|
stroke-dashoffset="-${strokeDashOffset}"
|
||||||
|
/>
|
||||||
|
</svg>`;
|
||||||
|
} else {
|
||||||
|
timerEl.textContent = text;
|
||||||
|
svgEl.firstElementChild!.setAttribute('stroke-dashoffset', `-${strokeDashOffset}`);
|
||||||
|
}
|
||||||
|
}, [type, message.date, location, serverTimeOffset, lang]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (countdownRef.current) {
|
||||||
|
updateCountdown(countdownRef.current);
|
||||||
|
}
|
||||||
|
}, [updateCountdown]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (shouldRenderText) return;
|
||||||
|
const contentEl = ref.current!.closest<HTMLDivElement>('.message-content')!;
|
||||||
|
if (mapBlobUrl) {
|
||||||
|
getCustomAppendixBg(mapBlobUrl, isOwn, isInSelectMode, isSelected, theme).then((appendixBg) => {
|
||||||
|
contentEl.style.setProperty('--appendix-bg', appendixBg);
|
||||||
|
contentEl.classList.add('has-appendix-thumb');
|
||||||
|
contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOwn, isInSelectMode, isSelected, theme, mapBlobUrl, shouldRenderText]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Prevent map refetching for slight location changes
|
||||||
|
if (Math.abs(geo.lat - point.lat) < MOVE_THRESHOLD && Math.abs(geo.long - point.long) < MOVE_THRESHOLD) {
|
||||||
|
if (point.accuracyRadius !== geo.accuracyRadius) {
|
||||||
|
setPoint({
|
||||||
|
...point,
|
||||||
|
accuracyRadius: geo.accuracyRadius,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPoint(geo);
|
||||||
|
}, [geo, point]);
|
||||||
|
|
||||||
|
useTimeout(() => {
|
||||||
|
forceUpdate();
|
||||||
|
}, !isExpired ? (secondsBeforeEnd || 0) * 1000 : undefined);
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
const countdownEl = countdownRef.current;
|
||||||
|
|
||||||
|
if (countdownEl) {
|
||||||
|
updateCountdown(countdownEl);
|
||||||
|
}
|
||||||
|
}, secondsBeforeEnd ? 1000 : undefined);
|
||||||
|
|
||||||
|
function renderInfo() {
|
||||||
|
if (!shouldRenderText) return undefined;
|
||||||
|
if (type === 'venue') {
|
||||||
|
return (
|
||||||
|
<div className="location-info">
|
||||||
|
<div className="location-info-title">
|
||||||
|
{location.title}
|
||||||
|
</div>
|
||||||
|
<div className="location-info-subtitle">
|
||||||
|
{location.address}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'geoLive') {
|
||||||
|
return (
|
||||||
|
<div className="location-info">
|
||||||
|
<div className="location-info-title">{lang('AttachLiveLocation')}</div>
|
||||||
|
<div className="location-info-subtitle">
|
||||||
|
{formatLastUpdated(lang, serverTime, message.editDate)}
|
||||||
|
</div>
|
||||||
|
{!isExpired && <div className="geo-countdown" ref={countdownRef} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMap() {
|
||||||
|
if (!mapBlobUrl) return <Skeleton width={width} height={height} />;
|
||||||
|
return <img
|
||||||
|
className="full-media map"
|
||||||
|
src={mapBlobUrl}
|
||||||
|
alt="Location on a map"
|
||||||
|
width={DEFAULT_MAP_CONFIG.width}
|
||||||
|
height={DEFAULT_MAP_CONFIG.height}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPin() {
|
||||||
|
const pinClassName = buildClassName(
|
||||||
|
'pin',
|
||||||
|
type,
|
||||||
|
isExpired && 'expired',
|
||||||
|
);
|
||||||
|
if (type === 'geoLive') {
|
||||||
|
return (
|
||||||
|
<div className={pinClassName} dangerouslySetInnerHTML={SVG_PIN}>
|
||||||
|
<Avatar chat={avatarChat} user={avatarUser} className="location-avatar" />
|
||||||
|
{location.heading !== undefined && (
|
||||||
|
<div className="direction" style={`--direction: ${location.heading}deg`} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'venue') {
|
||||||
|
const color = getVenueColor(location.venueType);
|
||||||
|
const icon = getVenueIconUrl(location.venueType);
|
||||||
|
return (
|
||||||
|
<div className={pinClassName} dangerouslySetInnerHTML={SVG_PIN} style={`--pin-color: ${color}`}>
|
||||||
|
<img src={icon} className="venue-icon" alt="" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img className={pinClassName} src={mapPin} alt="" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOverlay() {
|
||||||
|
if (!mapBlobUrl) return undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Boolean(accuracyRadiusPx) && !isExpired && (
|
||||||
|
<div
|
||||||
|
className="location-accuracy"
|
||||||
|
style={`width: ${accuracyRadiusPx * 2}px; height: ${accuracyRadiusPx * 2}px`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{renderPin()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="Location media-inner interactive"
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className="map-wrapper">
|
||||||
|
{renderMap()}
|
||||||
|
{renderOverlay()}
|
||||||
|
</div>
|
||||||
|
{renderInfo()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Location);
|
||||||
|
|
||||||
@ -61,6 +61,8 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
|
margin-right: 0.3125rem;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
@ -553,10 +555,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Avatar {
|
|
||||||
margin-right: 0.3125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
&.has-replies:not(.custom-shape):not(.has-reactions) .WebPage.with-video .media-inner {
|
&.has-replies:not(.custom-shape):not(.has-reactions) .WebPage.with-video .media-inner {
|
||||||
margin-bottom: 1.5rem !important;
|
margin-bottom: 1.5rem !important;
|
||||||
|
|||||||
@ -68,6 +68,7 @@ import {
|
|||||||
getSenderTitle,
|
getSenderTitle,
|
||||||
getUserColorKey,
|
getUserColorKey,
|
||||||
areReactionsEmpty,
|
areReactionsEmpty,
|
||||||
|
isGeoLiveExpired,
|
||||||
} from '../../../modules/helpers';
|
} from '../../../modules/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||||
@ -86,6 +87,7 @@ import useFlag from '../../../hooks/useFlag';
|
|||||||
import useFocusMessage from './hooks/useFocusMessage';
|
import useFocusMessage from './hooks/useFocusMessage';
|
||||||
import useOuterHandlers from './hooks/useOuterHandlers';
|
import useOuterHandlers from './hooks/useOuterHandlers';
|
||||||
import useInnerHandlers from './hooks/useInnerHandlers';
|
import useInnerHandlers from './hooks/useInnerHandlers';
|
||||||
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
@ -102,6 +104,7 @@ import Contact from './Contact';
|
|||||||
import Poll from './Poll';
|
import Poll from './Poll';
|
||||||
import WebPage from './WebPage';
|
import WebPage from './WebPage';
|
||||||
import Invoice from './Invoice';
|
import Invoice from './Invoice';
|
||||||
|
import Location from './Location';
|
||||||
import Album from './Album';
|
import Album from './Album';
|
||||||
import RoundVideo from './RoundVideo';
|
import RoundVideo from './RoundVideo';
|
||||||
import InlineButtons from './InlineButtons';
|
import InlineButtons from './InlineButtons';
|
||||||
@ -143,6 +146,7 @@ type StateProps = {
|
|||||||
forceSenderName?: boolean;
|
forceSenderName?: boolean;
|
||||||
chatUsername?: string;
|
chatUsername?: string;
|
||||||
sender?: ApiUser | ApiChat;
|
sender?: ApiUser | ApiChat;
|
||||||
|
canShowSender: boolean;
|
||||||
originSender?: ApiUser | ApiChat;
|
originSender?: ApiUser | ApiChat;
|
||||||
botSender?: ApiUser;
|
botSender?: ApiUser;
|
||||||
isThreadTop?: boolean;
|
isThreadTop?: boolean;
|
||||||
@ -163,6 +167,7 @@ type StateProps = {
|
|||||||
isChannel?: boolean;
|
isChannel?: boolean;
|
||||||
canReply?: boolean;
|
canReply?: boolean;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
|
serverTimeOffset: number;
|
||||||
highlight?: string;
|
highlight?: string;
|
||||||
isSingleEmoji?: boolean;
|
isSingleEmoji?: boolean;
|
||||||
animatedEmoji?: ApiSticker;
|
animatedEmoji?: ApiSticker;
|
||||||
@ -227,6 +232,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
theme,
|
theme,
|
||||||
forceSenderName,
|
forceSenderName,
|
||||||
sender,
|
sender,
|
||||||
|
canShowSender,
|
||||||
originSender,
|
originSender,
|
||||||
botSender,
|
botSender,
|
||||||
isThreadTop,
|
isThreadTop,
|
||||||
@ -247,6 +253,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
isChannel,
|
isChannel,
|
||||||
canReply,
|
canReply,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
serverTimeOffset,
|
||||||
highlight,
|
highlight,
|
||||||
animatedEmoji,
|
animatedEmoji,
|
||||||
localSticker,
|
localSticker,
|
||||||
@ -357,8 +364,10 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
}, [toggleMessageSelection, messageId, isAlbum, album]);
|
}, [toggleMessageSelection, messageId, isAlbum, album]);
|
||||||
|
|
||||||
const avatarPeer = forwardInfo && (isChatWithSelf || isRepliesChat || !sender) ? originSender : sender;
|
const messageSender = canShowSender ? sender : undefined;
|
||||||
const senderPeer = forwardInfo ? originSender : sender;
|
|
||||||
|
const avatarPeer = forwardInfo && (isChatWithSelf || isRepliesChat || !messageSender) ? originSender : messageSender;
|
||||||
|
const senderPeer = forwardInfo ? originSender : messageSender;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleMouseDown,
|
handleMouseDown,
|
||||||
@ -449,6 +458,11 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
Boolean(activeReaction) && 'has-active-reaction',
|
Boolean(activeReaction) && 'has-active-reaction',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location,
|
||||||
|
} = getMessageContent(message);
|
||||||
|
|
||||||
const contentClassName = buildContentClassName(message, {
|
const contentClassName = buildContentClassName(message, {
|
||||||
hasReply,
|
hasReply,
|
||||||
customShape,
|
customShape,
|
||||||
@ -459,15 +473,12 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
hasComments: threadInfo && threadInfo?.messagesCount > 0,
|
hasComments: threadInfo && threadInfo?.messagesCount > 0,
|
||||||
hasActionButton: canForward || canFocus,
|
hasActionButton: canForward || canFocus,
|
||||||
hasReactions,
|
hasReactions,
|
||||||
|
isGeoLiveActive: location?.type === 'geoLive' && !isGeoLiveExpired(message, getServerTime(serverTimeOffset)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const withAppendix = contentClassName.includes('has-appendix');
|
const withAppendix = contentClassName.includes('has-appendix');
|
||||||
const textParts = renderMessageText(message, highlight, isEmojiOnlyMessage(customShape));
|
const textParts = renderMessageText(message, highlight, isEmojiOnlyMessage(customShape));
|
||||||
|
|
||||||
const {
|
|
||||||
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice,
|
|
||||||
} = getMessageContent(message);
|
|
||||||
|
|
||||||
let metaPosition!: MetaPosition;
|
let metaPosition!: MetaPosition;
|
||||||
if (isInDocumentGroupNotLast) {
|
if (isInDocumentGroupNotLast) {
|
||||||
metaPosition = 'none';
|
metaPosition = 'none';
|
||||||
@ -764,13 +775,25 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{invoice && <Invoice message={message} />}
|
{invoice && <Invoice message={message} />}
|
||||||
|
{location && (
|
||||||
|
<Location
|
||||||
|
message={message}
|
||||||
|
lastSyncTime={lastSyncTime}
|
||||||
|
isInSelectMode={isInSelectMode}
|
||||||
|
isSelected={isSelected}
|
||||||
|
theme={theme}
|
||||||
|
peer={sender}
|
||||||
|
serverTimeOffset={serverTimeOffset}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSenderName() {
|
function renderSenderName() {
|
||||||
|
const media = photo || video || location;
|
||||||
const shouldRender = !(customShape && !viaBotId) && (
|
const shouldRender = !(customShape && !viaBotId) && (
|
||||||
(withSenderName && !photo && !video) || asForwarded || viaBotId || forceSenderName
|
(withSenderName && !media) || asForwarded || viaBotId || forceSenderName
|
||||||
) && !isInDocumentGroupNotFirst && !(hasReply && customShape);
|
) && !isInDocumentGroupNotFirst && !(hasReply && customShape);
|
||||||
|
|
||||||
if (!shouldRender) {
|
if (!shouldRender) {
|
||||||
@ -940,7 +963,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, ownProps): StateProps => {
|
(global, ownProps): StateProps => {
|
||||||
const { focusedMessage, forwardMessages, lastSyncTime } = global;
|
const { focusedMessage, forwardMessages, lastSyncTime, serverTimeOffset } = global;
|
||||||
const {
|
const {
|
||||||
message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup,
|
message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup,
|
||||||
} = ownProps;
|
} = ownProps;
|
||||||
@ -1013,7 +1036,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
chatUsername,
|
chatUsername,
|
||||||
forceSenderName,
|
forceSenderName,
|
||||||
sender: canShowSender ? sender : undefined,
|
sender,
|
||||||
|
canShowSender,
|
||||||
originSender,
|
originSender,
|
||||||
botSender,
|
botSender,
|
||||||
shouldHideReply,
|
shouldHideReply,
|
||||||
@ -1030,6 +1054,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isChannel,
|
isChannel,
|
||||||
canReply,
|
canReply,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
serverTimeOffset,
|
||||||
highlight,
|
highlight,
|
||||||
isSingleEmoji: Boolean(singleEmoji),
|
isSingleEmoji: Boolean(singleEmoji),
|
||||||
animatedEmoji: singleEmoji ? selectAnimatedEmoji(global, singleEmoji) : undefined,
|
animatedEmoji: singleEmoji ? selectAnimatedEmoji(global, singleEmoji) : undefined,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { ApiMessage } from '../../../api/types';
|
|||||||
import { ISettings } from '../../../types';
|
import { ISettings } from '../../../types';
|
||||||
import { IMediaDimensions } from './helpers/calculateAlbumLayout';
|
import { IMediaDimensions } from './helpers/calculateAlbumLayout';
|
||||||
|
|
||||||
|
import { CUSTOM_APPENDIX_ATTRIBUTE } from '../../../config';
|
||||||
import {
|
import {
|
||||||
getMessagePhoto,
|
getMessagePhoto,
|
||||||
getMessageWebPagePhoto,
|
getMessageWebPagePhoto,
|
||||||
@ -45,8 +46,6 @@ export type OwnProps = {
|
|||||||
onCancelUpload?: (message: ApiMessage) => void;
|
onCancelUpload?: (message: ApiMessage) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CUSTOM_APPENDIX_ATTRIBUTE = 'data-has-custom-appendix';
|
|
||||||
|
|
||||||
const Photo: FC<OwnProps> = ({
|
const Photo: FC<OwnProps> = ({
|
||||||
id,
|
id,
|
||||||
message,
|
message,
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export function buildContentClassName(
|
|||||||
hasComments,
|
hasComments,
|
||||||
hasActionButton,
|
hasActionButton,
|
||||||
hasReactions,
|
hasReactions,
|
||||||
|
isGeoLiveActive,
|
||||||
}: {
|
}: {
|
||||||
hasReply?: boolean;
|
hasReply?: boolean;
|
||||||
customShape?: boolean | number;
|
customShape?: boolean | number;
|
||||||
@ -28,20 +29,22 @@ export function buildContentClassName(
|
|||||||
hasComments?: boolean;
|
hasComments?: boolean;
|
||||||
hasActionButton?: boolean;
|
hasActionButton?: boolean;
|
||||||
hasReactions?: boolean;
|
hasReactions?: boolean;
|
||||||
|
isGeoLiveActive?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
text, photo, video, audio, voice, document, poll, webPage, contact,
|
text, photo, video, audio, voice, document, poll, webPage, contact, location,
|
||||||
} = getMessageContent(message);
|
} = getMessageContent(message);
|
||||||
|
|
||||||
const classNames = ['message-content'];
|
const classNames = ['message-content'];
|
||||||
const isMedia = photo || video;
|
const isMedia = photo || video || location;
|
||||||
const isMediaWithNoText = isMedia && !text;
|
const hasText = text || location?.type === 'venue' || isGeoLiveActive;
|
||||||
|
const isMediaWithNoText = isMedia && !hasText;
|
||||||
const isViaBot = Boolean(message.viaBotId);
|
const isViaBot = Boolean(message.viaBotId);
|
||||||
|
|
||||||
if (isEmojiOnlyMessage(customShape)) {
|
if (isEmojiOnlyMessage(customShape)) {
|
||||||
classNames.push(`emoji-only emoji-only-${customShape}`);
|
classNames.push(`emoji-only emoji-only-${customShape}`);
|
||||||
} else if (text) {
|
} else if (hasText) {
|
||||||
classNames.push('text');
|
classNames.push('text');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +62,7 @@ export function buildContentClassName(
|
|||||||
classNames.push('has-comments');
|
classNames.push('has-comments');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (photo || video) {
|
if (isMedia) {
|
||||||
classNames.push('media');
|
classNames.push('media');
|
||||||
} else if (audio) {
|
} else if (audio) {
|
||||||
classNames.push('audio');
|
classNames.push('audio');
|
||||||
@ -114,7 +117,7 @@ export function buildContentClassName(
|
|||||||
classNames.push('has-solid-background');
|
classNames.push('has-solid-background');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLastInGroup && (photo || !isMediaWithNoText)) {
|
if (isLastInGroup && (photo || (location && !hasText) || !isMediaWithNoText)) {
|
||||||
classNames.push('has-appendix');
|
classNames.push('has-appendix');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/components/ui/Skeleton.scss
Normal file
53
src/components/ui/Skeleton.scss
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
.Skeleton {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--color-skeleton-background);
|
||||||
|
&.round {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rounded-rect {
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pulse::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-skeleton-foreground);
|
||||||
|
animation: skeleton-pulse 2s ease-in-out 0.5s infinite;
|
||||||
|
|
||||||
|
@keyframes skeleton-pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.wave::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to right, transparent 0%, var(--color-skeleton-foreground) 50%, transparent 100%);
|
||||||
|
animation: skeleton-wave 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
|
||||||
|
@keyframes skeleton-wave {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/components/ui/Skeleton.tsx
Normal file
23
src/components/ui/Skeleton.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
|
import './Skeleton.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
variant?: 'rectangular' | 'rounded-rect' | 'round';
|
||||||
|
animation?: 'wave' | 'pulse';
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Skeleton: FC<OwnProps> = ({ variant = 'rectangular', animation = 'wave', width, height, className }) => {
|
||||||
|
const classNames = buildClassName('Skeleton', variant, animation, className);
|
||||||
|
const style = (width ? `width: ${width}px;` : '') + (height ? `height: ${height}px;` : '');
|
||||||
|
return (
|
||||||
|
<div className={classNames} style={style} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Skeleton;
|
||||||
@ -85,6 +85,8 @@ export const SEND_MESSAGE_ACTION_INTERVAL = 3000; // 3s
|
|||||||
export const EDITABLE_INPUT_ID = 'editable-message-text';
|
export const EDITABLE_INPUT_ID = 'editable-message-text';
|
||||||
export const EDITABLE_INPUT_MODAL_ID = 'editable-message-text-modal';
|
export const EDITABLE_INPUT_MODAL_ID = 'editable-message-text-modal';
|
||||||
|
|
||||||
|
export const CUSTOM_APPENDIX_ATTRIBUTE = 'data-has-custom-appendix';
|
||||||
|
|
||||||
// Screen width where Pinned Message / Audio Player in the Middle Header can be safely displayed
|
// Screen width where Pinned Message / Audio Player in the Middle Header can be safely displayed
|
||||||
export const SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN = 1440; // px
|
export const SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN = 1440; // px
|
||||||
// Screen width where Pinned Message / Audio Player in the Middle Header shouldn't collapse with ChatInfo
|
// Screen width where Pinned Message / Audio Player in the Middle Header shouldn't collapse with ChatInfo
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useLayoutEffect, useRef } from '../lib/teact/teact';
|
import { useEffect, useLayoutEffect, useRef } from '../lib/teact/teact';
|
||||||
|
|
||||||
function useTimeout(callback: () => void, delay: number | null) {
|
function useTimeout(callback: () => void, delay?: number) {
|
||||||
const savedCallback = useRef(callback);
|
const savedCallback = useRef(callback);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
|||||||
2
src/lib/gramjs/client/TelegramClient.d.ts
vendored
2
src/lib/gramjs/client/TelegramClient.d.ts
vendored
@ -6,7 +6,7 @@ import { downloadFile, DownloadFileParams } from './downloadFile';
|
|||||||
import { TwoFaParams, updateTwoFaSettings } from './2fa';
|
import { TwoFaParams, updateTwoFaSettings } from './2fa';
|
||||||
|
|
||||||
declare class TelegramClient {
|
declare class TelegramClient {
|
||||||
constructor(...args: any)
|
constructor(...args: any);
|
||||||
|
|
||||||
async start(authParams: UserAuthParams | BotAuthParams);
|
async start(authParams: UserAuthParams | BotAuthParams);
|
||||||
|
|
||||||
|
|||||||
@ -186,6 +186,7 @@ class TelegramClient {
|
|||||||
}
|
}
|
||||||
// set defaults vars
|
// set defaults vars
|
||||||
this._sender.userDisconnected = false;
|
this._sender.userDisconnected = false;
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
this._sender._user_connected = false;
|
this._sender._user_connected = false;
|
||||||
this._sender.isReconnecting = false;
|
this._sender.isReconnecting = false;
|
||||||
this._sender._disconnected = true;
|
this._sender._disconnected = true;
|
||||||
@ -678,7 +679,6 @@ class TelegramClient {
|
|||||||
throw new Error('not implemented');
|
throw new Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
async _downloadWebDocument(media) {
|
async _downloadWebDocument(media) {
|
||||||
try {
|
try {
|
||||||
const buff = [];
|
const buff = [];
|
||||||
@ -715,6 +715,59 @@ class TelegramClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadStaticMap(accessHash, long, lat, w, h, zoom, scale, accuracyRadius) {
|
||||||
|
try {
|
||||||
|
const buff = [];
|
||||||
|
let offset = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const downloaded = new requests.upload.GetWebFile({
|
||||||
|
location: new constructors.InputWebFileGeoPointLocation({
|
||||||
|
geoPoint: new constructors.InputGeoPoint({
|
||||||
|
lat,
|
||||||
|
long,
|
||||||
|
accuracyRadius,
|
||||||
|
}),
|
||||||
|
accessHash,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
zoom,
|
||||||
|
scale,
|
||||||
|
}),
|
||||||
|
offset,
|
||||||
|
limit: WEBDOCUMENT_REQUEST_PART_SIZE,
|
||||||
|
});
|
||||||
|
const sender = await this._borrowExportedSender(WEBDOCUMENT_DC_ID);
|
||||||
|
const res = await sender.send(downloaded);
|
||||||
|
offset += 131072;
|
||||||
|
if (res.bytes.length) {
|
||||||
|
buff.push(res.bytes);
|
||||||
|
if (res.bytes.length < WEBDOCUMENT_REQUEST_PART_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof errors.FloodWaitError) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(`getWebFile: sleeping for ${err.seconds}s on flood wait`);
|
||||||
|
await sleep(err.seconds * 1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Buffer.concat(buff);
|
||||||
|
} catch (e) {
|
||||||
|
// the file is no longer saved in telegram's cache.
|
||||||
|
if (e.message === 'WEBFILE_NOT_AVAILABLE') {
|
||||||
|
return Buffer.alloc(0);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// region Invoking Telegram request
|
// region Invoking Telegram request
|
||||||
/**
|
/**
|
||||||
* Invokes a MTProtoRequest (sends and receives it) and returns its result
|
* Invokes a MTProtoRequest (sends and receives it) and returns its result
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
ApiAudio, ApiMediaFormat, ApiMessage, ApiMessageSearchType, ApiPhoto, ApiVideo, ApiDimensions,
|
ApiAudio, ApiMediaFormat, ApiMessage, ApiMessageSearchType, ApiPhoto, ApiVideo, ApiDimensions, ApiLocation,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
|
|
||||||
import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, IS_SAFARI } from '../../util/environment';
|
import { IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, IS_SAFARI } from '../../util/environment';
|
||||||
@ -88,6 +88,10 @@ export function getMessageInvoice(message: ApiMessage) {
|
|||||||
return message.content.invoice;
|
return message.content.invoice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMessageLocation(message: ApiMessage) {
|
||||||
|
return message.content.location;
|
||||||
|
}
|
||||||
|
|
||||||
export function getMessageWebPage(message: ApiMessage) {
|
export function getMessageWebPage(message: ApiMessage) {
|
||||||
return message.content.webPage;
|
return message.content.webPage;
|
||||||
}
|
}
|
||||||
@ -123,6 +127,19 @@ export function getMessageMediaThumbDataUri(message: ApiMessage) {
|
|||||||
return getMessageMediaThumbnail(message)?.dataUri;
|
return getMessageMediaThumbnail(message)?.dataUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildStaticMapHash(
|
||||||
|
geo: ApiLocation['geo'],
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
zoom: number,
|
||||||
|
scale: number,
|
||||||
|
) {
|
||||||
|
const { long, lat, accessHash, accuracyRadius } = geo;
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
return `staticMap:${accessHash}?lat=${lat}&long=${long}&w=${width}&h=${height}&zoom=${zoom}&scale=${scale}&accuracyRadius=${accuracyRadius}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function getMessageMediaHash(
|
export function getMessageMediaHash(
|
||||||
message: ApiMessage,
|
message: ApiMessage,
|
||||||
target: Target,
|
target: Target,
|
||||||
@ -136,11 +153,13 @@ export function getMessageMediaHash(
|
|||||||
const messageVideo = video || webPageVideo;
|
const messageVideo = video || webPageVideo;
|
||||||
const messagePhoto = photo || webPagePhoto;
|
const messagePhoto = photo || webPagePhoto;
|
||||||
|
|
||||||
if (!(messagePhoto || messageVideo || sticker || audio || voice || document)) {
|
const content = messagePhoto || messageVideo || sticker || audio || voice || document;
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaId = (messagePhoto || messageVideo || sticker || audio || voice || document)!.id;
|
const mediaId = content.id;
|
||||||
const base = `${getMessageKey(message)}${mediaId ? `:${mediaId}` : ''}`;
|
const base = `${getMessageKey(message)}${mediaId ? `:${mediaId}` : ''}`;
|
||||||
|
|
||||||
if (messageVideo) {
|
if (messageVideo) {
|
||||||
|
|||||||
@ -118,6 +118,7 @@ export function getMessageSummaryDescription(
|
|||||||
contact,
|
contact,
|
||||||
poll,
|
poll,
|
||||||
invoice,
|
invoice,
|
||||||
|
location,
|
||||||
} = message.content;
|
} = message.content;
|
||||||
|
|
||||||
let summary: string | TextPart[] | undefined;
|
let summary: string | TextPart[] | undefined;
|
||||||
@ -170,6 +171,14 @@ export function getMessageSummaryDescription(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (location?.type === 'geo' || location?.type === 'venue') {
|
||||||
|
summary = lang('Message.Location');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location?.type === 'geoLive') {
|
||||||
|
summary = lang('Message.LiveLocation');
|
||||||
|
}
|
||||||
|
|
||||||
const reaction = !noReactions && getMessageRecentReaction(message);
|
const reaction = !noReactions && getMessageRecentReaction(message);
|
||||||
if (summary && reaction) {
|
if (summary && reaction) {
|
||||||
summary = `to your "${summary}"`;
|
summary = `to your "${summary}"`;
|
||||||
|
|||||||
@ -40,14 +40,15 @@ export function getMessageOriginalId(message: ApiMessage) {
|
|||||||
|
|
||||||
export function getMessageText(message: ApiMessage) {
|
export function getMessageText(message: ApiMessage) {
|
||||||
const {
|
const {
|
||||||
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice,
|
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location,
|
||||||
} = message.content;
|
} = message.content;
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
return text.text;
|
return text.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sticker || photo || video || audio || voice || document || contact || poll || webPage || invoice) {
|
if (sticker || photo || video || audio || voice || document
|
||||||
|
|| contact || poll || webPage || invoice || location) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,3 +216,9 @@ export function getMessageContentFilename(message: ApiMessage) {
|
|||||||
export function areReactionsEmpty(reactions: ApiReactions) {
|
export function areReactionsEmpty(reactions: ApiReactions) {
|
||||||
return !reactions.results.some((l) => l.count > 0);
|
return !reactions.results.some((l) => l.count > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isGeoLiveExpired(message: ApiMessage, timestamp = Date.now() / 1000) {
|
||||||
|
const { location } = message.content;
|
||||||
|
if (location?.type !== 'geoLive') return false;
|
||||||
|
return (timestamp - (message.date || 0) >= location.period);
|
||||||
|
}
|
||||||
|
|||||||
@ -368,7 +368,7 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
|| getServerTime(global.serverTimeOffset) - message.date < MESSAGE_EDIT_ALLOWED_TIME
|
|| getServerTime(global.serverTimeOffset) - message.date < MESSAGE_EDIT_ALLOWED_TIME
|
||||||
) && !(
|
) && !(
|
||||||
content.sticker || content.contact || content.poll || content.action || content.audio
|
content.sticker || content.contact || content.poll || content.action || content.audio
|
||||||
|| (content.video?.isRound)
|
|| (content.video?.isRound) || content.location
|
||||||
)
|
)
|
||||||
&& !isForwardedMessage(message)
|
&& !isForwardedMessage(message)
|
||||||
&& !message.viaBotId
|
&& !message.viaBotId
|
||||||
|
|||||||
@ -162,6 +162,9 @@ $color-message-reaction-own-hover: #b5e0a4;
|
|||||||
--color-default-shadow: #72727240;
|
--color-default-shadow: #72727240;
|
||||||
--color-light-shadow: #7272722b;
|
--color-light-shadow: #7272722b;
|
||||||
|
|
||||||
|
--color-skeleton-background: rgba(33, 33, 33, 0.15);
|
||||||
|
--color-skeleton-foreground: rgba(232, 232, 232, 0.2);
|
||||||
|
|
||||||
--vh: 1vh;
|
--vh: 1vh;
|
||||||
|
|
||||||
--border-radius-default: 0.75rem;
|
--border-radius-default: 0.75rem;
|
||||||
|
|||||||
@ -106,6 +106,29 @@ export function formatCountdown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatCountdownShort(lang: LangFn, msLeft: number) {
|
||||||
|
if (msLeft < 60 * 1000) {
|
||||||
|
return Math.ceil(msLeft / 1000);
|
||||||
|
} else if (msLeft < 60 * 60 * 1000) {
|
||||||
|
return Math.ceil(msLeft / (60 * 1000));
|
||||||
|
} else if (msLeft < MILLISECONDS_IN_DAY) {
|
||||||
|
return lang('MessageTimer.ShortHours', Math.ceil(msLeft / (60 * 60 * 1000)));
|
||||||
|
} else {
|
||||||
|
return lang('MessageTimer.ShortDays', Math.ceil(msLeft / MILLISECONDS_IN_DAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatLastUpdated(lang: LangFn, currentTime: number, lastUpdated = currentTime) {
|
||||||
|
const seconds = currentTime - lastUpdated;
|
||||||
|
if (seconds < 60) {
|
||||||
|
return lang('LiveLocationUpdated.JustNow');
|
||||||
|
} else if (seconds < 60 * 60) {
|
||||||
|
return lang('LiveLocationUpdated.MinutesAgo', Math.floor(seconds / 60));
|
||||||
|
} else {
|
||||||
|
return lang('LiveLocationUpdated.TodayAt', formatTime(lang, lastUpdated));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function formatHumanDate(
|
export function formatHumanDate(
|
||||||
lang: LangFn,
|
lang: LangFn,
|
||||||
datetime: number | Date,
|
datetime: number | Date,
|
||||||
|
|||||||
57
src/util/map.ts
Normal file
57
src/util/map.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const PROVIDER = 'http://maps.google.com/maps';
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// https://github.com/TelegramMessenger/Telegram-iOS/blob/2a32c871882c4e1b1ccdecd34fccd301723b30d9/submodules/LocationResources/Sources/VenueIconResources.swift#L82
|
||||||
|
const VENUE_COLORS = new Map(Object.entries({
|
||||||
|
'building/medical': '#43b3f4',
|
||||||
|
'building/gym': '#43b3f4',
|
||||||
|
'arts_entertainment': '#e56dd6',
|
||||||
|
'travel/bedandbreakfast': '#9987ff',
|
||||||
|
'travel/hotel': '#9987ff',
|
||||||
|
'travel/hostel': '#9987ff',
|
||||||
|
'travel/resort': '#9987ff',
|
||||||
|
'building': '#6e81b2',
|
||||||
|
'education': '#a57348',
|
||||||
|
'event': '#959595',
|
||||||
|
'food': '#f7943f',
|
||||||
|
'education/cafeteria': '#f7943f',
|
||||||
|
'nightlife': '#e56dd6',
|
||||||
|
'travel/hotel_bar': '#e56dd6',
|
||||||
|
'parks_outdoors': '#6cc039',
|
||||||
|
'shops': '#ffb300',
|
||||||
|
'travel': '#1c9fff',
|
||||||
|
'work': '#ad7854',
|
||||||
|
'home': '#00aeef',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const RANDOM_COLORS = [
|
||||||
|
'#e56cd5', '#f89440', '#9986ff', '#44b3f5', '#6dc139', '#ff5d5a', '#f87aad', '#6e82b3', '#f5ba21',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function prepareMapUrl(lat: number, long: number, zoom: number) {
|
||||||
|
return `${PROVIDER}/place/${lat}+${long}/@${lat},${long},${zoom}z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMetersPerPixel(lat: number, zoom: number) {
|
||||||
|
// https://groups.google.com/g/google-maps-js-api-v3/c/hDRO4oHVSeM/m/osOYQYXg2oUJ
|
||||||
|
return 156543.03392 * Math.cos(lat * Math.PI / 180) / Math.pow(2, zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVenueIconUrl(type?: string) {
|
||||||
|
if (!type) return '';
|
||||||
|
return `https://ss3.4sqi.net/img/categories_v2/${type}_88.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// https://github.com/TelegramMessenger/Telegram-iOS/blob/2a32c871882c4e1b1ccdecd34fccd301723b30d9/submodules/LocationResources/Sources/VenueIconResources.swift#L104
|
||||||
|
export function getVenueColor(type?: string) {
|
||||||
|
if (!type) return '#008df2';
|
||||||
|
return VENUE_COLORS.get(type)
|
||||||
|
|| VENUE_COLORS.get(type.split('/')[0])
|
||||||
|
|| RANDOM_COLORS[stringToNumber(type) % RANDOM_COLORS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringToNumber(str: string) {
|
||||||
|
return str.split('').reduce((prevHash, currVal) =>
|
||||||
|
(((prevHash << 5) - prevHash) + currVal.charCodeAt(0)) | 0, 0);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user