diff --git a/android/sdk/build.gradle b/android/sdk/build.gradle index b0288a3e0..4de1a3237 100644 --- a/android/sdk/build.gradle +++ b/android/sdk/build.gradle @@ -50,7 +50,8 @@ dependencies { api 'com.facebook.react:react-native:+' //noinspection GradleDynamicVersion implementation 'org.webkit:android-jsc:+' - + + implementation 'com.facebook.fresco:animated-gif:2.5.0' implementation 'com.dropbox.core:dropbox-core-sdk:4.0.1' implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.squareup.duktape:duktape-android:1.3.0' @@ -80,6 +81,7 @@ dependencies { implementation project(':react-native-default-preference') implementation project(':react-native-gesture-handler') implementation project(':react-native-get-random-values') + implementation project(':react-native-giphy') implementation project(':react-native-immersive') implementation project(':react-native-keep-awake') implementation project(':react-native-masked-view_masked-view') diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java index ea6007aed..658489592 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java @@ -110,6 +110,7 @@ class ReactInstanceManagerHolder { new com.corbt.keepawake.KCKeepAwakePackage(), new com.facebook.react.shell.MainReactPackage(), new com.reactnativecommunity.clipboard.ClipboardPackage(), + new com.giphyreactnativesdk.GiphyReactNativeSdkPackage(), new com.reactnativecommunity.netinfo.NetInfoPackage(), new com.reactnativepagerview.PagerViewPackage(), new com.oblador.performance.PerformancePackage(), diff --git a/android/settings.gradle b/android/settings.gradle index 0deb85590..09fdd3085 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -21,6 +21,8 @@ include ':react-native-gesture-handler' project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android') include ':react-native-get-random-values' project(':react-native-get-random-values').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-get-random-values/android') +include ':react-native-giphy' +project(':react-native-giphy').projectDir = new File(rootProject.projectDir, '../node_modules/@giphy/react-native-sdk/android') include ':react-native-google-signin' project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-google-signin/google-signin/android') include ':react-native-immersive' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9f422108c..e3c81dbc8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -66,6 +66,11 @@ PODS: - GoogleUtilities/UserDefaults (~> 6.7) - PromisesObjC (~> 1.2) - fmt (6.2.1) + - Giphy (2.1.20): + - libwebp + - giphy-react-native-sdk (1.7.0): + - Giphy (= 2.1.20) + - React-Core - glog (0.3.5) - GoogleAppMeasurement (6.8.3): - GoogleUtilities/AppDelegateSwizzler (~> 6.7) @@ -102,6 +107,15 @@ PODS: - AppAuth/Core (~> 1.4) - GTMSessionFetcher/Core (~> 1.5) - GTMSessionFetcher/Core (1.7.0) + - libwebp (1.2.1): + - libwebp/demux (= 1.2.1) + - libwebp/mux (= 1.2.1) + - libwebp/webp (= 1.2.1) + - libwebp/demux (1.2.1): + - libwebp/webp + - libwebp/mux (1.2.1): + - libwebp/demux + - libwebp/webp (1.2.1) - nanopb (1.30906.0): - nanopb/decode (= 1.30906.0) - nanopb/encode (= 1.30906.0) @@ -442,6 +456,7 @@ DEPENDENCIES: - Firebase/Analytics (~> 6.33.0) - Firebase/Crashlytics (~> 6.33.0) - Firebase/DynamicLinks (~> 6.33.0) + - "giphy-react-native-sdk (from `../node_modules/@giphy/react-native-sdk`)" - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - ObjectiveDropboxOfficial (= 6.2.3) - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) @@ -510,12 +525,14 @@ SPEC REPOS: - FirebaseDynamicLinks - FirebaseInstallations - fmt + - Giphy - GoogleAppMeasurement - GoogleDataTransport - GoogleSignIn - GoogleUtilities - GTMAppAuth - GTMSessionFetcher + - libwebp - nanopb - ObjectiveDropboxOfficial - PromisesObjC @@ -531,6 +548,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/Libraries/FBLazyVector" FBReactNativeSpec: :path: "../node_modules/react-native/React/FBReactNativeSpec" + giphy-react-native-sdk: + :path: "../node_modules/@giphy/react-native-sdk" glog: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" RCT-Folly: @@ -651,6 +670,8 @@ SPEC CHECKSUMS: FirebaseDynamicLinks: 6eac37d86910382eafb6315d952cc44c9e176094 FirebaseInstallations: 466c7b4d1f58fe16707693091da253726a731ed2 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 + Giphy: b6d5087521d251bb8c99cdc0eb07bbdf86d142d5 + giphy-react-native-sdk: 7abccf2b52123a0f30ce99da895ab6288023680c glog: 5337263514dd6f09803962437687240c5dc39aa4 GoogleAppMeasurement: 966e88df9d19c15715137bb2ddaf52373f111436 GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833 @@ -658,6 +679,7 @@ SPEC CHECKSUMS: GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3 GTMAppAuth: ad5c2b70b9a8689e1a04033c9369c4915bfcbe89 GTMSessionFetcher: 43748f93435c2aa068b1cbe39655aaf600652e91 + libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc ObjectiveDropboxOfficial: fe206ce8c0bc49976c249d472db7fdbc53ebbd53 PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97 diff --git a/ios/app/app.xcodeproj/project.pbxproj b/ios/app/app.xcodeproj/project.pbxproj index 7cc74424f..5dfef8c15 100644 --- a/ios/app/app.xcodeproj/project.pbxproj +++ b/ios/app/app.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ DEA9F28A258A6EA800D4CD74 /* JitsiMeetSDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DEA9F288258A6EA800D4CD74 /* JitsiMeetSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E588011722789D43008B0561 /* JitsiMeetContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = E58801132278944E008B0561 /* JitsiMeetContext.swift */; }; E5C97B63227A1EB400199214 /* JitsiMeetCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5C97B62227A1EB400199214 /* JitsiMeetCommands.swift */; }; + FD572B9827EDF32300A800FB /* GiphyUISDK.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD572B9727EDF32300A800FB /* GiphyUISDK.xcframework */; }; + FD572B9927EDF32300A800FB /* GiphyUISDK.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FD572B9727EDF32300A800FB /* GiphyUISDK.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -72,6 +74,7 @@ dstSubfolderSpec = 10; files = ( DEA9F28A258A6EA800D4CD74 /* JitsiMeetSDK.framework in Embed Frameworks */, + FD572B9927EDF32300A800FB /* GiphyUISDK.xcframework in Embed Frameworks */, DE05038A256E904600DEE3A5 /* WebRTC.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -158,6 +161,7 @@ DEFDBBDB25656E3B00344B23 /* WebRTC.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WebRTC.xcframework; path = "../../node_modules/react-native-webrtc/ios/WebRTC.xcframework"; sourceTree = ""; }; E58801132278944E008B0561 /* JitsiMeetContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetContext.swift; sourceTree = ""; }; E5C97B62227A1EB400199214 /* JitsiMeetCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetCommands.swift; sourceTree = ""; }; + FD572B9727EDF32300A800FB /* GiphyUISDK.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = GiphyUISDK.xcframework; path = ../Pods/Giphy/GiphySDK/GiphyUISDK.xcframework; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -174,6 +178,7 @@ files = ( DE050389256E904600DEE3A5 /* WebRTC.xcframework in Frameworks */, DEA9F289258A6EA800D4CD74 /* JitsiMeetSDK.framework in Frameworks */, + FD572B9827EDF32300A800FB /* GiphyUISDK.xcframework in Frameworks */, 2681BB562C7A0B42CFBA6719 /* libPods-JitsiMeet.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -199,6 +204,7 @@ 0B26BE711EC5BC4D00EEFB41 /* Frameworks */ = { isa = PBXGroup; children = ( + FD572B9727EDF32300A800FB /* GiphyUISDK.xcframework */, DEA9F288258A6EA800D4CD74 /* JitsiMeetSDK.framework */, DE050388256E904600DEE3A5 /* WebRTC.xcframework */, 0B26BE6D1EC5BC3C00EEFB41 /* JitsiMeet.framework */, diff --git a/lang/main.json b/lang/main.json index 7b9d97f39..5081279ee 100644 --- a/lang/main.json +++ b/lang/main.json @@ -629,6 +629,7 @@ "displayNotifications": "Display notifications for", "focus": "Conference focus", "focusFail": "{{component}} not available - retry in {{ms}} sec", + "gifsMenu": "GIPHY", "groupTitle": "Notifications", "hostAskedUnmute": "The moderator would like you to speak", "invitedOneMember": "{{name}} has been invited", diff --git a/package-lock.json b/package-lock.json index b895632ac..7f2766575 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@atlaskit/tooltip": "17.1.2", "@giphy/js-fetch-api": "4.1.2", "@giphy/react-components": "5.6.0", + "@giphy/react-native-sdk": "1.7.0", "@hapi/bourne": "2.0.0", "@jitsi/js-utils": "2.0.0", "@jitsi/logger": "2.0.0", @@ -3378,6 +3379,30 @@ "react": ">=16.3.0" } }, + "node_modules/@giphy/react-native-sdk": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@giphy/react-native-sdk/-/react-native-sdk-1.7.0.tgz", + "integrity": "sha512-mCIqtPkDAstL+BDTbC1EQ4SiRkND3zd9uLKUeR4RkK2AhjRTUIheGzfxOZrdR014LVwcwKw5s9qpogoXr66mgw==", + "dependencies": { + "@giphy/js-types": "^4.0.3", + "type-fest": "^2.10.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/@giphy/react-native-sdk/node_modules/type-fest": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.12.1.tgz", + "integrity": "sha512-AiknQSEqKVGDDjtZqeKrUoTlcj7FKhupmnVUgz6KoOKtvMwRGE6hUNJ/nVear+h7fnUPO1q/htSkYKb1pyntkQ==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@hapi/bourne": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.0.0.tgz", @@ -22246,6 +22271,22 @@ } } }, + "@giphy/react-native-sdk": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@giphy/react-native-sdk/-/react-native-sdk-1.7.0.tgz", + "integrity": "sha512-mCIqtPkDAstL+BDTbC1EQ4SiRkND3zd9uLKUeR4RkK2AhjRTUIheGzfxOZrdR014LVwcwKw5s9qpogoXr66mgw==", + "requires": { + "@giphy/js-types": "^4.0.3", + "type-fest": "^2.10.0" + }, + "dependencies": { + "type-fest": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.12.1.tgz", + "integrity": "sha512-AiknQSEqKVGDDjtZqeKrUoTlcj7FKhupmnVUgz6KoOKtvMwRGE6hUNJ/nVear+h7fnUPO1q/htSkYKb1pyntkQ==" + } + } + }, "@hapi/bourne": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-2.0.0.tgz", diff --git a/package.json b/package.json index beb15bec7..ddff20337 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@atlaskit/tooltip": "17.1.2", "@giphy/js-fetch-api": "4.1.2", "@giphy/react-components": "5.6.0", + "@giphy/react-native-sdk": "1.7.0", "@hapi/bourne": "2.0.0", "@jitsi/js-utils": "2.0.0", "@jitsi/logger": "2.0.0", diff --git a/react/features/app/middlewares.any.js b/react/features/app/middlewares.any.js index b38cfd9b8..5cc31fc88 100644 --- a/react/features/app/middlewares.any.js +++ b/react/features/app/middlewares.any.js @@ -30,7 +30,6 @@ import '../display-name/middleware'; import '../etherpad/middleware'; import '../filmstrip/middleware'; import '../follow-me/middleware'; -import '../gifs/middleware'; import '../invite/middleware'; import '../jaas/middleware'; import '../large-video/middleware'; diff --git a/react/features/app/middlewares.native.js b/react/features/app/middlewares.native.js index 7a98b015b..1cdf30ff2 100644 --- a/react/features/app/middlewares.native.js +++ b/react/features/app/middlewares.native.js @@ -1,6 +1,7 @@ // @flow import '../authentication/middleware'; +import '../gifs/middleware'; import '../mobile/audio-mode/middleware'; import '../mobile/background/middleware'; import '../mobile/call-integration/middleware'; diff --git a/react/features/app/middlewares.web.js b/react/features/app/middlewares.web.js index fd2719a92..50ff53ec2 100644 --- a/react/features/app/middlewares.web.js +++ b/react/features/app/middlewares.web.js @@ -22,5 +22,6 @@ import '../talk-while-muted/middleware'; import '../virtual-background/middleware'; import '../face-centering/middleware'; import '../facial-recognition/middleware'; +import '../gifs/middleware'; import './middlewares.any'; diff --git a/react/features/base/react/components/web/Message.js b/react/features/base/react/components/web/Message.js index df0a2dac9..6469f1f6d 100644 --- a/react/features/base/react/components/web/Message.js +++ b/react/features/base/react/components/web/Message.js @@ -4,6 +4,7 @@ import React, { Component } from 'react'; import { toArray } from 'react-emoji-render'; import GifMessage from '../../../../chat/components/web/GifMessage'; +import { GIF_PREFIX } from '../../../../gifs/constants'; import { isGifMessage } from '../../../../gifs/functions'; import Linkify from './Linkify'; @@ -49,7 +50,7 @@ class Message extends Component { // check if the message is a GIF if (isGifMessage(text)) { - const url = text.substring(4, text.length - 1); + const url = text.substring(GIF_PREFIX.length, text.length - 1); content.push( { messageBubbleStyle.push(_styles.lobbyMessageBubble); } + const messageText = replaceNonUnicodeEmojis(this._getMessageText()); + return ( { this._renderAvatar() } @@ -82,9 +86,13 @@ class ChatMessage extends AbstractChatMessage { { this._renderDisplayName() } - - { replaceNonUnicodeEmojis(this._getMessageText()) } - + {isGifMessage(messageText) + ? + : ( + + {messageText} + + )} { this._renderPrivateNotice() } { this._renderPrivateReplyButton() } diff --git a/react/features/chat/components/native/GifMessage.js b/react/features/chat/components/native/GifMessage.js new file mode 100644 index 000000000..ba7f3a54b --- /dev/null +++ b/react/features/chat/components/native/GifMessage.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { Image, View } from 'react-native'; + +import { GIF_PREFIX } from '../../../gifs/constants'; + +import styles from './styles'; + +type Props = { + + /** + * The formatted gif message. + */ + message: string +} + +const GifMessage = ({ message }: Props) => { + const url = message.substring(GIF_PREFIX.length, message.length - 1); + + return ( + + ); +}; + +export default GifMessage; diff --git a/react/features/chat/components/native/styles.js b/react/features/chat/components/native/styles.js index 30f68e696..ab106815a 100644 --- a/react/features/chat/components/native/styles.js +++ b/react/features/chat/components/native/styles.js @@ -145,6 +145,17 @@ export default { borderTopLeftRadius: 0, borderTopRightRadius: 0, borderBottomRightRadius: 0 + }, + + gifContainer: { + maxHeight: 150 + }, + + gifImage: { + resizeMode: 'contain', + width: 250, + height: undefined, + flexGrow: 1 } }; diff --git a/react/features/filmstrip/components/native/Thumbnail.js b/react/features/filmstrip/components/native/Thumbnail.js index 31e926a51..31ed6d263 100644 --- a/react/features/filmstrip/components/native/Thumbnail.js +++ b/react/features/filmstrip/components/native/Thumbnail.js @@ -1,7 +1,7 @@ // @flow import React, { PureComponent } from 'react'; -import { View } from 'react-native'; +import { Image, View } from 'react-native'; import type { Dispatch } from 'redux'; import { ColorSchemeRegistry } from '../../../base/color-scheme'; @@ -22,6 +22,7 @@ import { StyleType } from '../../../base/styles'; import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks'; import { ConnectionIndicator } from '../../../connection-indicator'; import { DisplayNameLabel } from '../../../display-name'; +import { getGifDisplayMode, getGifForParticipant } from '../../../gifs/functions'; import { showContextMenuDetails, showSharedVideoMenu @@ -45,6 +46,11 @@ type Props = { */ _audioMuted: boolean, + /** + * URL of GIF sent by this participant, null if there's none. + */ + _gifSrc: ?string, + /** * Indicates whether the participant is fake. */ @@ -247,6 +253,7 @@ class Thumbnail extends PureComponent { */ render() { const { + _gifSrc, _isScreenShare: isScreenShare, _isFakeParticipant, _participantId: participantId, @@ -280,15 +287,21 @@ class Thumbnail extends PureComponent { _renderDominantSpeakerIndicator ? styles.thumbnailDominantSpeaker : null ] } touchFeedback = { false }> - - { - this._renderIndicators() + {_gifSrc ? + : <> + + { + this._renderIndicators() + } + } ); @@ -324,9 +337,12 @@ function _mapStateToProps(state, ownProps) { const renderModeratorIndicator = !_isEveryoneModerator && participant?.role === PARTICIPANT_ROLE.MODERATOR; const participantInLargeVideo = id === largeVideo.participantId; + const { gifUrl: gifSrc } = getGifForParticipant(state, id); + const mode = getGifDisplayMode(state); return { _audioMuted: audioTrack?.muted ?? true, + _gifSrc: mode === 'chat' ? null : gifSrc, _isFakeParticipant: participant?.isFakeParticipant, _isScreenShare: isScreenShare, _local: participant?.local, diff --git a/react/features/filmstrip/components/native/styles.js b/react/features/filmstrip/components/native/styles.js index d0e59cb4b..b2ca36ece 100644 --- a/react/features/filmstrip/components/native/styles.js +++ b/react/features/filmstrip/components/native/styles.js @@ -171,6 +171,11 @@ export default { thumbnailDominantSpeaker: { borderWidth: 4, borderColor: BaseTheme.palette.action01Hover + }, + + thumbnailGif: { + flexGrow: 1, + resizeMode: 'contain' } }; diff --git a/react/features/gifs/components/_.native.js b/react/features/gifs/components/_.native.js new file mode 100644 index 000000000..738c4d2b8 --- /dev/null +++ b/react/features/gifs/components/_.native.js @@ -0,0 +1 @@ +export * from './native'; diff --git a/react/features/gifs/components/native/GifsMenu.js b/react/features/gifs/components/native/GifsMenu.js new file mode 100644 index 000000000..dac217275 --- /dev/null +++ b/react/features/gifs/components/native/GifsMenu.js @@ -0,0 +1,66 @@ +import { GiphyContent, GiphyGridView, GiphyMediaType } from '@giphy/react-native-sdk'; +import React, { useCallback, useState } from 'react'; +import { Image, Keyboard, Text, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useDispatch } from 'react-redux'; + +import { createGifSentEvent, sendAnalytics } from '../../../analytics'; +import JitsiScreen from '../../../base/modal/components/JitsiScreen'; +import { sendMessage } from '../../../chat/actions.any'; +import { goBack } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef'; +import ClearableInput from '../../../participants-pane/components/native/ClearableInput'; +import { formatGifUrlMessage, getGifUrl } from '../../functions'; + +import styles from './styles'; + +const GifsMenu = () => { + const [ searchQuery, setSearchQuery ] = useState(''); + const dispatch = useDispatch(); + const insets = useSafeAreaInsets(); + + const content = searchQuery === '' + ? GiphyContent.trending({ mediaType: GiphyMediaType.Gif }) + : GiphyContent.search({ + searchQuery, + mediaType: GiphyMediaType.Gif + }); + + const sendGif = useCallback(e => { + const url = getGifUrl(e.nativeEvent.media); + + sendAnalytics(createGifSentEvent()); + + dispatch(sendMessage(formatGifUrlMessage(url), true)); + goBack(); + }, []); + + const onScroll = useCallback(Keyboard.dismiss, []); + + return ( + + + + Powered by + + + ); +}; + +export default GifsMenu; diff --git a/react/features/gifs/components/native/index.js b/react/features/gifs/components/native/index.js new file mode 100644 index 000000000..0aa308127 --- /dev/null +++ b/react/features/gifs/components/native/index.js @@ -0,0 +1,3 @@ +// @flow + +export { default as GifsMenu } from './GifsMenu'; diff --git a/react/features/gifs/components/native/styles.js b/react/features/gifs/components/native/styles.js new file mode 100644 index 000000000..ddd10d87d --- /dev/null +++ b/react/features/gifs/components/native/styles.js @@ -0,0 +1,40 @@ +import BaseTheme from '../../../base/ui/components/BaseTheme.native'; + +export default { + container: { + backgroundColor: BaseTheme.palette.ui01, + flex: 1 + }, + + clearableInput: { + wrapper: { + marginBottom: BaseTheme.spacing[3], + marginTop: BaseTheme.spacing[3] + }, + + input: { textAlign: 'left' } + }, + + grid: { + flex: 1, + marginLeft: BaseTheme.spacing[3], + marginRight: BaseTheme.spacing[3] + }, + + credit: { + backgroundColor: BaseTheme.palette.ui01, + width: '100%', + height: 40, + position: 'absolute', + marginBottom: 0, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center' + }, + + creditText: { + color: 'white', + fontWeight: 'bold' + } +}; diff --git a/react/features/gifs/functions.js b/react/features/gifs/functions.js index a355ffece..11d906bf8 100644 --- a/react/features/gifs/functions.js +++ b/react/features/gifs/functions.js @@ -10,7 +10,7 @@ import { GIF_PREFIX } from './constants'; * @returns {Object} */ export function getGifForParticipant(state, participantId) { - return state['features/gifs'].gifList.get(participantId) || {}; + return isGifEnabled(state) ? state['features/gifs'].gifList.get(participantId) || {} : {}; } /** @@ -20,7 +20,8 @@ export function getGifForParticipant(state, participantId) { * @returns {boolean} */ export function isGifMessage(message) { - return message.trim().startsWith(GIF_PREFIX); + return message.trim().toLowerCase() + .startsWith(GIF_PREFIX); } /** @@ -43,11 +44,11 @@ export function isGifsMenuOpen(state) { * @returns {boolean} */ export function getGifUrl(gif) { - const embedUrl = gif?.embed_url || ''; + const embedUrl = gif?.embed_url || gif?.data?.embed_url || ''; const idx = embedUrl.lastIndexOf('/'); const id = embedUrl.substr(idx + 1); - return `https://i.giphy.com/media/${id}/giphy.webp`; + return `https://i.giphy.com/media/${id}/giphy.gif`; } /** diff --git a/react/features/gifs/middleware.js b/react/features/gifs/middleware.any.js similarity index 100% rename from react/features/gifs/middleware.js rename to react/features/gifs/middleware.any.js diff --git a/react/features/gifs/middleware.native.js b/react/features/gifs/middleware.native.js new file mode 100644 index 000000000..5e480e47a --- /dev/null +++ b/react/features/gifs/middleware.native.js @@ -0,0 +1,3 @@ + +import './middleware.any'; +import './subscriber.native'; diff --git a/react/features/gifs/middleware.web.js b/react/features/gifs/middleware.web.js new file mode 100644 index 000000000..818cf0f5f --- /dev/null +++ b/react/features/gifs/middleware.web.js @@ -0,0 +1,2 @@ + +import './middleware.any'; diff --git a/react/features/gifs/subscriber.native.js b/react/features/gifs/subscriber.native.js new file mode 100644 index 000000000..2f570607d --- /dev/null +++ b/react/features/gifs/subscriber.native.js @@ -0,0 +1,20 @@ +import { GiphySDK } from '@giphy/react-native-sdk'; + +import { StateListenerRegistry } from '../base/redux'; + +import { isGifEnabled } from './functions'; + +/** + * Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles. + */ +StateListenerRegistry.register( + /* selector */ state => state['features/base/config']?.giphy, + /* listener */ (_, store) => { + const state = store.getState(); + + if (isGifEnabled(state)) { + GiphySDK.configure({ apiKey: state['features/base/config'].giphy?.sdkKey }); + } + }, { + deepEquals: true + }); diff --git a/react/features/mobile/navigation/components/conference/components/ConferenceNavigationContainer.js b/react/features/mobile/navigation/components/conference/components/ConferenceNavigationContainer.js index e52706714..d27d8e064 100644 --- a/react/features/mobile/navigation/components/conference/components/ConferenceNavigationContainer.js +++ b/react/features/mobile/navigation/components/conference/components/ConferenceNavigationContainer.js @@ -10,6 +10,7 @@ import { Chat } from '../../../../../chat'; import Conference from '../../../../../conference/components/native/Conference'; import { getDisablePolls } from '../../../../../conference/functions'; import { SharedDocument } from '../../../../../etherpad'; +import { GifsMenu } from '../../../../../gifs/components'; import AddPeopleDialog from '../../../../../invite/components/add-people-dialog/native/AddPeopleDialog'; import LobbyScreen from '../../../../../lobby/components/native/LobbyScreen'; @@ -27,6 +28,7 @@ import { screen } from '../../../routes'; import { chatScreenOptions, conferenceScreenOptions, + gifsMenuOptions, inviteScreenOptions, liveStreamScreenOptions, lobbyScreenOptions, @@ -124,6 +126,13 @@ const ConferenceNavigationContainer = () => { ...salesforceScreenOptions, title: t('notify.linkToSalesforce') }} /> + - {REACTIONS[reaction].emoji} + {children ?? {REACTIONS[reaction].emoji}} ); } diff --git a/react/features/reactions/components/native/ReactionMenu.js b/react/features/reactions/components/native/ReactionMenu.js index f07bb40cb..98a1d26e6 100644 --- a/react/features/reactions/components/native/ReactionMenu.js +++ b/react/features/reactions/components/native/ReactionMenu.js @@ -1,10 +1,13 @@ // @flow -import React from 'react'; -import { View } from 'react-native'; +import React, { useCallback } from 'react'; +import { Image, View } from 'react-native'; import { useSelector } from 'react-redux'; import { ColorSchemeRegistry } from '../../../base/color-scheme'; +import { isGifEnabled } from '../../../gifs/functions'; +import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef'; +import { screen } from '../../../mobile/navigation/routes'; import { REACTIONS } from '../../constants'; import RaiseHandButton from './RaiseHandButton'; @@ -36,6 +39,12 @@ function ReactionMenu({ overflowMenu }: Props) { const _styles = useSelector(state => ColorSchemeRegistry.get(state, 'Toolbox')); + const gifEnabled = useSelector(isGifEnabled); + + const openGifMenu = useCallback(() => { + navigate(screen.conference.gifsMenu); + onCancel(); + }, []); return ( @@ -46,6 +55,15 @@ function ReactionMenu({ reaction = { key } styles = { _styles.reactionButton } /> ))} + {gifEnabled && ( + + + + )} diff --git a/react/features/toolbox/components/native/styles.js b/react/features/toolbox/components/native/styles.js index 4a3e4487f..0a5417048 100644 --- a/react/features/toolbox/components/native/styles.js +++ b/react/features/toolbox/components/native/styles.js @@ -51,6 +51,11 @@ const reactionButton = { marginHorizontal: 0 }; +const gifButton = { + ...reactionButton, + backgroundColor: '#000' +}; + /** * The style of the emoji on the reaction buttons. */ @@ -161,6 +166,7 @@ ColorSchemeRegistry.register('Toolbox', { }, reactionButton: { + gifButton, style: reactionButton, underlayColor: BaseTheme.palette.ui13, emoji: reactionEmoji