Skip to content

Instantly share code, notes, and snippets.

@naporin0624
Last active June 23, 2025 04:46
Show Gist options
  • Save naporin0624/f60c8932990d64deabe9c7d925536ed2 to your computer and use it in GitHub Desktop.
Save naporin0624/f60c8932990d64deabe9c7d925536ed2 to your computer and use it in GitHub Desktop.
Toast Emoji UI
// 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