import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from '../../../lib/teact/teact'; 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 { CUSTOM_APPENDIX_ATTRIBUTE, MESSAGE_CONTENT_SELECTOR } from '../../../config'; import { getMessageLocation, buildStaticMapHash, isGeoLiveExpired, isOwnMessage, isUserId, } from '../../../global/helpers'; import getCustomAppendixBg from './helpers/getCustomAppendixBg'; import { formatCountdownShort, formatLastUpdated } from '../../../util/dateFormat'; import { getMetersPerPixel, getVenueColor, getVenueIconUrl, prepareMapUrl, } from '../../../util/map'; import { getServerTime } from '../../../util/serverTime'; 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 useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevDeps'; import Avatar from '../../common/Avatar'; import Skeleton from '../../ui/Skeleton'; import mapPin from '../../../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: '' }; type OwnProps = { message: ApiMessage; peer?: ApiUser | ApiChat; lastSyncTime?: number; isInSelectMode?: boolean; isSelected?: boolean; theme: ISettings['theme']; }; const Location: FC = ({ message, peer, lastSyncTime, isInSelectMode, isSelected, theme, }) => { 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 = 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); openUrl({ url }); }; 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(); 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}`); } }, [type, message.date, location, lang]); useLayoutEffect(() => { if (countdownRef.current) { updateCountdown(countdownRef.current); } }, [updateCountdown]); useLayoutEffectWithPrevDeps(([prevShouldRenderText]) => { if (shouldRenderText) { if (!prevShouldRenderText) { ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!.removeAttribute(CUSTOM_APPENDIX_ATTRIBUTE); } return; } if (mapBlobUrl) { const contentEl = ref.current!.closest(MESSAGE_CONTENT_SELECTOR)!; 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, ''); }); } }, [shouldRenderText, isOwn, isInSelectMode, isSelected, theme, mapBlobUrl]); 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 (
{location.title}
{location.address}
); } if (type === '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', type, isExpired && 'expired', ); if (type === 'geoLive') { return (
{location.heading !== undefined && (
)}
); } if (type === 'venue') { const color = getVenueColor(location.venueType); const iconSrc = getVenueIconUrl(location.venueType); return (
); } return ( ); } function renderOverlay() { if (!mapBlobUrl) return undefined; return ( <> {Boolean(accuracyRadiusPx) && !isExpired && (
)} {renderPin()} ); } return (
{renderMap()} {renderOverlay()}
{renderInfo()}
); }; export default memo(Location);