import React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { getActions } from '../../../global'; import type { FC } from '../../../lib/teact/teact'; import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types'; import type { ISettings } from '../../../types'; import { getMessageLocation, buildStaticMapHash, isGeoLiveExpired, } from '../../../global/helpers'; import { formatCountdownShort, formatLastUpdated } from '../../../util/dateFormat'; import { getMetersPerPixel, getVenueColor, getVenueIconUrl, prepareMapUrl, } from '../../../util/map'; import { getServerTime } from '../../../util/serverTime'; import useLastCallback from '../../../hooks/useLastCallback'; import useMedia from '../../../hooks/useMedia'; import useLang from '../../../hooks/useLang'; import useForceUpdate from '../../../hooks/useForceUpdate'; import useTimeout from '../../../hooks/useTimeout'; import buildClassName from '../../../util/buildClassName'; import usePrevious from '../../../hooks/usePrevious'; import useInterval from '../../../hooks/useInterval'; import Avatar from '../../common/Avatar'; import Skeleton from '../../ui/Skeleton'; import './Location.scss'; import mapPin from '../../../assets/map-pin.svg'; 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: '' }; type OwnProps = { message: ApiMessage; peer?: ApiUser | ApiChat; isInSelectMode?: boolean; isSelected?: boolean; theme: ISettings['theme']; }; const Location: FC = ({ message, peer, }) => { const { openUrl } = getActions(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); // eslint-disable-next-line no-null/no-null const countdownRef = useRef(null); const lang = useLang(); const forceUpdate = useForceUpdate(); const location = getMessageLocation(message)!; const { type, geo } = location; const serverTime = getServerTime(); 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 = buildStaticMapHash(point, width, height, zoom, scale); const mediaBlobUrl = useMedia(mediaHash); const prevMediaBlobUrl = usePrevious(mediaBlobUrl); const mapBlobUrl = mediaBlobUrl || prevMediaBlobUrl; 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); openUrl({ url }); }; const updateCountdown = useLastCallback((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(); const strokeDashOffset = (1 - timeLeft / location.period) * circumference; const text = formatCountdownShort(lang, timeLeft * 1000); if (!svgEl || !timerEl) { countdownEl.innerHTML = ` ${text} `; } else { timerEl.textContent = text; svgEl.firstElementChild!.setAttribute('stroke-dashoffset', `-${strokeDashOffset}`); } }); useLayoutEffect(() => { if (countdownRef.current) { updateCountdown(countdownRef.current); } }, [updateCountdown]); 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(() => { requestMutation(() => { const countdownEl = countdownRef.current; if (countdownEl) { updateCountdown(countdownEl); } }); }, secondsBeforeEnd ? 1000 : undefined); function renderInfo() { if (!shouldRenderText) return undefined; if (type === 'venue') { return ( {location.title} {location.address} ); } if (type === 'geoLive') { return ( {lang('AttachLiveLocation')} {formatLastUpdated(lang, serverTime, message.editDate)} {!isExpired && } ); } return undefined; } function renderMap() { if (!mapBlobUrl) return ; return ( ); } function renderPin() { const pinClassName = buildClassName( 'pin', type, isExpired && 'expired', ); if (type === 'geoLive') { return ( {location.heading !== undefined && ( )} ); } if (type === 'venue') { const color = getVenueColor(location.venueType); const iconSrc = getVenueIconUrl(location.venueType); if (iconSrc) { return ( ); } } return ( ); } function renderOverlay() { if (!mapBlobUrl) return undefined; return ( <> {Boolean(accuracyRadiusPx) && !isExpired && ( )} {renderPin()} > ); } return ( {renderMap()} {renderOverlay()} {renderInfo()} ); }; export default memo(Location);