Merge pull request #23 from daily-demos/dailyjs/flying-emojis
Flying emojis 🦑
This commit is contained in:
commit
56a30937c7
|
|
@ -12,6 +12,10 @@ Send messages to other participants using sendAppMessage
|
|||
|
||||
Broadcast call to a custom RTMP endpoint using a variety of different layout modes
|
||||
|
||||
### [🔥 Flying emojis](./flying-emojis)
|
||||
|
||||
Send emoji reactions to all clients using sendAppMessage
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": ["inline-react-svg"]
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# Flying Emojis
|
||||
|
||||

|
||||
|
||||
### Live example
|
||||
|
||||
**[See it in action here ➡️](https://dailyjs-flying-emojis.vercel.app)**
|
||||
|
||||
---
|
||||
|
||||
## What does this demo do?
|
||||
|
||||
- Use [sendAppMessage](https://docs.daily.co/reference#%EF%B8%8F-sendappmessage) to send flying emojis to all clients
|
||||
- Implements a custom `<App />` that adds `<FlyingEmojisOverlay />` component that listens for incoming emoji events and appends a new node to the DOM
|
||||
- Todo: pool emoji DOM nodes to optimise on DOM mutations
|
||||
|
||||
Please note: this demo is not currently mobile optimised
|
||||
|
||||
### Getting started
|
||||
|
||||
```
|
||||
# set both DAILY_API_KEY and DAILY_DOMAIN
|
||||
mv env.example .env.local
|
||||
|
||||
yarn
|
||||
yarn workspace @dailyjs/flying-emojis dev
|
||||
```
|
||||
|
||||
## Deploy your own on Vercel
|
||||
|
||||
[](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import App from '@dailyjs/basic-call/components/App';
|
||||
import FlyingEmojiOverlay from '../FlyingEmojis/FlyingEmojisOverlay';
|
||||
|
||||
export const AppWithEmojis = () => (
|
||||
<>
|
||||
<FlyingEmojiOverlay />
|
||||
<App />
|
||||
</>
|
||||
);
|
||||
|
||||
export default AppWithEmojis;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { AppWithEmojis as default } from './App';
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import React, { useEffect, useRef, useCallback } from 'react';
|
||||
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||
|
||||
const EMOJI_MAP = {
|
||||
fire: '🔥',
|
||||
squid: '🦑',
|
||||
laugh: '🤣',
|
||||
};
|
||||
|
||||
export const FlyingEmojisOverlay = () => {
|
||||
const { callObject } = useCallState();
|
||||
const overlayRef = useRef();
|
||||
|
||||
// -- Handlers
|
||||
|
||||
const handleRemoveFlyingEmoji = useCallback((node) => {
|
||||
if (!overlayRef.current) return;
|
||||
overlayRef.current.removeChild(node);
|
||||
}, []);
|
||||
|
||||
const handleNewFlyingEmoji = useCallback(
|
||||
(emoji) => {
|
||||
if (!overlayRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`⭐ Displaying flying emoji: ${emoji}`);
|
||||
|
||||
const node = document.createElement('div');
|
||||
node.appendChild(document.createTextNode(EMOJI_MAP[emoji]));
|
||||
node.className =
|
||||
Math.random() * 1 > 0.5 ? 'emoji wiggle-1' : 'emoji wiggle-2';
|
||||
node.style.transform = `rotate(${-30 + Math.random() * 60}deg)`;
|
||||
node.style.left = `${Math.random() * 100}%`;
|
||||
node.src = '';
|
||||
overlayRef.current.appendChild(node);
|
||||
|
||||
node.addEventListener('animationend', (e) =>
|
||||
handleRemoveFlyingEmoji(e.target)
|
||||
);
|
||||
},
|
||||
[handleRemoveFlyingEmoji]
|
||||
);
|
||||
|
||||
// -- Effects
|
||||
|
||||
// Listen for new app messages and show new flying emojis
|
||||
useEffect(() => {
|
||||
if (!callObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`⭐ Listening for flying emojis...`);
|
||||
|
||||
callObject.on('app-message', handleNewFlyingEmoji);
|
||||
|
||||
return () => callObject.off('app-message', handleNewFlyingEmoji);
|
||||
}, [callObject, handleNewFlyingEmoji]);
|
||||
|
||||
// Listen to window events to show local user emojis
|
||||
useEffect(() => {
|
||||
if (!callObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleIncomingEmoji(e) {
|
||||
const { emoji } = e.detail;
|
||||
console.log(`⭐ Sending flying emoji: ${emoji}`);
|
||||
|
||||
if (emoji) {
|
||||
callObject.sendAppMessage({ emoji }, '*');
|
||||
handleNewFlyingEmoji(emoji);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('reaction_added', handleIncomingEmoji);
|
||||
return () =>
|
||||
window.removeEventListener('reaction_added', handleIncomingEmoji);
|
||||
}, [callObject, handleNewFlyingEmoji]);
|
||||
|
||||
// Remove all event listeners on unmount to prevent console warnings
|
||||
useEffect(
|
||||
() => () =>
|
||||
overlayRef.current.childNodes.forEach((n) =>
|
||||
n.removeEventListener('animationend', handleRemoveFlyingEmoji)
|
||||
),
|
||||
[handleRemoveFlyingEmoji]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flying-emojis" ref={overlayRef}>
|
||||
<style jsx>{`
|
||||
.flying-emojis {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.flying-emojis :global(.emoji) {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 50%;
|
||||
font-size: 48px;
|
||||
line-height: 1;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.flying-emojis :global(.emoji.wiggle-1) {
|
||||
animation: emerge 3s forwards,
|
||||
wiggle-1 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.flying-emojis :global(.emoji.wiggle-2) {
|
||||
animation: emerge 3s forwards,
|
||||
wiggle-2 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes emerge {
|
||||
to {
|
||||
bottom: 85%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wiggle-1 {
|
||||
from {
|
||||
margin-left: -50px;
|
||||
}
|
||||
to {
|
||||
margin-left: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wiggle-2 {
|
||||
from {
|
||||
margin-left: 50px;
|
||||
}
|
||||
to {
|
||||
margin-left: -50px;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FlyingEmojisOverlay;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { FlyingEmojisOverlay } from './FlyingEmojisOverlay';
|
||||
export { FlyingEmojisOverlay as default } from './FlyingEmojisOverlay';
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Button from '@dailyjs/shared/components/Button';
|
||||
import { TrayButton } from '@dailyjs/shared/components/Tray';
|
||||
import { ReactComponent as IconStar } from '@dailyjs/shared/icons/star-md.svg';
|
||||
|
||||
const COOLDOWN = 1500;
|
||||
|
||||
export const Tray = () => {
|
||||
const [showEmojis, setShowEmojis] = useState(false);
|
||||
const [isThrottled, setIsThrottled] = useState(false);
|
||||
|
||||
function sendEmoji(emoji) {
|
||||
// Dispatch custom event here so the local user can see their own emoji
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('reaction_added', { detail: { emoji } })
|
||||
);
|
||||
setShowEmojis(false);
|
||||
setIsThrottled(true);
|
||||
}
|
||||
|
||||
// Pseudo-throttling (should ideally be done serverside)
|
||||
useEffect(() => {
|
||||
if (!isThrottled) {
|
||||
return false;
|
||||
}
|
||||
const t = setTimeout(() => setIsThrottled(false), COOLDOWN);
|
||||
return () => clearTimeout(t);
|
||||
}, [isThrottled]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showEmojis && (
|
||||
<div className="emojis">
|
||||
<Button
|
||||
variant="outline-gray"
|
||||
size="small-square"
|
||||
onClick={() => sendEmoji('fire')}
|
||||
>
|
||||
🔥
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-gray"
|
||||
size="small-square"
|
||||
onClick={() => sendEmoji('squid')}
|
||||
>
|
||||
🦑
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-gray"
|
||||
size="small-square"
|
||||
onClick={() => sendEmoji('laugh')}
|
||||
>
|
||||
🤣
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<TrayButton
|
||||
label="Emoji"
|
||||
onClick={() => setShowEmojis(!showEmojis)}
|
||||
disabled={isThrottled}
|
||||
>
|
||||
<IconStar />
|
||||
</TrayButton>
|
||||
<style jsx>{`
|
||||
position: relative;
|
||||
|
||||
.emojis {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: calc(-100% + var(--spacing-xs));
|
||||
left: 0px;
|
||||
transform: translateX(calc(-50% + 26px));
|
||||
z-index: 99;
|
||||
background: white;
|
||||
padding: var(--spacing-xxxs);
|
||||
column-gap: 5px;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-depth-2);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tray;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { Tray as default } from './Tray';
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
|
||||
DAILY_DOMAIN=
|
||||
|
||||
# Obtained from https://dashboard.daily.co/developers
|
||||
DAILY_API_KEY=
|
||||
|
||||
# Daily REST API endpoint
|
||||
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
const withPlugins = require('next-compose-plugins');
|
||||
const withTM = require('next-transpile-modules')([
|
||||
'@dailyjs/shared',
|
||||
'@dailyjs/basic-call',
|
||||
]);
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
module.exports = withPlugins([withTM], {
|
||||
env: {
|
||||
PROJECT_TITLE: packageJson.description,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "@dailyjs/flying-emojis",
|
||||
"description": "Basic Call + Flying Emojis",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dailyjs/basic-call": "*",
|
||||
"@dailyjs/shared": "*",
|
||||
"next": "^11.0.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"next-compose-plugins": "^2.2.1",
|
||||
"next-transpile-modules": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import App from '@dailyjs/basic-call/pages/_app';
|
||||
import AppWithEmojis from '../components/App';
|
||||
import Tray from '../components/Tray';
|
||||
|
||||
App.customAppComponent = <AppWithEmojis />;
|
||||
App.customTrayComponent = <Tray />;
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../basic-call/pages/api
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import Index from '@dailyjs/basic-call/pages';
|
||||
import getDemoProps from '@dailyjs/shared/lib/demoProps';
|
||||
|
||||
export async function getStaticProps() {
|
||||
const defaultProps = getDemoProps();
|
||||
|
||||
// Pass through domain as prop
|
||||
return {
|
||||
props: defaultProps,
|
||||
};
|
||||
}
|
||||
|
||||
export default Index;
|
||||
|
|
@ -0,0 +1 @@
|
|||
../basic-call/public
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
|
||||
DAILY_DOMAIN=
|
||||
|
||||
# Obtained from https://dashboard.daily.co/developers
|
||||
DAILY_API_KEY=
|
||||
|
||||
# Daily REST API endpoint
|
||||
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||
|
|
@ -286,6 +286,9 @@ export const Button = forwardRef(
|
|||
.button.dark:focus {
|
||||
box-shadow: 0 0 0px 3px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.button.dark:disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.button.outline {
|
||||
background: transparent;
|
||||
|
|
|
|||
|
|
@ -9,11 +9,17 @@ export const TrayButton = ({
|
|||
onClick,
|
||||
bubble = false,
|
||||
orange = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const cx = classNames('tray-button', { orange, bubble });
|
||||
return (
|
||||
<div className={cx}>
|
||||
<Button onClick={() => onClick()} variant="dark" size="large-square">
|
||||
<Button
|
||||
onClick={() => onClick()}
|
||||
variant="dark"
|
||||
size="large-square"
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
<span>{label}</span>
|
||||
|
|
@ -57,6 +63,7 @@ TrayButton.propTypes = {
|
|||
orange: PropTypes.bool,
|
||||
bubble: PropTypes.bool,
|
||||
label: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const Tray = ({ children }) => (
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="none" stroke="currentColor"><polygon points="12,2.49 15.09,8.75 22,9.754 17,14.628 18.18,21.51 12,18.262 5.82,21.51 7,14.628 2,9.754 8.91,8.75 "></polygon></g></svg>
|
||||
|
After Width: | Height: | Size: 321 B |
|
|
@ -0,0 +1,8 @@
|
|||
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
|
||||
DAILY_DOMAIN=
|
||||
|
||||
# Obtained from https://dashboard.daily.co/developers
|
||||
DAILY_API_KEY=
|
||||
|
||||
# Daily REST API endpoint
|
||||
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||
27
yarn.lock
27
yarn.lock
|
|
@ -264,6 +264,11 @@
|
|||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@popperjs/core@^2.9.2":
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
|
||||
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
|
||||
|
||||
"@rushstack/eslint-patch@^1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.6.tgz#023d72a5c4531b4ce204528971700a78a85a0c50"
|
||||
|
|
@ -2204,7 +2209,7 @@ lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21:
|
|||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
|
|
@ -2897,6 +2902,11 @@ react-dom@^17.0.2:
|
|||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-fast-compare@^3.0.1:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||
|
||||
react-is@17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
|
|
@ -2907,6 +2917,14 @@ react-is@^16.8.1:
|
|||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-popper@^2.2.5:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96"
|
||||
integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw==
|
||||
dependencies:
|
||||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-refresh@0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||
|
|
@ -3616,6 +3634,13 @@ vm-browserify@1.1.2, vm-browserify@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
||||
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
|
||||
|
||||
warning@^4.0.2:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
watchpack@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.1.tgz#e99630550fca07df9f90a06056987baa40a689c7"
|
||||
|
|
|
|||
Loading…
Reference in New Issue