feat(gif, rn) Added GIPHY integration on native (#11236)

Update Android build to support gif
Use GIF format instead of animated webp
Show GIFs in chat messages
Display GIF over tile
Add Giphy button in reactions menu
Added Giphy dialog
Fix isGifMessage to also allow upper case
This commit is contained in:
Robert Pintilii 2022-03-30 16:54:03 +03:00 committed by GitHub
parent 1a3432d580
commit 1355876f83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 362 additions and 25 deletions

View File

@ -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')

View File

@ -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(),

View File

@ -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'

View File

@ -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

View File

@ -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 = "<group>"; };
E58801132278944E008B0561 /* JitsiMeetContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetContext.swift; sourceTree = "<group>"; };
E5C97B62227A1EB400199214 /* JitsiMeetCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetCommands.swift; sourceTree = "<group>"; };
FD572B9727EDF32300A800FB /* GiphyUISDK.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = GiphyUISDK.xcframework; path = ../Pods/Giphy/GiphySDK/GiphyUISDK.xcframework; sourceTree = "<group>"; };
/* 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 */,

View File

@ -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",

41
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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<Props> {
// 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(<GifMessage
key = { url }

View File

@ -9,10 +9,12 @@ import { translate } from '../../../base/i18n';
import { Linkify } from '../../../base/react';
import { connect } from '../../../base/redux';
import { type StyleType } from '../../../base/styles';
import { isGifMessage } from '../../../gifs/functions';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
import { replaceNonUnicodeEmojis } from '../../functions';
import AbstractChatMessage, { type Props as AbstractProps } from '../AbstractChatMessage';
import GifMessage from './GifMessage';
import PrivateMessageButton from './PrivateMessageButton';
import styles from './styles';
@ -75,6 +77,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
messageBubbleStyle.push(_styles.lobbyMessageBubble);
}
const messageText = replaceNonUnicodeEmojis(this._getMessageText());
return (
<View style = { styles.messageWrapper } >
{ this._renderAvatar() }
@ -82,9 +86,13 @@ class ChatMessage extends AbstractChatMessage<Props> {
<View style = { messageBubbleStyle }>
<View style = { styles.textWrapper } >
{ this._renderDisplayName() }
<Linkify linkStyle = { styles.chatLink }>
{ replaceNonUnicodeEmojis(this._getMessageText()) }
</Linkify>
{isGifMessage(messageText)
? <GifMessage message = { messageText } />
: (
<Linkify linkStyle = { styles.chatLink }>
{messageText}
</Linkify>
)}
{ this._renderPrivateNotice() }
</View>
{ this._renderPrivateReplyButton() }

View File

@ -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 (<View
style = { styles.gifContainer }>
<Image
source = {{ uri: url }}
style = { styles.gifImage } />
</View>);
};
export default GifMessage;

View File

@ -145,6 +145,17 @@ export default {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
borderBottomRightRadius: 0
},
gifContainer: {
maxHeight: 150
},
gifImage: {
resizeMode: 'contain',
width: 250,
height: undefined,
flexGrow: 1
}
};

View File

@ -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<Props> {
*/
render() {
const {
_gifSrc,
_isScreenShare: isScreenShare,
_isFakeParticipant,
_participantId: participantId,
@ -280,15 +287,21 @@ class Thumbnail extends PureComponent<Props> {
_renderDominantSpeakerIndicator ? styles.thumbnailDominantSpeaker : null
] }
touchFeedback = { false }>
<ParticipantView
avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
disableVideo = { isScreenShare || _isFakeParticipant }
participantId = { participantId }
tintEnabled = { participantInLargeVideo && !disableTint }
tintStyle = { _styles.activeThumbnailTint }
zOrder = { 1 } />
{
this._renderIndicators()
{_gifSrc ? <Image
source = {{ uri: _gifSrc }}
style = { styles.thumbnailGif } />
: <>
<ParticipantView
avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
disableVideo = { isScreenShare || _isFakeParticipant }
participantId = { participantId }
tintEnabled = { participantInLargeVideo && !disableTint }
tintStyle = { _styles.activeThumbnailTint }
zOrder = { 1 } />
{
this._renderIndicators()
}
</>
}
</Container>
);
@ -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,

View File

@ -171,6 +171,11 @@ export default {
thumbnailDominantSpeaker: {
borderWidth: 4,
borderColor: BaseTheme.palette.action01Hover
},
thumbnailGif: {
flexGrow: 1,
resizeMode: 'contain'
}
};

View File

@ -0,0 +1 @@
export * from './native';

View File

@ -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 (<JitsiScreen
style = { styles.container }>
<ClearableInput
autoFocus = { true }
customStyles = { styles.clearableInput }
onChange = { setSearchQuery }
placeholder = 'Search GIPHY'
value = { searchQuery } />
<GiphyGridView
cellPadding = { 5 }
content = { content }
onMediaSelect = { sendGif }
onScroll = { onScroll }
style = { styles.grid } />
<View
style = { [ styles.credit, {
bottom: insets.bottom,
left: insets.left,
right: insets.right
} ] }>
<Text
style = { styles.creditText }>Powered by</Text>
<Image source = { require('../../../../../images/GIPHY_logo.png') } />
</View>
</JitsiScreen>);
};
export default GifsMenu;

View File

@ -0,0 +1,3 @@
// @flow
export { default as GifsMenu } from './GifsMenu';

View File

@ -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'
}
};

View File

@ -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`;
}
/**

View File

@ -0,0 +1,3 @@
import './middleware.any';
import './subscriber.native';

View File

@ -0,0 +1,2 @@
import './middleware.any';

View File

@ -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
});

View File

@ -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')
}} />
<ConferenceStack.Screen
component = { GifsMenu }
name = { screen.conference.gifsMenu }
options = {{
...gifsMenuOptions,
title: t('notify.gifsMenu')
}} />
<ConferenceStack.Screen
component = { LobbyScreen }
name = { screen.lobby }

View File

@ -30,6 +30,7 @@ export const screen = {
speakerStats: 'Speaker Stats',
salesforce: 'Link to Salesforce',
participants: 'Participants',
gifsMenu: 'GIPHY',
invite: 'Invite',
sharedDocument: 'Shared document'
},

View File

@ -278,6 +278,11 @@ export const liveStreamScreenOptions = {
*/
export const salesforceScreenOptions = presentationScreenOptions;
/**
* Screen options for GIPHY integration modal.
*/
export const gifsMenuOptions = presentationScreenOptions;
/**
* Screen options for shared document.
*/

View File

@ -13,6 +13,11 @@ import { REACTIONS } from '../../constants';
export type ReactionStyles = {
/**
* Style for the gif button.
*/
gifButton: StyleType,
/**
* Style for the button.
*/
@ -45,6 +50,16 @@ export type ReactionStyles = {
*/
type Props = {
/**
* Component children.
*/
children?: ReactNode,
/**
* External click handler.
*/
onClick?: Function,
/**
* Collection of styles for the button.
*/
@ -67,6 +82,8 @@ type Props = {
* @returns {ReactElement}
*/
function ReactionButton({
children,
onClick,
styles,
reaction,
t
@ -81,10 +98,10 @@ function ReactionButton({
<TouchableHighlight
accessibilityLabel = { t(`toolbar.accessibilityLabel.${reaction}`) }
accessibilityRole = 'button'
onPress = { _onClick }
style = { styles.style }
onPress = { onClick || _onClick }
style = { [ styles.style, children && styles?.gifButton ] }
underlayColor = { styles.underlayColor }>
<Text style = { styles.emoji }>{REACTIONS[reaction].emoji}</Text>
{children ?? <Text style = { styles.emoji }>{REACTIONS[reaction].emoji}</Text>}
</TouchableHighlight>
);
}

View File

@ -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 (
<View style = { overflowMenu ? _styles.overflowReactionMenu : _styles.reactionMenu }>
@ -46,6 +55,15 @@ function ReactionMenu({
reaction = { key }
styles = { _styles.reactionButton } />
))}
{gifEnabled && (
<ReactionButton
onClick = { openGifMenu }
styles = { _styles.reactionButton }>
<Image
height = { 22 }
source = { require('../../../../../images/GIPHY_icon.png') } />
</ReactionButton>
)}
</View>
<RaiseHandButton onCancel = { onCancel } />
</View>

View File

@ -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