From 1de3bd3b4340cb51b2699a14a92d653db988a988 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Wed, 3 Aug 2016 00:01:33 -0400 Subject: PLT-3640 Add mobile landing pages (#3674) * PLT-3640 Moved all clientside user agent snooping into a single file * PLT-3640 Added mobile landing pages on login to iOS and Android web apps * PLT-3640 Moved landing page to appear before first login * PLT-3640 Fixed detection of Chrome on Android * PLT-3640 Disabled mobile landing pages when their respective URLs are set to blank --- webapp/components/file_upload.jsx | 5 +- .../components/get_android_app/get_android_app.jsx | 75 ++++++++++++++++ webapp/components/get_ios_app/get_ios_app.jsx | 68 +++++++++++++++ webapp/components/new_channel_modal.jsx | 5 +- .../components/post_view/components/post_list.jsx | 3 +- webapp/components/select_team/select_team.jsx | 3 +- webapp/components/settings_sidebar.jsx | 4 +- webapp/components/sidebar_right_menu.jsx | 3 +- .../user_settings/user_settings_notifications.jsx | 3 +- webapp/i18n/en.json | 9 ++ webapp/images/app-store-button.png | Bin 0 -> 11277 bytes webapp/images/iphone-6-mockup.png | Bin 0 -> 77346 bytes webapp/images/nexus-6p-mockup.png | Bin 0 -> 60764 bytes webapp/routes/route_root.jsx | 29 +++++++ webapp/sass/routes/_get-app.scss | 96 +++++++++++++++++++++ webapp/sass/routes/_module.scss | 1 + webapp/stores/browser_store.jsx | 39 +++------ webapp/utils/user_agent.jsx | 86 ++++++++++++++++++ webapp/utils/utils.jsx | 51 +---------- 19 files changed, 398 insertions(+), 82 deletions(-) create mode 100644 webapp/components/get_android_app/get_android_app.jsx create mode 100644 webapp/components/get_ios_app/get_ios_app.jsx create mode 100644 webapp/images/app-store-button.png create mode 100644 webapp/images/iphone-6-mockup.png create mode 100644 webapp/images/nexus-6p-mockup.png create mode 100644 webapp/sass/routes/_get-app.scss create mode 100644 webapp/utils/user_agent.jsx diff --git a/webapp/components/file_upload.jsx b/webapp/components/file_upload.jsx index 088e8bde7..39abec7e4 100644 --- a/webapp/components/file_upload.jsx +++ b/webapp/components/file_upload.jsx @@ -8,6 +8,7 @@ import Client from 'client/web_client.jsx'; import Constants from 'utils/constants.jsx'; import ChannelStore from 'stores/channel_store.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; +import * as UserAgent from 'utils/user_agent.jsx'; import * as Utils from 'utils/utils.jsx'; import {intlShape, injectIntl, defineMessages} from 'react-intl'; @@ -311,13 +312,13 @@ class FileUpload extends React.Component { render() { let multiple = true; - if (Utils.isMobileApp()) { + if (UserAgent.isMobileApp()) { // iOS WebViews don't upload videos properly in multiple mode multiple = false; } let accept = ''; - if (Utils.isIosChrome()) { + if (UserAgent.isIosChrome()) { // iOS Chrome can't upload videos at all accept = 'image/*'; } diff --git a/webapp/components/get_android_app/get_android_app.jsx b/webapp/components/get_android_app/get_android_app.jsx new file mode 100644 index 000000000..ab73141b1 --- /dev/null +++ b/webapp/components/get_android_app/get_android_app.jsx @@ -0,0 +1,75 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {FormattedMessage} from 'react-intl'; +import {Link} from 'react-router'; + +import MattermostIcon from 'images/favicon/android-chrome-192x192.png'; +import Nexus6Mockup from 'images/nexus-6p-mockup.png'; + +export default class GetAndroidApp extends React.Component { + render() { + return ( +
+

+ +

+
+
+ +
+ + + + + + +
+
+ + + + + + + + + ) + }} + /> + +
+ ); + } +} \ No newline at end of file diff --git a/webapp/components/get_ios_app/get_ios_app.jsx b/webapp/components/get_ios_app/get_ios_app.jsx new file mode 100644 index 000000000..0980b5882 --- /dev/null +++ b/webapp/components/get_ios_app/get_ios_app.jsx @@ -0,0 +1,68 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; + +import {FormattedMessage} from 'react-intl'; +import {Link} from 'react-router'; + +import AppStoreButton from 'images/app-store-button.png'; +import IPhone6Mockup from 'images/iphone-6-mockup.png'; + +export default class GetIosApp extends React.Component { + render() { + return ( +
+

+ +

+
+ + + + +

+ +

+ + + + + + + + ) + }} + /> + +
+ ); + } +} \ No newline at end of file diff --git a/webapp/components/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx index 1198335ca..e174ddd32 100644 --- a/webapp/components/new_channel_modal.jsx +++ b/webapp/components/new_channel_modal.jsx @@ -4,6 +4,7 @@ import $ from 'jquery'; import ReactDOM from 'react-dom'; +import * as UserAgent from 'utils/user_agent.jsx'; import * as Utils from 'utils/utils.jsx'; import Constants from 'utils/constants.jsx'; @@ -53,9 +54,11 @@ class NewChannelModal extends React.Component { } componentDidMount() { - if (Utils.isBrowserIE()) { + // ??? + if (UserAgent.isInternetExplorer()) { $('body').addClass('browser--ie'); } + PreferenceStore.addChangeListener(this.onPreferenceChange); } diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx index 9f958a5b6..befd1a10d 100644 --- a/webapp/components/post_view/components/post_list.jsx +++ b/webapp/components/post_view/components/post_list.jsx @@ -11,6 +11,7 @@ import * as GlobalActions from 'actions/global_actions.jsx'; import {createChannelIntroMessage} from 'utils/channel_intro_messages.jsx'; +import * as UserAgent from 'utils/user_agent.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; import DelayedAction from 'utils/delayed_action.jsx'; @@ -336,7 +337,7 @@ export default class PostList extends React.Component { // Temporary fix to solve ie11 rendering issue let newSeparatorId = ''; - if (!Utils.isBrowserIE()) { + if (!UserAgent.isInternetExplorer()) { newSeparatorId = 'new_message_' + post.id; } postCtls.push( diff --git a/webapp/components/select_team/select_team.jsx b/webapp/components/select_team/select_team.jsx index f1816238b..25a056954 100644 --- a/webapp/components/select_team/select_team.jsx +++ b/webapp/components/select_team/select_team.jsx @@ -3,6 +3,7 @@ import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; +import * as UserAgent from 'utils/user_agent.jsx'; import * as Utils from 'utils/utils.jsx'; import ErrorBar from 'components/error_bar.jsx'; import LoadingScreen from 'components/loading_screen.jsx'; @@ -176,7 +177,7 @@ export default class SelectTeam extends React.Component { } let teamSignUp; - if (isSystemAdmin || (global.window.mm_config.EnableTeamCreation === 'true' && !Utils.isMobileApp())) { + if (isSystemAdmin || (global.window.mm_config.EnableTeamCreation === 'true' && !UserAgent.isMobileApp())) { teamSignUp = (
{ System.import('components/login/login_controller.jsx').then(RouteUtils.importComponentSuccess(callback)); } @@ -66,6 +83,18 @@ export default { ] ) }, + { + path: 'get_ios_app', + getComponents: (location, callback) => { + System.import('components/get_ios_app/get_ios_app.jsx').then(RouteUtils.importComponentSuccess(callback)); + } + }, + { + path: 'get_android_app', + getComponents: (location, callback) => { + System.import('components/get_android_app/get_android_app.jsx').then(RouteUtils.importComponentSuccess(callback)); + } + }, { path: 'error', getComponents: (location, callback) => { diff --git a/webapp/sass/routes/_get-app.scss b/webapp/sass/routes/_get-app.scss new file mode 100644 index 000000000..88797d053 --- /dev/null +++ b/webapp/sass/routes/_get-app.scss @@ -0,0 +1,96 @@ +.get-app { + hr { + border-top: 1px solid #ddd; + } + + .get-app__header { + color: #666; + font-size: 20px; + font-weight: bold; + text-align: center + } + + .get-app__screenshot { + border-bottom: 1px solid #ddd; + display: block; + margin: auto; + } + + .get-app__continue-with-browser { + display: block; + margin-top: 40px; + text-align: center; + } +} + +.get-android-app { + margin: 20px; + + .get-app__header { + text-align: left; + } + + .get-android-app__icon { + width: 60px; + } + + .get-android-app__app-info { + display: inline-block; + margin-left: 8px; + vertical-align: middle; + + .get-android-app__app-name { + color: #666; + display: block; + font-size: 13px; + font-weight: bold; + } + + .get-android-app__app-creator { + color: #aaa; + display: block; + font-size: 10px + } + } + + .get-app__screenshot { + width: 240px; + } + + .get-android-app__continue { + display: block; + font-size: 16px; + margin-bottom: 40px; + margin-top: 15px; + padding: 12px; + } +} + +.get-ios-app { + margin: 30px; + + .get-app__screenshot { + width: 180px; + } + + .get-ios-app__app-store-link { + display: block; + margin: auto; + margin-bottom: 30px; + width: 180px; + } + + .get-ios-app__already-have-it { + font-size: 18px; + margin-bottom: 20px; + text-align: center; + } + + .get-ios-app__open-mattermost { + display: block; + font-size: 20px; + margin: auto; + padding: 12px; + width: 220px; + } +} \ No newline at end of file diff --git a/webapp/sass/routes/_module.scss b/webapp/sass/routes/_module.scss index 11b815007..c0a5b19bc 100644 --- a/webapp/sass/routes/_module.scss +++ b/webapp/sass/routes/_module.scss @@ -7,6 +7,7 @@ @import 'compliance'; @import 'docs'; @import 'error-page'; +@import 'get-app'; @import 'loading'; @import 'print'; @import 'settings'; diff --git a/webapp/stores/browser_store.jsx b/webapp/stores/browser_store.jsx index f19e5b9a1..9acd8530c 100644 --- a/webapp/stores/browser_store.jsx +++ b/webapp/stores/browser_store.jsx @@ -20,27 +20,6 @@ function getPrefix() { } class BrowserStoreClass { - constructor() { - this.getItem = this.getItem.bind(this); - this.setItem = this.setItem.bind(this); - this.removeItem = this.removeItem.bind(this); - this.setGlobalItem = this.setGlobalItem.bind(this); - this.getGlobalItem = this.getGlobalItem.bind(this); - this.removeGlobalItem = this.removeGlobalItem.bind(this); - this.actionOnItemsWithPrefix = this.actionOnItemsWithPrefix.bind(this); - this.actionOnGlobalItemsWithPrefix = this.actionOnGlobalItemsWithPrefix.bind(this); - this.isLocalStorageSupported = this.isLocalStorageSupported.bind(this); - this.getLastServerVersion = this.getLastServerVersion.bind(this); - this.setLastServerVersion = this.setLastServerVersion.bind(this); - this.clear = this.clear.bind(this); - this.clearAll = this.clearAll.bind(this); - this.checkedLocalStorageSupported = ''; - this.signalLogout = this.signalLogout.bind(this); - this.isSignallingLogout = this.isSignallingLogout.bind(this); - this.signalLogin = this.signalLogin.bind(this); - this.isSignallingLogin = this.isSignallingLogin.bind(this); - } - setItem(name, value) { this.setGlobalItem(getPrefix() + name, value); } @@ -162,9 +141,10 @@ class BrowserStoreClass { } clear() { - // don't clear the logout id so IE11 can tell which tab sent a logout request + // persist some values through logout since they're independent of which user is logged in const logoutId = sessionStorage.getItem('__logout__'); const serverVersion = this.getLastServerVersion(); + const landingPageSeen = this.hasSeenLandingPage(); sessionStorage.clear(); localStorage.clear(); @@ -176,11 +156,10 @@ class BrowserStoreClass { if (serverVersion) { this.setLastServerVersion(serverVersion); } - } - clearAll() { - sessionStorage.clear(); - localStorage.clear(); + if (landingPageSeen) { + this.setLandingPageSeen(landingPageSeen); + } } isLocalStorageSupported() { @@ -210,6 +189,14 @@ class BrowserStoreClass { return this.checkedLocalStorageSupported; } + + hasSeenLandingPage() { + return JSON.parse(sessionStorage.getItem('__landingPageSeen__')); + } + + setLandingPageSeen(landingPageSeen) { + return sessionStorage.setItem('__landingPageSeen__', JSON.stringify(landingPageSeen)); + } } var BrowserStore = new BrowserStoreClass(); diff --git a/webapp/utils/user_agent.jsx b/webapp/utils/user_agent.jsx new file mode 100644 index 000000000..657718627 --- /dev/null +++ b/webapp/utils/user_agent.jsx @@ -0,0 +1,86 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +/* +Example User Agents +-------------------- + +Chrome: + Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36 + +Firefox: + Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0 + +IE11: + Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko + +Edge: + Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586 + +Desktop App: + Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Mattermost/1.2.1 Chrome/49.0.2623.75 Electron/0.37.8 Safari/537.36 + Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586 + +Android Chrome: + Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19 + +Android App: + Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Build/KLP) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30 + Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/_BuildID_) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36 + Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48B; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/43.0.2357.65 Mobile Safari/537.36 + +iOS Safari: + Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) Version/3.0 Mobile/1A543 Safari/419.3 + +iOS Android: + Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_1_1 like Mac OS X; en) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/19.0.1084.60 Mobile/9B206 Safari/7534.48.3 + +iOS App: + Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13F69 +*/ + +const userAgent = window.navigator.userAgent; + +export function isChrome() { + return userAgent.indexOf('Chrome') > -1; +} + +export function isSafari() { + return userAgent.indexOf('Safari') !== -1 && userAgent.indexOf('Chrome') === -1; +} + +export function isIosSafari() { + return userAgent.indexOf('iPhone') !== -1 && userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('CriOS') === -1; +} + +export function isIosChrome() { + return userAgent.indexOf('CriOS') !== -1; +} + +export function isIosWeb() { + return isIosSafari() || isIosChrome(); +} + +export function isAndroidChrome() { + return userAgent.indexOf('Android') !== -1 && userAgent.indexOf('Chrome') !== -1 && userAgent.indexOf('Version') === -1; +} + +export function isAndroidWeb() { + return isAndroidChrome(); +} + +export function isMobileApp() { + return userAgent.indexOf('iPhone') !== -1 && userAgent.indexOf('Safari') === -1 && userAgent.indexOf('CriOS') === -1; +} + +export function isFirefox() { + return userAgent.indexOf('Firefox') !== -1; +} + +export function isInternetExplorer() { + return userAgent.indexOf('Trident') !== -1; +} + +export function isEdge() { + return userAgent.indexOf('Edge') !== -1; +} \ No newline at end of file diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 4b3c8518c..187c7d7f4 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -12,6 +12,7 @@ import Constants from 'utils/constants.jsx'; var ActionTypes = Constants.ActionTypes; import * as AsyncClient from './async_client.jsx'; import Client from 'client/web_client.jsx'; +import * as UserAgent from 'utils/user_agent.jsx'; import {browserHistory} from 'react-router/es6'; import {FormattedMessage} from 'react-intl'; @@ -43,31 +44,6 @@ export function cmdOrCtrlPressed(e) { return (isMac() && e.metaKey) || (!isMac() && e.ctrlKey); } -export function isChrome() { - if (navigator.userAgent.indexOf('Chrome') > -1) { - return true; - } - return false; -} - -export function isSafari() { - if (navigator.userAgent.indexOf('Safari') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) { - return true; - } - return false; -} - -export function isIosChrome() { - // https://developer.chrome.com/multidevice/user-agent - return navigator.userAgent.indexOf('CriOS') !== -1; -} - -export function isMobileApp() { - const userAgent = navigator.userAgent; - - return userAgent.indexOf('iPhone') !== -1 && userAgent.indexOf('Safari') === -1 && userAgent.indexOf('CriOS') === -1; -} - export function isInRole(roles, inRole) { var parts = roles.split(' '); for (var i = 0; i < parts.length; i++) { @@ -146,7 +122,7 @@ export function notifyMe(title, body, channel, teamId) { var canDing = true; export function ding() { - if (!isBrowserFirefox() && canDing) { + if (!UserAgent.isFirefox() && canDing) { var audio = new Audio(bing); audio.play(); canDing = false; @@ -751,7 +727,7 @@ export function updateCodeTheme(userTheme) { xmlHTTP.open('GET', cssPath, true); xmlHTTP.onload = function onLoad() { $link.attr('href', cssPath); - if (isBrowserFirefox()) { + if (UserAgent.isFirefox()) { $link.one('load', () => { changeCss('code.hljs', 'visibility: visible'); }); @@ -1048,25 +1024,6 @@ export function generateId() { return id; } -export function isBrowserFirefox() { - return navigator && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('firefox') > -1; -} - -// Checks if browser is IE10 or IE11 -export function isBrowserIE() { - if (window.navigator && window.navigator.userAgent) { - var ua = window.navigator.userAgent; - - return ua.indexOf('Trident/7.0') > 0 || ua.indexOf('Trident/6.0') > 0; - } - - return false; -} - -export function isBrowserEdge() { - return window.navigator && navigator.userAgent && navigator.userAgent.toLowerCase().indexOf('edge') > -1; -} - export function getDirectChannelName(id, otherId) { let handle; @@ -1244,7 +1201,7 @@ export function fillArray(value, length) { // Checks if a data transfer contains files not text, folders, etc.. // Slightly modified from http://stackoverflow.com/questions/6848043/how-do-i-detect-a-file-is-being-dragged-rather-than-a-draggable-element-on-my-pa export function isFileTransfer(files) { - if (isBrowserIE() || isBrowserEdge()) { + if (UserAgent.isInternetExplorer() || UserAgent.isEdge()) { return files.types != null && files.types.contains('Files'); } -- cgit v1.2.3-1-g7c22