diff --git a/css/_base.scss b/css/_base.scss index ea5a83d..744292e 100644 --- a/css/_base.scss +++ b/css/_base.scss @@ -106,7 +106,7 @@ form { .watermark { display: block; position: absolute; - top: 15; + top: 15px; width: $watermarkWidth; height: $watermarkHeight; background-size: contain; diff --git a/css/_meeting_intelligence.scss b/css/_meeting_intelligence.scss index c95b948..2444c65 100644 --- a/css/_meeting_intelligence.scss +++ b/css/_meeting_intelligence.scss @@ -7,6 +7,7 @@ right: 0; top: 0; width: 400px; + max-width: 100%; height: 100%; background: rgba(28, 32, 37, 0.98); border-left: 1px solid rgba(255, 255, 255, 0.1); @@ -14,6 +15,7 @@ flex-direction: column; z-index: 350; box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3); + padding-bottom: env(safe-area-inset-bottom, 0); } .meeting-intelligence-header { @@ -42,8 +44,8 @@ border: none; color: rgba(255, 255, 255, 0.7); cursor: pointer; - padding: 4px; - border-radius: 4px; + padding: 10px; + border-radius: 8px; display: flex; align-items: center; justify-content: center; diff --git a/css/_toolbars.scss b/css/_toolbars.scss index d1327a9..84864a8 100644 --- a/css/_toolbars.scss +++ b/css/_toolbars.scss @@ -93,6 +93,7 @@ .toolbox-content-wrapper::after { content: ''; + display: block; background: $newToolbarBackgroundColor; padding-bottom: env(safe-area-inset-bottom, 0); } diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index 9113b24..795c4e5 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -29,7 +29,7 @@ } .layout-indicator { - bottom: 80px; + bottom: calc(100px + env(safe-area-inset-bottom, 0px)); display: flex; gap: 6px; justify-content: center; @@ -37,13 +37,13 @@ pointer-events: none; position: absolute; right: 0; - z-index: 2; + z-index: 251; } .layout-indicator-dot { border-radius: 50%; - height: 6px; - width: 6px; + height: 8px; + width: 8px; } .layout-indicator-dot--active { @@ -74,7 +74,7 @@ #localVideoWrapper video, #localVideoWrapper object { border-radius: $borderRadius !important; - cursor: hand; + cursor: pointer; object-fit: cover; } diff --git a/css/filmstrip/_small_video.scss b/css/filmstrip/_small_video.scss index 4671a06..036e399 100644 --- a/css/filmstrip/_small_video.scss +++ b/css/filmstrip/_small_video.scss @@ -7,11 +7,11 @@ margin: 0 2px; &:hover { - cursor: hand; + cursor: pointer; } & > video { - cursor: hand; + cursor: pointer; border-radius: $borderRadius; object-fit: cover; overflow: hidden; diff --git a/index.html b/index.html index c624ca3..939e3cd 100644 --- a/index.html +++ b/index.html @@ -3,8 +3,8 @@ - - + + diff --git a/package-lock.json b/package-lock.json index 64c15b6..bfb0596 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "dayjs": "1.11.13", "dompurify": "3.2.6", "dropbox": "10.7.0", + "emoji-mart": "^5.6.0", "focus-visible": "5.1.0", "glob": "11.0.3", "grapheme-splitter": "1.0.4", @@ -132,6 +133,7 @@ "@babel/preset-env": "7.25.9", "@babel/preset-react": "7.25.9", "@jitsi/eslint-config": "6.0.4", + "@playwright/test": "^1.58.2", "@react-native-community/cli": "15.0.1", "@react-native-community/cli-platform-android": "15.0.1", "@react-native-community/cli-platform-ios": "15.0.1", @@ -5247,6 +5249,22 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", @@ -13088,6 +13106,12 @@ "integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==", "license": "ISC" }, + "node_modules/emoji-mart": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", + "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -21198,6 +21222,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pngjs": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", @@ -30480,6 +30551,15 @@ "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true }, + "@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "requires": { + "playwright": "1.58.2" + } + }, "@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", @@ -36018,6 +36098,11 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz", "integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==" }, + "emoji-mart": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", + "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==" + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -41782,6 +41867,31 @@ "find-up": "^4.0.0" } }, + "playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.58.2" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + } + } + }, + "playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true + }, "pngjs": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", diff --git a/package.json b/package.json index 83014f9..6abf2a1 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "dayjs": "1.11.13", "dompurify": "3.2.6", "dropbox": "10.7.0", + "emoji-mart": "^5.6.0", "focus-visible": "5.1.0", "glob": "11.0.3", "grapheme-splitter": "1.0.4", @@ -138,6 +139,7 @@ "@babel/preset-env": "7.25.9", "@babel/preset-react": "7.25.9", "@jitsi/eslint-config": "6.0.4", + "@playwright/test": "^1.58.2", "@react-native-community/cli": "15.0.1", "@react-native-community/cli-platform-android": "15.0.1", "@react-native-community/cli-platform-ios": "15.0.1", diff --git a/react/features/app/components/App.web.tsx b/react/features/app/components/App.web.tsx index ca38235..5891176 100644 --- a/react/features/app/components/App.web.tsx +++ b/react/features/app/components/App.web.tsx @@ -3,6 +3,7 @@ import React from 'react'; import GlobalStyles from '../../base/ui/components/GlobalStyles.web'; import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider.web'; import DialogContainer from '../../base/ui/components/web/DialogContainer'; +import DialogErrorBoundary from '../../base/ui/components/web/DialogErrorBoundary'; import ChromeExtensionBanner from '../../chrome-extension-banner/components/ChromeExtensionBanner.web'; import OverlayContainer from '../../overlay/components/web/OverlayContainer'; import PiP from '../../pip/components/PiP'; @@ -62,7 +63,9 @@ export class App extends AbstractApp { override _renderDialogContainer() { return ( - + + + ); } diff --git a/react/features/shared-music/components/web/EmbedPlayerManager.tsx b/react/features/shared-music/components/web/EmbedPlayerManager.tsx index 22c9dec..c15bfee 100644 --- a/react/features/shared-music/components/web/EmbedPlayerManager.tsx +++ b/react/features/shared-music/components/web/EmbedPlayerManager.tsx @@ -72,6 +72,35 @@ function getEmbedUrl(url: string, sourceType: string): string { return url; } + case SOURCE_TYPES.APPLE_MUSIC: { + // Replace music.apple.com with embed.music.apple.com + return url.replace('music.apple.com', 'embed.music.apple.com'); + } + case SOURCE_TYPES.DEEZER: { + // Extract type and ID: deezer.com/{country}/track/123 + const deezerMatch = url.match(/deezer\.com\/(?:[a-z]{2}\/)?(track|album|playlist)\/(\d+)/); + + if (deezerMatch) { + return `https://widget.deezer.com/widget/dark/${deezerMatch[1]}/${deezerMatch[2]}`; + } + + return url; + } + case SOURCE_TYPES.TIDAL: { + // Extract type and ID: tidal.com/browse/track/123 + const tidalMatch = url.match(/tidal\.com\/(?:browse\/)?(track|album|playlist|mix)\/([a-zA-Z0-9-]+)/); + + if (tidalMatch) { + return `https://embed.tidal.com/${tidalMatch[1]}s/${tidalMatch[2]}`; + } + + return url; + } + case SOURCE_TYPES.BANDCAMP: { + // Bandcamp doesn't have a simple embed URL transformation + // The full page URL works in an iframe for playback + return url; + } default: return url; } diff --git a/react/features/shared-music/components/web/SharedMusicPlayer.tsx b/react/features/shared-music/components/web/SharedMusicPlayer.tsx index 004d798..fb839fb 100644 --- a/react/features/shared-music/components/web/SharedMusicPlayer.tsx +++ b/react/features/shared-music/components/web/SharedMusicPlayer.tsx @@ -42,7 +42,11 @@ const EMBED_SOURCE_TYPES: readonly string[] = [ SOURCE_TYPES.SOUNDCLOUD, SOURCE_TYPES.SPOTIFY, SOURCE_TYPES.DAILYMOTION, - SOURCE_TYPES.TWITCH + SOURCE_TYPES.TWITCH, + SOURCE_TYPES.APPLE_MUSIC, + SOURCE_TYPES.DEEZER, + SOURCE_TYPES.TIDAL, + SOURCE_TYPES.BANDCAMP ]; /** diff --git a/react/features/shared-music/components/web/SharedMusicTile.tsx b/react/features/shared-music/components/web/SharedMusicTile.tsx index 6d5c205..3f96e61 100644 --- a/react/features/shared-music/components/web/SharedMusicTile.tsx +++ b/react/features/shared-music/components/web/SharedMusicTile.tsx @@ -29,7 +29,11 @@ const EMBEDDED_CONTROL_TYPES: readonly string[] = [ SOURCE_TYPES.SOUNDCLOUD, SOURCE_TYPES.SPOTIFY, SOURCE_TYPES.DAILYMOTION, - SOURCE_TYPES.TWITCH + SOURCE_TYPES.TWITCH, + SOURCE_TYPES.APPLE_MUSIC, + SOURCE_TYPES.DEEZER, + SOURCE_TYPES.TIDAL, + SOURCE_TYPES.BANDCAMP ]; interface IProps { diff --git a/react/features/shared-music/constants.ts b/react/features/shared-music/constants.ts index 803c310..9a48a12 100644 --- a/react/features/shared-music/constants.ts +++ b/react/features/shared-music/constants.ts @@ -36,6 +36,10 @@ export const SOURCE_TYPES = { SPOTIFY: 'spotify', DAILYMOTION: 'dailymotion', TWITCH: 'twitch', + APPLE_MUSIC: 'apple_music', + DEEZER: 'deezer', + TIDAL: 'tidal', + BANDCAMP: 'bandcamp', DIRECT: 'direct' } as const; @@ -73,3 +77,23 @@ export const DAILYMOTION_URL_DOMAIN = 'dailymotion.com'; * Twitch domain. */ export const TWITCH_URL_DOMAIN = 'twitch.tv'; + +/** + * Apple Music domain. + */ +export const APPLE_MUSIC_URL_DOMAIN = 'music.apple.com'; + +/** + * Deezer domain. + */ +export const DEEZER_URL_DOMAIN = 'deezer.com'; + +/** + * Tidal domain. + */ +export const TIDAL_URL_DOMAIN = 'tidal.com'; + +/** + * Bandcamp domain. + */ +export const BANDCAMP_URL_DOMAIN = 'bandcamp.com'; diff --git a/react/features/shared-music/functions.ts b/react/features/shared-music/functions.ts index 021fce5..4ad5e85 100644 --- a/react/features/shared-music/functions.ts +++ b/react/features/shared-music/functions.ts @@ -3,13 +3,17 @@ import { IJitsiConference } from '../base/conference/reducer'; import { toState } from '../base/redux/functions'; import { + APPLE_MUSIC_URL_DOMAIN, + BANDCAMP_URL_DOMAIN, DAILYMOTION_URL_DOMAIN, + DEEZER_URL_DOMAIN, PLAYBACK_START, PLAYBACK_STATUSES, SHARED_MUSIC, SOUNDCLOUD_URL_DOMAIN, SOURCE_TYPES, SPOTIFY_URL_DOMAIN, + TIDAL_URL_DOMAIN, TWITCH_URL_DOMAIN, VIMEO_URL_DOMAIN, YOUTUBE_MUSIC_URL_DOMAIN, @@ -115,6 +119,64 @@ function getSpotifyInfo(url: string): { id: string; type: string; } | null { return result ? { type: result[1], id: result[2] } : null; } +/** + * Extracts Apple Music info from URL. + * + * @param {string} url - The entered URL. + * @returns {Object|null} The Apple Music info if matched. + */ +function getAppleMusicInfo(url: string): { id: string; path: string; } | null { + if (!url) { + return null; + } + + // Matches music.apple.com/{country}/album/{name}/{id} or /playlist/{name}/{id} + const p = /(?:https?:\/\/)?music\.apple\.com\/([a-z]{2})\/(album|playlist|song)\/([^/]+)\/([a-zA-Z0-9.]+)/; + const result = url.match(p); + + if (result) { + return { path: `${result[1]}/${result[2]}/${result[3]}/${result[4]}`, id: result[4] }; + } + + return null; +} + +/** + * Extracts Deezer info from URL. + * + * @param {string} url - The entered URL. + * @returns {Object|null} The Deezer info if matched. + */ +function getDeezerInfo(url: string): { id: string; type: string; } | null { + if (!url) { + return null; + } + + // Matches deezer.com/{country}/track/123, /album/123, /playlist/123 + const p = /(?:https?:\/\/)?(?:www\.)?deezer\.com\/(?:[a-z]{2}\/)?(track|album|playlist)\/(\d+)/; + const result = url.match(p); + + return result ? { type: result[1], id: result[2] } : null; +} + +/** + * Extracts Tidal info from URL. + * + * @param {string} url - The entered URL. + * @returns {Object|null} The Tidal info if matched. + */ +function getTidalInfo(url: string): { id: string; type: string; } | null { + if (!url) { + return null; + } + + // Matches tidal.com/browse/track/123, /album/123, /playlist/{uuid} + const p = /(?:https?:\/\/)?(?:www\.)?(?:listen\.)?tidal\.com\/(?:browse\/)?(track|album|playlist|mix)\/([a-zA-Z0-9-]+)/; + const result = url.match(p); + + return result ? { type: result[1], id: result[2] } : null; +} + /** * Checks if the status is one that is actually sharing music - playing, pause or start. * @@ -180,6 +242,22 @@ export function getSourceType(url: string): SourceType { if (hostname.includes(TWITCH_URL_DOMAIN)) { return SOURCE_TYPES.TWITCH; } + + if (hostname.includes(APPLE_MUSIC_URL_DOMAIN)) { + return SOURCE_TYPES.APPLE_MUSIC; + } + + if (hostname.includes(DEEZER_URL_DOMAIN)) { + return SOURCE_TYPES.DEEZER; + } + + if (hostname.includes(TIDAL_URL_DOMAIN)) { + return SOURCE_TYPES.TIDAL; + } + + if (hostname.includes(BANDCAMP_URL_DOMAIN)) { + return SOURCE_TYPES.BANDCAMP; + } } catch (_) { // Not a valid URL } @@ -276,6 +354,47 @@ export function extractMusicUrl(input: string): { }; } + // Apple Music + const appleMusicInfo = getAppleMusicInfo(trimmedLink); + + if (appleMusicInfo) { + return { + url: trimmedLink, + sourceType: SOURCE_TYPES.APPLE_MUSIC, + embedInfo: { id: appleMusicInfo.id, type: appleMusicInfo.path } + }; + } + + // Deezer + const deezerInfo = getDeezerInfo(trimmedLink); + + if (deezerInfo) { + return { + url: trimmedLink, + sourceType: SOURCE_TYPES.DEEZER, + embedInfo: deezerInfo + }; + } + + // Tidal + const tidalInfo = getTidalInfo(trimmedLink); + + if (tidalInfo) { + return { + url: trimmedLink, + sourceType: SOURCE_TYPES.TIDAL, + embedInfo: tidalInfo + }; + } + + // Bandcamp - use full URL for embedding + if (hostname.includes(BANDCAMP_URL_DOMAIN)) { + return { + url: trimmedLink, + sourceType: SOURCE_TYPES.BANDCAMP + }; + } + // It's a direct URL (audio/video file) return { url: trimmedLink, diff --git a/react/features/shared-video/constants.ts b/react/features/shared-video/constants.ts index 1a07a2c..66afb3f 100644 --- a/react/features/shared-video/constants.ts +++ b/react/features/shared-video/constants.ts @@ -34,6 +34,17 @@ export const PLAYBACK_STATUSES = { */ export const PLAYBACK_START = 'start'; +/** + * Video source types. + */ +export const VIDEO_SOURCE_TYPES = { + YOUTUBE: 'youtube', + VIMEO: 'vimeo', + DAILYMOTION: 'dailymotion', + TWITCH: 'twitch', + DIRECT: 'direct' +} as const; + /** * The domain for youtube URLs. */ @@ -46,5 +57,6 @@ export const ALLOW_ALL_URL_DOMAINS = '*'; /** * The default white listed domains for shared video. + * Allow all domains so Vimeo, Dailymotion, Twitch etc. work out of the box. */ -export const DEFAULT_ALLOWED_URL_DOMAINS = [ YOUTUBE_URL_DOMAIN ]; +export const DEFAULT_ALLOWED_URL_DOMAINS = [ ALLOW_ALL_URL_DOMAINS ]; diff --git a/react/features/shared-video/functions.ts b/react/features/shared-video/functions.ts index b96c7ec..a49f253 100644 --- a/react/features/shared-video/functions.ts +++ b/react/features/shared-video/functions.ts @@ -9,6 +9,7 @@ import { PLAYBACK_STATUSES, SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME, + VIDEO_SOURCE_TYPES, YOUTUBE_PLAYER_PARTICIPANT_NAME, YOUTUBE_URL_DOMAIN } from './constants'; @@ -139,6 +140,95 @@ export function isURLAllowedForSharedVideo(url: string, return false; } +/** + * Determines the video source type from a URL or YouTube ID. + * + * @param {string} url - The video URL or YouTube ID. + * @returns {string} The source type. + */ +export function getVideoSourceType(url: string): string { + if (!url) { + return VIDEO_SOURCE_TYPES.DIRECT; + } + + // If it doesn't look like a URL, it's a YouTube ID + if (!url.match(/^https?:\/\//)) { + return VIDEO_SOURCE_TYPES.YOUTUBE; + } + + try { + const urlObj = new URL(url); + const hostname = urlObj.hostname.toLowerCase(); + + if (hostname.includes('youtube.com') || hostname.includes('youtu.be')) { + return VIDEO_SOURCE_TYPES.YOUTUBE; + } + + if (hostname.includes('vimeo.com')) { + return VIDEO_SOURCE_TYPES.VIMEO; + } + + if (hostname.includes('dailymotion.com') || hostname.includes('dai.ly')) { + return VIDEO_SOURCE_TYPES.DAILYMOTION; + } + + if (hostname.includes('twitch.tv')) { + return VIDEO_SOURCE_TYPES.TWITCH; + } + } catch (_) { + // Not a valid URL + } + + return VIDEO_SOURCE_TYPES.DIRECT; +} + +/** + * Generates an embed URL for video platforms. + * + * @param {string} url - The original URL. + * @param {string} sourceType - The source type. + * @returns {string} The embed URL. + */ +export function getVideoEmbedUrl(url: string, sourceType: string): string { + switch (sourceType) { + case VIDEO_SOURCE_TYPES.VIMEO: { + const vimeoMatch = url.match(/vimeo\.com\/(?:video\/)?(\d+)/); + + if (vimeoMatch) { + return `https://player.vimeo.com/video/${vimeoMatch[1]}?autoplay=1&autopause=0`; + } + + return url; + } + case VIDEO_SOURCE_TYPES.DAILYMOTION: { + const dmMatch = url.match(/(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/); + + if (dmMatch) { + return `https://www.dailymotion.com/embed/video/${dmMatch[1]}?autoplay=1`; + } + + return url; + } + case VIDEO_SOURCE_TYPES.TWITCH: { + const parent = window.location.hostname; + const videoMatch = url.match(/twitch\.tv\/videos\/(\d+)/); + const channelMatch = url.match(/twitch\.tv\/([a-zA-Z0-9_]+)(?:\?|$)/); + + if (videoMatch) { + return `https://player.twitch.tv/?video=v${videoMatch[1]}&parent=${parent}&autoplay=true`; + } + + if (channelMatch && channelMatch[1] !== 'videos') { + return `https://player.twitch.tv/?channel=${channelMatch[1]}&parent=${parent}&autoplay=true`; + } + + return url; + } + default: + return url; + } +} + /** * Sends SHARED_VIDEO command. * diff --git a/react/features/toolbox/constants.ts b/react/features/toolbox/constants.ts index b8c2624..d739dcf 100644 --- a/react/features/toolbox/constants.ts +++ b/react/features/toolbox/constants.ts @@ -56,7 +56,7 @@ export const THRESHOLDS = [ }, { width: 200, - order: [ 'microphone', 'camera' ] + order: [ 'microphone', 'camera', 'hangup' ] } ]; @@ -162,6 +162,7 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [ 'invite', 'linktosalesforce', 'livestreaming', + 'meetingintelligence', 'microphone', 'mute-everyone', 'mute-video-everyone', diff --git a/static/close2.html b/static/close2.html index 8a25b45..38b5622 100644 --- a/static/close2.html +++ b/static/close2.html @@ -17,9 +17,11 @@ color: #fff; font-size: 16px; font-weight: 500; - padding: 12px 24px; + padding: 14px 28px; text-decoration: none; transition: opacity 0.2s; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; } .meeting-intelligence-link a:hover { opacity: 0.9;