initial commit
This commit is contained in:
parent
5d68418d81
commit
30e77c985c
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"presets": ["next/babel"],
|
||||||
|
"plugins": ["inline-react-svg"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Text Chat
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Live example
|
||||||
|
|
||||||
|
**[See it in action here ➡️](https://dailyjs-text-chat.vercel.app)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What does this demo do?
|
||||||
|
|
||||||
|
- Use [sendAppMessage](https://docs.daily.co/reference#%EF%B8%8F-sendappmessage) to send messages
|
||||||
|
- Listen for incoming messages using the call object `app-message` event
|
||||||
|
- Extend the basic call demo with a chat provider and aside
|
||||||
|
- Show a notification bubble on chat tray button when a new message is received
|
||||||
|
- Demonstrate how to play a sound whenever a message is received
|
||||||
|
|
||||||
|
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/text-chat dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## How does this example work?
|
||||||
|
|
||||||
|
In this example we extend the [basic call demo](../basic-call) with the ability to send chat messages.
|
||||||
|
|
||||||
|
We pass a custom tray object, a custom app object (wrapping the original in a new `ChatProvider`) as well as add our `ChatAside` panel. We also symlink both the `public` and `pages/api` folders from the basic call.
|
||||||
|
|
||||||
|
In a real world use case you would likely want to implement serverside logic so that participants joining a call can retrieve previously sent messages. This round trip could be done inside of the Chat context.
|
||||||
|
|
||||||
|
## 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,153 @@
|
||||||
|
import React, { useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||||
|
|
||||||
|
const EMOJI_MAP = {
|
||||||
|
fire: '🔥',
|
||||||
|
squid: '🦑',
|
||||||
|
};
|
||||||
|
|
||||||
|
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(`⭐ New 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: 0px;
|
||||||
|
right: 0px;
|
||||||
|
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: 300px;
|
||||||
|
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,44 @@
|
||||||
|
import React, { 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';
|
||||||
|
|
||||||
|
export const Tray = () => {
|
||||||
|
const [showEmojis, setShowEmojis] = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{showEmojis && (
|
||||||
|
<div className="emojis">
|
||||||
|
<Button onClick={() => sendEmoji('fire')}>A</Button>
|
||||||
|
<Button onClick={() => sendEmoji('squid')}>B</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<TrayButton label="Emoji" onClick={() => setShowEmojis(!showEmojis)}>
|
||||||
|
<IconStar />
|
||||||
|
</TrayButton>
|
||||||
|
<style jsx>{`
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.emojis {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
top: -50px;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tray;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { Tray as default } from './Tray';
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 135 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 @@
|
||||||
|
<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 |
27
yarn.lock
27
yarn.lock
|
|
@ -264,6 +264,11 @@
|
||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
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":
|
"@rushstack/eslint-patch@^1.0.6":
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.6.tgz#023d72a5c4531b4ce204528971700a78a85a0c50"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
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"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||||
|
|
@ -2897,6 +2902,11 @@ react-dom@^17.0.2:
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
scheduler "^0.20.2"
|
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:
|
react-is@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
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"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
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:
|
react-refresh@0.8.3:
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
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"
|
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
||||||
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
|
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:
|
watchpack@2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.1.tgz#e99630550fca07df9f90a06056987baa40a689c7"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.1.1.tgz#e99630550fca07df9f90a06056987baa40a689c7"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue