import type { FC } from '../../../lib/teact/teact'; import React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; import type { ApiMessage, ApiPeer } from '../../../api/types'; import type { ISettings } from '../../../types'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { buildStaticMapHash, getMessageLocation, isGeoLiveExpired, } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import { formatCountdownShort, formatLastUpdated } from '../../../util/dates/dateFormat'; import { getMetersPerPixel, getVenueColor, getVenueIconUrl, } from '../../../util/map'; import { getServerTime } from '../../../util/serverTime'; import useInterval from '../../../hooks/schedulers/useInterval'; import useTimeout from '../../../hooks/schedulers/useTimeout'; import useForceUpdate from '../../../hooks/useForceUpdate'; import useLastCallback from '../../../hooks/useLastCallback'; import useMedia from '../../../hooks/useMedia'; import useOldLang from '../../../hooks/useOldLang'; import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated'; import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio'; import Avatar from '../../common/Avatar'; import Skeleton from '../../ui/placeholder/Skeleton'; import './Location.scss'; import mapPin from '../../../assets/map-pin.svg'; const TIMER_RADIUS = 12; const TIMER_CIRCUMFERENCE = TIMER_RADIUS * 2 * Math.PI; const MOVE_THRESHOLD = 0.0001; // ~11m const DEFAULT_MAP_CONFIG = { width: 400, height: 300, zoom: 16, }; type OwnProps = { message: ApiMessage; peer?: ApiPeer; isInSelectMode?: boolean; isSelected?: boolean; theme: ISettings['theme']; }; const Location: FC = ({ message, peer, }) => { const { openMapModal } = 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 = useOldLang(); const forceUpdate = useForceUpdate(); const location = getMessageLocation(message)!; const { mediaType, geo } = location; const serverTime = getServerTime(); const isExpired = isGeoLiveExpired(message); const secondsBeforeEnd = (mediaType === 'geoLive' && !isExpired) ? message.date + location.period - serverTime : undefined; const [point, setPoint] = useState(geo); const shouldRenderText = mediaType === 'venue' || (mediaType === 'geoLive' && !isExpired); const { width, height, zoom } = DEFAULT_MAP_CONFIG; const dpr = useDevicePixelRatio(); const mediaHash = buildStaticMapHash(point, width, height, zoom, dpr); const mediaBlobUrl = useMedia(mediaHash); const prevMediaBlobUrl = usePreviousDeprecated(mediaBlobUrl, true); const mapBlobUrl = mediaBlobUrl || prevMediaBlobUrl; const accuracyRadiusPx = useMemo(() => { if (mediaType !== 'geoLive' || !point.accuracyRadius) { return 0; } const { lat, accuracyRadius } = point; return accuracyRadius / getMetersPerPixel(lat, zoom); }, [mediaType, point, zoom]); const handleClick = () => { openMapModal({ geoPoint: point, zoom }); }; const updateCountdown = useLastCallback((countdownEl: HTMLDivElement) => { if (mediaType !== 'geoLive') return; const svgEl = countdownEl.lastElementChild!; const timerEl = countdownEl.firstElementChild!; const timeLeft = message.date + location.period - getServerTime(); const strokeDashOffset = (1 - timeLeft / location.period) * TIMER_CIRCUMFERENCE; const text = formatCountdownShort(lang, timeLeft * 1000); 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 (mediaType === 'venue') { return (
{location.title}
{location.address}
); } if (mediaType === 'geoLive') { return (
{lang('AttachLiveLocation')}
{formatLastUpdated(lang, serverTime, message.editDate)}
{!isExpired && (
)}
); } return undefined; } function renderMap() { if (!mapBlobUrl) return ; return ( Location on a map ); } function renderPin() { const pinClassName = buildClassName( 'pin', mediaType, isExpired && 'expired', ); if (mediaType === 'geoLive') { return (
{location.heading !== undefined && (
)}
); } if (mediaType === '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()}
); }; function PinSvg() { return ( ); } export default memo(Location);