/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ import { connect } from "react-redux"; import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu"; import { LocationSearch } from "content-src/components/Weather/LocationSearch"; import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; import { useIntersectionObserver } from "../../lib/utils"; import React, { useState } from "react"; const VISIBLE = "visible"; const VISIBILITY_CHANGE_EVENT = "visibilitychange"; function WeatherPlaceholder() { const [isSeen, setIsSeen] = useState(false); // We are setting up a visibility and intersection event // so animations don't happen with headless automation. // The animations causes tests to fail beause they never stop, // and many tests wait until everything has stopped before passing. const ref = useIntersectionObserver(() => setIsSeen(true), 1); const isSeenClassName = isSeen ? `placeholder-seen` : ``; return (
{ ref.current = [el]; }} >
); } export class _Weather extends React.PureComponent { constructor(props) { super(props); this.state = { contextMenuKeyboard: false, showContextMenu: false, url: "https://example.com", impressionSeen: false, errorSeen: false, }; this.setImpressionRef = element => { this.impressionElement = element; }; this.setErrorRef = element => { this.errorElement = element; }; this.onClick = this.onClick.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.onUpdate = this.onUpdate.bind(this); this.onProviderClick = this.onProviderClick.bind(this); } componentDidMount() { const { props } = this; if (!props.dispatch) { return; } if (props.document.visibilityState === VISIBLE) { // Setup the impression observer once the page is visible. this.setImpressionObservers(); } else { // We should only ever send the latest impression stats ping, so remove any // older listeners. if (this._onVisibilityChange) { props.document.removeEventListener( VISIBILITY_CHANGE_EVENT, this._onVisibilityChange ); } this._onVisibilityChange = () => { if (props.document.visibilityState === VISIBLE) { // Setup the impression observer once the page is visible. this.setImpressionObservers(); props.document.removeEventListener( VISIBILITY_CHANGE_EVENT, this._onVisibilityChange ); } }; props.document.addEventListener( VISIBILITY_CHANGE_EVENT, this._onVisibilityChange ); } } componentWillUnmount() { // Remove observers on unmount if (this.observer && this.impressionElement) { this.observer.unobserve(this.impressionElement); } if (this.observer && this.errorElement) { this.observer.unobserve(this.errorElement); } if (this._onVisibilityChange) { this.props.document.removeEventListener( VISIBILITY_CHANGE_EVENT, this._onVisibilityChange ); } } setImpressionObservers() { if (this.impressionElement) { this.observer = new IntersectionObserver(this.onImpression.bind(this)); this.observer.observe(this.impressionElement); } if (this.errorElement) { this.observer = new IntersectionObserver(this.onError.bind(this)); this.observer.observe(this.errorElement); } } onImpression(entries) { if (this.state) { const entry = entries.find(e => e.isIntersecting); if (entry) { if (this.impressionElement) { this.observer.unobserve(this.impressionElement); } this.props.dispatch( ac.OnlyToMain({ type: at.WEATHER_IMPRESSION, }) ); // Stop observing since element has been seen this.setState({ impressionSeen: true, }); } } } onError(entries) { if (this.state) { const entry = entries.find(e => e.isIntersecting); if (entry) { if (this.errorElement) { this.observer.unobserve(this.errorElement); } this.props.dispatch( ac.OnlyToMain({ type: at.WEATHER_LOAD_ERROR, }) ); // Stop observing since element has been seen this.setState({ errorSeen: true, }); } } } openContextMenu(isKeyBoard) { if (this.props.onUpdate) { this.props.onUpdate(true); } this.setState({ showContextMenu: true, contextMenuKeyboard: isKeyBoard, }); } onClick(event) { event.preventDefault(); this.openContextMenu(false, event); } onKeyDown(event) { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); this.openContextMenu(true, event); } } onUpdate(showContextMenu) { if (this.props.onUpdate) { this.props.onUpdate(showContextMenu); } this.setState({ showContextMenu }); } onProviderClick() { this.props.dispatch( ac.OnlyToMain({ type: at.WEATHER_OPEN_PROVIDER_URL, data: { source: "WEATHER", }, }) ); } render() { // Check if weather should be rendered const isWeatherEnabled = this.props.Prefs.values["system.showWeather"]; if (!isWeatherEnabled) { return false; } if (!this.props.Weather.initialized) { return ; } const { showContextMenu } = this.state; const { props } = this; const { dispatch, Prefs, Weather } = props; const WEATHER_SUGGESTION = Weather.suggestions?.[0]; const outerClassName = ["weather", Weather.searchActive && "search"] .filter(v => v) .join(" "); const showDetailedView = Prefs.values["weather.display"] === "detailed"; // Note: The temperature units/display options will become secondary menu items const WEATHER_SOURCE_CONTEXT_MENU_OPTIONS = [ ...(Prefs.values["weather.locationSearchEnabled"] ? ["ChangeWeatherLocation"] : []), ...(Prefs.values["weather.temperatureUnits"] === "f" ? ["ChangeTempUnitCelsius"] : ["ChangeTempUnitFahrenheit"]), ...(Prefs.values["weather.display"] === "simple" ? ["ChangeWeatherDisplayDetailed"] : ["ChangeWeatherDisplaySimple"]), "HideWeather", "OpenLearnMoreURL", ]; const WEATHER_SOURCE_ERROR_CONTEXT_MENU_OPTIONS = [ ...(Prefs.values["weather.locationSearchEnabled"] ? ["ChangeWeatherLocation"] : []), "HideWeather", "OpenLearnMoreURL", ]; const contextMenu = contextOpts => (
); if (Weather.searchActive) { return ; } else if (WEATHER_SUGGESTION) { return ( ); } return (
{" "}

{contextMenu(WEATHER_SOURCE_ERROR_CONTEXT_MENU_OPTIONS)}
); } } export const Weather = connect(state => ({ Weather: state.Weather, Prefs: state.Prefs, IntersectionObserver: globalThis.IntersectionObserver, document: globalThis.document, }))(_Weather);