Last active
June 23, 2025 04:46
-
-
Save naporin0624/f60c8932990d64deabe9c7d925536ed2 to your computer and use it in GitHub Desktop.
Toast Emoji UI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app.tsx | |
import "./styles.css"; | |
import { Subject } from "rxjs"; | |
import { useEffect } from "react"; | |
import getEmoji from "@0xadada/random-emoji"; | |
import { emoji, EmojiProvider } from "./emoji"; | |
// websocket からやってくる stream | |
const stream = new Subject(); | |
export default function App() { | |
useEffect(() => { | |
const subscription = stream.subscribe(() => { | |
emoji.send(getEmoji()); | |
}); | |
return () => { | |
subscription.unsubscribe(); | |
}; | |
}, []); | |
return ( | |
<EmojiProvider> | |
<div className="App"> | |
<button onClick={() => stream.next(Date.now())}>click</button> | |
</div> | |
</EmojiProvider> | |
); | |
} | |
// emoji.tsx | |
import { animated, useTransition } from "@react-spring/web"; | |
import { PropsWithChildren, FC, useEffect, useMemo } from "react"; | |
import { BehaviorSubject } from "rxjs"; | |
import { useObservable } from "./hooks"; | |
import { gid } from "./utils"; | |
type EmojiStore = { | |
[id: string]: string; | |
}; | |
const store = new BehaviorSubject<EmojiStore>({}); | |
export const emoji = { | |
send(value: string) { | |
store.next({ ...store.getValue(), [gid()]: value }); | |
}, | |
remove(gid: string) { | |
const v = store.getValue(); | |
const { [gid]: a, ...rest } = v; | |
store.next(rest); | |
}, | |
}; | |
type EmojiFuwaFuwaProps = PropsWithChildren<{ | |
id: string; | |
style?: any; // 型調べるのがめんどくさかった | |
}>; | |
const EmojiFuwaFuwa: FC<EmojiFuwaFuwaProps> = ({ id, children, style }) => { | |
useEffect(() => { | |
const _id = setTimeout(() => { | |
emoji.remove(id); | |
}, 300); | |
return () => { | |
clearTimeout(_id); | |
}; | |
}, [id]); | |
return ( | |
<animated.div | |
style={{ | |
...style, | |
position: "absolute", | |
left: useMemo(() => Math.floor(Math.random() * 50), []), | |
}} | |
> | |
{children} | |
</animated.div> | |
); | |
}; | |
type EmojiProviderPrpos = PropsWithChildren; | |
export const EmojiProvider: FC<EmojiProviderPrpos> = ({ children }) => { | |
const emojis = useObservable(store); | |
const data = Object.entries(emojis).map(([key, value]) => ({ | |
id: key, | |
value, | |
})); | |
console.log(data); | |
const transition = useTransition(data, { | |
trail: 400 / data.length, | |
keys: (a) => a.id, | |
from: { opacity: 0, scale: 1, bottom: "0px" }, | |
enter: { opacity: 1, scale: 1.5, bottom: "100px" }, | |
leave: { opacity: 0, scale: 1, bottom: "200px" }, | |
}); | |
return ( | |
<div> | |
<div | |
style={{ | |
position: "fixed", | |
zIndex: 0, | |
top: 0, | |
left: 0, | |
width: "100vw", | |
height: "100vh", | |
}} | |
> | |
{transition((style, item) => ( | |
<EmojiFuwaFuwa style={style} key={item.id} id={item.id}> | |
{item.value} | |
</EmojiFuwaFuwa> | |
))} | |
</div> | |
<div style={{ position: "relative", zIndex: 1 }}>{children}</div> | |
</div> | |
); | |
}; | |
// hooks.ts | |
import { useSyncExternalStore } from "react"; | |
import { BehaviorSubject } from "rxjs"; | |
export const useObservable = <T extends unknown>( | |
observable: BehaviorSubject<T> | |
) => { | |
return useSyncExternalStore( | |
(onStoreChange) => { | |
const s = observable.subscribe(onStoreChange); | |
return () => s.unsubscribe(); | |
}, | |
() => observable.getValue() | |
); | |
}; | |
// utils.ts | |
let count = 0; | |
export const gid = () => { | |
return btoa(`GlobalId:${++count}`); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment