diff --git a/app/assets/javascripts/initializers/initializeCommentDropdown.js b/app/assets/javascripts/initializers/initializeCommentDropdown.js index c620aafb3..e946982d2 100644 --- a/app/assets/javascripts/initializers/initializeCommentDropdown.js +++ b/app/assets/javascripts/initializers/initializeCommentDropdown.js @@ -1,3 +1,5 @@ +/* global Runtime */ + function initializeCommentDropdown() { const announcer = document.getElementById('article-copy-link-announcer'); @@ -7,14 +9,6 @@ function initializeCommentDropdown() { ); } - function isNativeAndroidDevice() { - return ( - navigator.userAgent === 'DEV-Native-android' && - typeof AndroidBridge !== 'undefined' && - AndroidBridge !== null - ); - } - function removeClass(className) { return (element) => element.classList.remove(className); } @@ -47,7 +41,7 @@ function initializeCommentDropdown() { function copyText() { const inputValue = document.getElementById('article-copy-link-input').value; - if (isNativeAndroidDevice()) { + if (Runtime.isNativeAndroid('copyToClipboard')) { AndroidBridge.copyToClipboard(inputValue); showAnnouncer(); } else if (isClipboardSupported()) { @@ -113,6 +107,13 @@ function initializeCommentDropdown() { return; } + // Android native apps have enhanced sharing capabilities for Articles + const articleShowMoreClicked = button.id === 'article-show-more-button'; + if (articleShowMoreClicked && Runtime.isNativeAndroid('shareText')) { + AndroidBridge.shareText(location.href); + return; + } + finalizeAbuseReportLink( dropdownContent.querySelector('.report-abuse-link-wrapper'), ); diff --git a/app/assets/javascripts/initializers/initializePodcastPlayback.js b/app/assets/javascripts/initializers/initializePodcastPlayback.js index 226f9749d..965a5a050 100644 --- a/app/assets/javascripts/initializers/initializePodcastPlayback.js +++ b/app/assets/javascripts/initializers/initializePodcastPlayback.js @@ -22,12 +22,15 @@ * the problem of using a method before it's defined: */ +/* global ahoy, Runtime */ /* eslint no-use-before-define: 0 */ /* eslint no-param-reassign: 0 */ var audioInitialized = false; function initializePodcastPlayback() { + var deviceType = 'web'; + function getById(name) { return document.getElementById(name); } @@ -100,41 +103,6 @@ function initializePodcastPlayback() { return podcastLiquidTagrecords; } - function isNativePlayer() { - return isNativeIOS() || isNativeAndroid(); - } - - function isNativeIOS() { - return ( - navigator.userAgent === 'DEV-Native-ios' && - window && - window.webkit && - window.webkit.messageHandlers && - window.webkit.messageHandlers.podcast - ); - } - - function isNativeAndroid() { - return ( - navigator.userAgent === 'DEV-Native-android' && - typeof AndroidBridge !== 'undefined' && - AndroidBridge !== null && - AndroidBridge.podcastMessage !== undefined - ); - } - - function sendNativeMessage(message) { - try { - if (isNativeIOS()) { - window.webkit.messageHandlers.podcast.postMessage(message); - } else if (isNativeAndroid()) { - AndroidBridge.podcastMessage(JSON.stringify(message)); - } - } catch (err) { - console.log(err.message); // eslint-disable-line no-console - } - } - function saveMediaState(state) { var currentState = state || currentAudioState(); var newState = newAudioState(); @@ -189,8 +157,8 @@ function initializePodcastPlayback() { } function loadAudio(audio) { - if (isNativePlayer()) { - sendNativeMessage({ + if (Runtime.podcastMessage) { + Runtime.podcastMessage({ action: 'load', url: audio.querySelector('source').src, }); @@ -244,8 +212,8 @@ function initializePodcastPlayback() { } saveMediaState(currentState); - if (isNativePlayer()) { - sendNativeMessage({ + if (Runtime.podcastMessage) { + Runtime.podcastMessage({ action: 'rate', rate: currentState.playbackRate.toString(), }); @@ -285,8 +253,8 @@ function initializePodcastPlayback() { function playAudio(audio) { return new Promise(function (resolve, reject) { var currentState = currentAudioState(); - if (isNativePlayer()) { - sendNativeMessage({ + if (Runtime.podcastMessage) { + Runtime.podcastMessage({ action: 'play', url: audio.querySelector('source').src, seconds: currentState.currentTime.toString(), @@ -319,23 +287,23 @@ function initializePodcastPlayback() { } function sendMetadataMessage() { - try { - var metadata = JSON.parse(fetchMetadataString()); - sendNativeMessage({ - action: 'metadata', - episodeName: metadata.episodeName, - podcastName: metadata.podcastName, - podcastImageUrl: metadata.podcastImageUrl, - }); - } catch (e) { - console.log('Unable to load Podcast Episode metadata', e); // eslint-disable-line no-console + if (Runtime.podcastMessage) { + try { + var metadata = JSON.parse(fetchMetadataString()); + Runtime.podcastMessage({ + action: 'metadata', + episodeName: metadata.episodeName, + podcastName: metadata.podcastName, + podcastImageUrl: metadata.podcastImageUrl, + }); + } catch (e) { + console.log('Unable to load Podcast Episode metadata', e); // eslint-disable-line no-console + } } } function startAudioPlayback(audio) { - if (isNativePlayer()) { - sendMetadataMessage(); - } + sendMetadataMessage(); playAudio(audio) .then(function () { @@ -352,8 +320,8 @@ function initializePodcastPlayback() { } function pauseAudioPlayback(audio) { - if (isNativePlayer()) { - sendNativeMessage({ action: 'pause' }); + if (Runtime.podcastMessage) { + Runtime.podcastMessage({ action: 'pause' }); } else { audio.pause(); } @@ -367,29 +335,21 @@ function initializePodcastPlayback() { window.activePodcast = audio.getAttribute('data-podcast'); var currentState = currentAudioState(); + var properties = { + episode: window.activeEpisode, + podcast: window.activePodcast, + deviceType: deviceType, + }; if (!currentState.playing) { - ga( - 'send', - 'event', - 'click', - 'play podcast', - `${window.activePodcast} ${window.activeEpisode}`, - null, - ); + properties.action = 'play'; changeStatusMessage('initializing...'); startAudioPlayback(audio); } else { - ga( - 'send', - 'event', - 'click', - 'pause podcast', - `${window.activePodcast} ${window.activeEpisode}`, - null, - ); + properties.action = 'pause'; pauseAudioPlayback(audio); changeStatusMessage(null); } + ahoy.track('Podcast Player Streaming', properties); } function muteUnmute(audio) { @@ -408,8 +368,8 @@ function initializePodcastPlayback() { ); currentState.muted = !currentState.muted; - if (isNativePlayer()) { - sendNativeMessage({ + if (Runtime.podcastMessage) { + Runtime.podcastMessage({ action: 'muted', muted: currentState.muted.toString(), }); @@ -422,8 +382,8 @@ function initializePodcastPlayback() { function updateVolume(e, audio) { var currentState = currentAudioState(); currentState.volume = e.target.value / 100; - if (isNativePlayer()) { - sendNativeMessage({ action: 'volume', volume: currentState.volume }); + if (Runtime.podcastMessage) { + Runtime.podcastMessage({ action: 'volume', volume: currentState.volume }); } else { audio.volume = currentState.volume; } @@ -463,8 +423,8 @@ function initializePodcastPlayback() { var duration = currentState.duration; currentState.currentTime = duration * percent; // jumps to 29th secs - if (isNativePlayer()) { - sendNativeMessage({ + if (Runtime.podcastMessage) { + Runtime.podcastMessage({ action: 'seek', seconds: currentState.currentTime.toString(), }); @@ -498,8 +458,8 @@ function initializePodcastPlayback() { getById('audiocontent').innerHTML = ''; stopRotatingActivePodcastIfExist(); saveMediaState(newAudioState()); - if (isNativePlayer()) { - sendNativeMessage({ action: 'terminate' }); + if (Runtime.podcastMessage) { + Runtime.podcastMessage({ action: 'terminate' }); } } @@ -540,6 +500,29 @@ function initializePodcastPlayback() { mutationObserver.observe(getById('audiocontent'), { attributes: true }); } + // When Runtime.podcastMessage is undefined we need to execute web logic + function initRuntime() { + if (Runtime.isNativeIOS('podcast')) { + deviceType = 'iOS'; + Runtime.podcastMessage = function (message) { + try { + window.webkit.messageHandlers.podcast.postMessage(message); + } catch (err) { + console.log(err.message); // eslint-disable-line no-console + } + }; + } else if (Runtime.isNativeAndroid('podcastMessage')) { + deviceType = 'Android'; + Runtime.podcastMessage = function (message) { + try { + AndroidBridge.podcastMessage(JSON.stringify(message)); + } catch (err) { + console.log(err.message); // eslint-disable-line no-console + } + }; + } + } + function initializeMedia() { var currentState = currentAudioState(); document.getElementById('audiocontent').innerHTML = currentState.html; @@ -548,7 +531,7 @@ function initializePodcastPlayback() { audioInitialized = false; return; } - if (!isNativeIOS()) { + if (Runtime.podcastMessage) { audio.currentTime = currentState.currentTime || 0; } loadAudio(audio); @@ -574,6 +557,7 @@ function initializePodcastPlayback() { saveMediaState(currentState); } + initRuntime(); spinPodcastRecord(); findAndApplyOnclickToRecords(); if (!audioInitialized) { diff --git a/app/assets/javascripts/initializers/initializeVideoPlayback.js b/app/assets/javascripts/initializers/initializeVideoPlayback.js index d3bcc7a70..211bb6fd4 100644 --- a/app/assets/javascripts/initializers/initializeVideoPlayback.js +++ b/app/assets/javascripts/initializers/initializeVideoPlayback.js @@ -12,11 +12,9 @@ /* eslint no-use-before-define: 0 */ /* eslint no-param-reassign: 0 */ /* eslint no-useless-escape: 0 */ -/* global jwplayer */ -/* global ahoy */ +/* global jwplayer, ahoy, Runtime */ function initializeVideoPlayback() { - var nativeBridgeMessage; var currentTime = '0'; var deviceType = 'web'; var lastEvent = ''; @@ -25,25 +23,6 @@ function initializeVideoPlayback() { return document.getElementById(name); } - function isNativeIOS() { - return ( - navigator.userAgent === 'DEV-Native-ios' && - window && - window.webkit && - window.webkit.messageHandlers && - window.webkit.messageHandlers.video - ); - } - - function isNativeAndroid() { - return ( - navigator.userAgent === 'DEV-Native-android' && - typeof AndroidBridge !== 'undefined' && - AndroidBridge !== null && - AndroidBridge.videoMessage !== undefined - ); - } - function getParameterByName(name, url) { if (!url) url = window.location.href; name = name.replace(/[\[\]]/g, '\\$&'); @@ -138,7 +117,7 @@ function initializeVideoPlayback() { getById('pause-butt').classList.add('active'); getById('play-butt').classList.remove('active'); - nativeBridgeMessage({ + Runtime.videoMessage({ action: 'play', url: metadata.video_source_url, seconds: currentTime, @@ -174,18 +153,18 @@ function initializeVideoPlayback() { var seconds = timeToSeconds(getParameterByName('t') || '0'); var metadata = videoMetadata(videoSource); - if (isNativeIOS()) { + if (Runtime.isNativeIOS('video')) { deviceType = 'iOS'; - nativeBridgeMessage = function (message) { + Runtime.videoMessage = function (message) { try { window.webkit.messageHandlers.video.postMessage(message); } catch (err) { console.log(err.message); // eslint-disable-line no-console } }; - } else if (isNativeAndroid()) { + } else if (Runtime.isNativeAndroid('videoMessage')) { deviceType = 'Android'; - nativeBridgeMessage = function (message) { + Runtime.videoMessage = function (message) { try { AndroidBridge.videoMessage(JSON.stringify(message)); } catch (err) { diff --git a/app/assets/javascripts/initializers/runtime.js b/app/assets/javascripts/initializers/runtime.js new file mode 100644 index 000000000..1027dfdd1 --- /dev/null +++ b/app/assets/javascripts/initializers/runtime.js @@ -0,0 +1,54 @@ +/** + * This class helps managing native feature support. Can easily be referenced + * from anywhere in JavaScript with: + * + * if (Runtime.isNativeiOS('video')) { ... } + * + * if (Runtime.isNativeAndroid('podcastMessage')) { ... } + */ +class Runtime { + /** + * Checks the device for iOS (webkit) native feature support + * + * @function isNativeIOS + * @param {string} namespace Specifies support for a specific feature + * (i.e. video, podcast, etc) + * @returns {boolean} true if current environment support native features + */ + static isNativeIOS(namespace = null) { + const nativeCheck = + /DEV-Native-ios|forem-native-ios/i.test(navigator.userAgent) && + navigator.userAgent === '' && + window && + window.webkit && + window.webkit.messageHandlers; + + let namespaceCheck = true; + if (nativeCheck && namespace) { + namespaceCheck = window.webkit.messageHandlers[namespace] != undefined; + } + + return nativeCheck && namespaceCheck; + } + + /** + * Checks the device for Android native feature support + * + * @function isNativeAndroid + * @param {string} namespace Specifies support for a specific feature + * (i.e. videoMessage, podcastMessage, etc) + * @returns {boolean} true if current environment support native features + */ + static isNativeAndroid(namespace = null) { + const nativeCheck = + /DEV-Native-android|forem-native-android/i.test(navigator.userAgent) && + AndroidBridge != undefined; + + let namespaceCheck = true; + if (nativeCheck && namespace) { + namespaceCheck = AndroidBridge[namespace] != undefined; + } + + return nativeCheck && namespaceCheck; + } +} diff --git a/app/javascript/article-form/components/ImageUploader.jsx b/app/javascript/article-form/components/ImageUploader.jsx index 3281f7995..d24313a66 100644 --- a/app/javascript/article-form/components/ImageUploader.jsx +++ b/app/javascript/article-form/components/ImageUploader.jsx @@ -1,3 +1,5 @@ +/* global Runtime */ + import { h } from 'preact'; import { useReducer } from 'preact/hooks'; import { generateMainImage } from '../actions'; @@ -5,14 +7,6 @@ import { validateFileInputs } from '../../packs/validateFileInputs'; import { ClipboardButton } from './ClipboardButton'; import { Button, Spinner } from '@crayons'; -function isNativeAndroid() { - return ( - navigator.userAgent === 'DEV-Native-android' && - typeof AndroidBridge !== 'undefined' && - AndroidBridge !== null - ); -} - function isClipboardSupported() { return ( typeof navigator.clipboard !== 'undefined' && navigator.clipboard !== null @@ -108,7 +102,7 @@ export const ImageUploader = () => { 'image-markdown-copy-link-input', ); - if (isNativeAndroid()) { + if (Runtime.isNativeAndroid('copyToClipboard')) { AndroidBridge.copyToClipboard(imageMarkdownInput.value); dispatch({ type: 'show_copied_image_message', diff --git a/app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx b/app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx index 0ec9b9a42..7ecc35e30 100644 --- a/app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx +++ b/app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx @@ -1,6 +1,7 @@ +/* global Runtime */ + import { h, Component } from 'preact'; import PropTypes from 'prop-types'; -import { isNativeAndroid } from '../../../utilities/validateAndroidNative'; import { Button } from '@crayons'; @@ -46,7 +47,7 @@ export default class InvitationLinkManager extends Component { 'chat-channel-unviation-url', ); - if (isNativeAndroid()) { + if (Runtime.isNativeAndroid('copyToClipboard')) { AndroidBridge.copyToClipboard(this.imageMarkdownInput.value); this.setState({ showImageCopiedMessage: true }); } else if (isClipboardSupported()) { diff --git a/app/javascript/utilities/validateAndroidNative.js b/app/javascript/utilities/validateAndroidNative.js deleted file mode 100644 index d660825a6..000000000 --- a/app/javascript/utilities/validateAndroidNative.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * This function is used to check that is Device is android native or not - */ -export function isNativeAndroid() { - return ( - navigator.userAgent === 'DEV-Native-android' && - typeof AndroidBridge !== 'undefined' && - AndroidBridge !== null - ); -}