diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index d24a4559a..f20148e66 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html @@ -1,7 +1,7 @@ <div *ngIf="mobileMsg" class="mobile-msg"> <div class="msg ellipsis me-auto" i18n>Open in the application?</div> - <a class="peertube-button-link secondary-button me-3" [href]="mobileAppUrl">Open</a> + <a class="peertube-button-link secondary-button me-3" [href]="androidAppUrl || iosAppUrl" (click)="onOpenClientClick()">Open</a> <button class="border-0 p-0" title="Close this message" i18n-title (click)="hideMobileMsg()"> <my-global-icon iconName="cross"></my-global-icon> diff --git a/client/src/app/header/header.component.ts b/client/src/app/header/header.component.ts index 990a512a1..54cc83114 100644 --- a/client/src/app/header/header.component.ts +++ b/client/src/app/header/header.component.ts @@ -22,7 +22,7 @@ import { SignupLabelComponent } from '@app/shared/shared-main/users/signup-label import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { ServerConfig } from '@peertube/peertube-models' import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' -import { isAndroid } from '@root-helpers/web-browser' +import { isAndroid, isIOS, isIphone } from '@root-helpers/web-browser' import { Subscription } from 'rxjs' import { GlobalIconComponent } from '../shared/shared-icons/global-icon.component' import { ButtonComponent } from '../shared/shared-main/buttons/button.component' @@ -66,7 +66,8 @@ export class HeaderComponent implements OnInit, OnDestroy { currentInterfaceLanguage: string mobileMsg = false - mobileAppUrl = '' + androidAppUrl = '' + iosAppUrl = '' private serverConfig: ServerConfig @@ -159,12 +160,15 @@ export class HeaderComponent implements OnInit, OnDestroy { private setupMobileMsg () { if (!this.isInMobileView()) return if (peertubeLocalStorage.getItem(HeaderComponent.LS_HIDE_MOBILE_MSG) === 'true') return - if (!isAndroid()) return + + if (!isAndroid() && !isIphone()) return this.mobileMsg = true document.body.classList.add('mobile-app-msg') const host = window.location.host + const intentConfig = this.serverConfig.client.openInApp.android.intent + const iosConfig = this.serverConfig.client.openInApp.ios const getVideoId = (url: string) => { const matches = url.match(/^\/w\/([^/]+)$/) @@ -183,19 +187,39 @@ export class HeaderComponent implements OnInit, OnDestroy { const url = event.url + const baseAndroid = `intent://${intentConfig.host}` + const fallbackAndroid = `#Intent;scheme=${intentConfig.scheme};S.browser_fallback_url=${intentConfig.fallbackUrl};end` + + const baseIOS = `peertube://${iosConfig.host}` + const videoId = getVideoId(url) - if (videoId) { - this.mobileAppUrl = `peertube://joinpeertube.org/video/${videoId}?host=${host}` - return - } - const channelId = getChannelId(url) - if (channelId) { - this.mobileAppUrl = `peertube://joinpeertube.org/video-channel/${channelId}?host=${host}` + + if (videoId) { + if (isAndroid()) { + this.androidAppUrl = `${baseAndroid}/video/${videoId}?host=${host}${fallbackAndroid}` + } else { + this.iosAppUrl = `${baseIOS}/video/${videoId}?host=${host}` + } + return } - this.mobileAppUrl = `peertube://joinpeertube.org/?host=${host}` + if (channelId) { + if (isAndroid()) { + this.androidAppUrl = `${baseAndroid}/video-channel/${channelId}?host=${host}${fallbackAndroid}` + } else { + this.iosAppUrl = `${baseIOS}/video/${videoId}?host=${host}` + } + + return + } + + if (isAndroid()) { + this.androidAppUrl = `${baseAndroid}/?host=${host}${fallbackAndroid}` + } else { + this.iosAppUrl = `${baseIOS}/?host=${host}` + } }) } @@ -206,6 +230,14 @@ export class HeaderComponent implements OnInit, OnDestroy { peertubeLocalStorage.setItem(HeaderComponent.LS_HIDE_MOBILE_MSG, 'true') } + onOpenClientClick () { + if (!isIOS()) return + + setTimeout(() => { + window.location.href = this.serverConfig.client.openInApp.ios.fallbackUrl + }, 2500) + } + // --------------------------------------------------------------------------- isRegistrationAllowed () { diff --git a/client/src/app/shared/shared-user-subscription/subscribe-button.component.ts b/client/src/app/shared/shared-user-subscription/subscribe-button.component.ts index 2d8ca65d7..6f6af1976 100644 --- a/client/src/app/shared/shared-user-subscription/subscribe-button.component.ts +++ b/client/src/app/shared/shared-user-subscription/subscribe-button.component.ts @@ -1,12 +1,11 @@ import { NgClass, NgIf, NgTemplateOutlet } from '@angular/common' import { Component, Input, OnChanges, ViewChild } from '@angular/core' import { AuthService, Notifier, RedirectService } from '@app/core' -import { NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap' +import { NgbDropdown, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap' import { FeedFormat } from '@peertube/peertube-models' import { concat, forkJoin, merge } from 'rxjs' import { Account } from '../shared-main/account/account.model' import { VideoChannel } from '../shared-main/channel/video-channel.model' -import { NumberFormatterPipe } from '../shared-main/common/number-formatter.pipe' import { VideoService } from '../shared-main/video/video.service' import { RemoteSubscribeComponent } from './remote-subscribe.component' import { UserSubscriptionService } from './user-subscription.service' @@ -22,9 +21,7 @@ import { UserSubscriptionService } from './user-subscription.service' NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, - NgbDropdownItem, - RemoteSubscribeComponent, - NumberFormatterPipe + RemoteSubscribeComponent ] }) export class SubscribeButtonComponent implements OnChanges { diff --git a/config/default.yaml b/config/default.yaml index bbb023cac..6e5dae6f6 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -1077,6 +1077,29 @@ client: # You can automatically redirect your users on this external platform when they click on the login button redirect_on_single_external_auth: false + open_in_app: + android: + # Use an intent URL: https://developer.chrome.com/docs/android/intents + intent: + enabled: true + # Host registered by the mobile app + host: 'joinpeertube.org' + # Scheme registered by the mobile app + scheme: 'peertube' + # If not having the app on the mobile device, open this page + # F-Droid alternative: https://f-droid.org/packages/org.framasoft.peertube/ + fallback_url: 'https://play.google.com/store/apps/details?id=org.framasoft.peertube' + + ios: + # We use a timeout for iOS: if the app is not opened after a few seconds, open the fallback URL + enabled: true + # Host registered by the mobile app + host: 'joinpeertube.org' + # Scheme registered by the mobile app + scheme: 'peertube' + # If not having the app on the mobile device, open this page + fallback_url: 'https://apps.apple.com/fr/app/peertube/id6737834858' + storyboards: # Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video enabled: true diff --git a/config/production.yaml.example b/config/production.yaml.example index 97e682c2a..e2fd77185 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -1087,6 +1087,29 @@ client: # You can automatically redirect your users on this external platform when they click on the login button redirect_on_single_external_auth: false + open_in_app: + android: + # Use an intent URL: https://developer.chrome.com/docs/android/intents + intent: + enabled: true + # Host registered by the mobile app + host: 'joinpeertube.org' + # Scheme registered by the mobile app + scheme: 'peertube' + # If not having the app on the mobile device, open this page + # F-Droid alternative: https://f-droid.org/packages/org.framasoft.peertube/ + fallback_url: 'https://play.google.com/store/apps/details?id=org.framasoft.peertube' + + ios: + # We use a timeout for iOS: if the app is not opened after a few seconds, open the fallback URL + enabled: true + # Host registered by the mobile app + host: 'joinpeertube.org' + # Scheme registered by the mobile app + scheme: 'peertube' + # If not having the app on the mobile device, open this page + fallback_url: 'https://apps.apple.com/fr/app/peertube/id6737834858' + storyboards: # Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video enabled: true diff --git a/packages/models/src/server/server-config.model.ts b/packages/models/src/server/server-config.model.ts index 3060d711f..1e3108774 100644 --- a/packages/models/src/server/server-config.model.ts +++ b/packages/models/src/server/server-config.model.ts @@ -51,6 +51,24 @@ export interface ServerConfig { redirectOnSingleExternalAuth: boolean } } + + openInApp: { + android: { + intent: { + enabled: boolean + host: string + scheme: string + fallbackUrl: string + } + } + + ios: { + enabled: boolean + host: string + scheme: string + fallbackUrl: string + } + } } defaults: { diff --git a/server/core/initializers/config.ts b/server/core/initializers/config.ts index 7c016fb65..0976060c1 100644 --- a/server/core/initializers/config.ts +++ b/server/core/initializers/config.ts @@ -87,6 +87,22 @@ const CONFIG = { LOGIN: { get REDIRECT_ON_SINGLE_EXTERNAL_AUTH () { return config.get<boolean>('client.menu.login.redirect_on_single_external_auth') } } + }, + OPEN_IN_APP: { + ANDROID: { + INTENT: { + get ENABLED () { return config.get<boolean>('client.open_in_app.android.intent.enabled') }, + get HOST () { return config.get<string>('client.open_in_app.android.intent.host') }, + get SCHEME () { return config.get<string>('client.open_in_app.android.intent.scheme') }, + get FALLBACK_URL () { return config.get<string>('client.open_in_app.android.intent.fallback_url') } + } + }, + IOS: { + get ENABLED () { return config.get<boolean>('client.open_in_app.ios.enabled') }, + get HOST () { return config.get<string>('client.open_in_app.ios.host') }, + get SCHEME () { return config.get<string>('client.open_in_app.ios.scheme') }, + get FALLBACK_URL () { return config.get<string>('client.open_in_app.ios.fallback_url') } + } } }, diff --git a/server/core/lib/server-config-manager.ts b/server/core/lib/server-config-manager.ts index 5dd99b55a..afc8ef8ae 100644 --- a/server/core/lib/server-config-manager.ts +++ b/server/core/lib/server-config-manager.ts @@ -66,6 +66,22 @@ class ServerConfigManager { login: { redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH } + }, + openInApp: { + android: { + intent: { + enabled: CONFIG.CLIENT.OPEN_IN_APP.ANDROID.INTENT.ENABLED, + host: CONFIG.CLIENT.OPEN_IN_APP.ANDROID.INTENT.HOST, + scheme: CONFIG.CLIENT.OPEN_IN_APP.ANDROID.INTENT.SCHEME, + fallbackUrl: CONFIG.CLIENT.OPEN_IN_APP.ANDROID.INTENT.FALLBACK_URL + } + }, + ios: { + enabled: CONFIG.CLIENT.OPEN_IN_APP.IOS.ENABLED, + host: CONFIG.CLIENT.OPEN_IN_APP.IOS.HOST, + scheme: CONFIG.CLIENT.OPEN_IN_APP.IOS.SCHEME, + fallbackUrl: CONFIG.CLIENT.OPEN_IN_APP.IOS.FALLBACK_URL + } } },