Merge pull request #23 from daily-demos/dailyjs/flying-emojis

Flying emojis 🦑
This commit is contained in:
Jon Taylor 2021-07-14 15:48:48 +01:00 committed by GitHub
commit 56a30937c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 419 additions and 2 deletions

View File

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

View File

@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": ["inline-react-svg"]
}

View File

@ -0,0 +1,31 @@
# Flying Emojis
![Flying Emojis](./image.png)
### 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
[![Deploy with Vercel](https://vercel.com/button)](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)

View File

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

View File

@ -0,0 +1 @@
export { AppWithEmojis as default } from './App';

View File

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

View File

@ -0,0 +1,2 @@
export { FlyingEmojisOverlay } from './FlyingEmojisOverlay';
export { FlyingEmojisOverlay as default } from './FlyingEmojisOverlay';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
../../basic-call/pages/api

View File

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

View File

@ -0,0 +1 @@
../basic-call/public

View File

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

View File

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

View File

@ -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 }) => (

View File

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

View File

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

View File

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