notification-sound
not working in safari
This commit is contained in:
parent
d135674e46
commit
d1fe43463e
BIN
public/audio/notification-new-message-1.mp3
Normal file
BIN
public/audio/notification-new-message-1.mp3
Normal file
Binary file not shown.
@ -4,14 +4,13 @@ import {
|
||||
ChatModalsWrapper,
|
||||
MessageInputWrapper,
|
||||
} from "@/components/domains/chat";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
export default function Chat() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ChatHeader chatsPromise={loadChatsList()} />
|
||||
<ChatHeader />
|
||||
<ChatMessagesWrapper />
|
||||
<MessageInputWrapper />
|
||||
<ChatModalsWrapper />
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
NewMessagesWrapperSkeleton,
|
||||
} from "@/components/domains/chat";
|
||||
import { NavigationBar } from "@/components/layout";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
|
||||
import styles from "./page.module.scss";
|
||||
|
||||
@ -19,23 +18,21 @@ export const revalidate = 0;
|
||||
export const fetchCache = "force-no-store";
|
||||
|
||||
export default function Chats() {
|
||||
const chatsPromise = loadChatsList();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ChatListHeader />
|
||||
<section className={styles.categories}>
|
||||
<Suspense fallback={<NewMessagesWrapperSkeleton />}>
|
||||
<NewMessagesWrapper chatsPromise={chatsPromise} />
|
||||
<NewMessagesWrapper />
|
||||
</Suspense>
|
||||
<Suspense fallback={<CorrespondenceStartedSkeleton />}>
|
||||
<CorrespondenceStartedWrapper chatsPromise={chatsPromise} />
|
||||
<CorrespondenceStartedWrapper />
|
||||
</Suspense>
|
||||
<Suspense fallback={<ChatCategoriesSkeleton />}>
|
||||
<ChatCategories chatsPromise={chatsPromise} />
|
||||
<ChatCategories />
|
||||
</Suspense>
|
||||
</section>
|
||||
<NavigationBar chatsPromise={chatsPromise} />
|
||||
<NavigationBar />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { DrawerProvider, Header, NavigationBar } from "@/components/layout";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
import { ChatStoreProvider } from "@/providers/chat-store-provider";
|
||||
|
||||
import styles from "./layout.module.scss";
|
||||
@ -9,13 +8,12 @@ export default function CoreLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const chatsPromise = loadChatsList();
|
||||
return (
|
||||
<DrawerProvider>
|
||||
<ChatStoreProvider>
|
||||
<Header className={styles.navBar} chatsPromise={chatsPromise} />
|
||||
<Header className={styles.navBar} />
|
||||
<main className={styles.main}>{children}</main>
|
||||
<NavigationBar chatsPromise={chatsPromise} />
|
||||
<NavigationBar />
|
||||
</ChatStoreProvider>
|
||||
</DrawerProvider>
|
||||
);
|
||||
|
||||
@ -28,7 +28,7 @@ export default function Home() {
|
||||
return (
|
||||
<section className={styles.page}>
|
||||
<Suspense fallback={<NewMessagesSectionSkeleton />}>
|
||||
<NewMessagesSection chatsPromise={chatsPromise} />
|
||||
<NewMessagesSection />
|
||||
</Suspense>
|
||||
|
||||
<Horoscope />
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { DrawerProvider, Header } from "@/components/layout";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
|
||||
import styles from "./layout.module.scss";
|
||||
|
||||
@ -10,7 +9,7 @@ export default function CoreLayout({
|
||||
}>) {
|
||||
return (
|
||||
<DrawerProvider>
|
||||
<Header className={styles.navBar} chatsPromise={loadChatsList()} />
|
||||
<Header className={styles.navBar} />
|
||||
<main className={styles.main}>{children}</main>
|
||||
</DrawerProvider>
|
||||
);
|
||||
|
||||
@ -10,10 +10,12 @@ import { getMessages } from "next-intl/server";
|
||||
import clsx from "clsx";
|
||||
|
||||
import YandexMetrika from "@/components/analytics/YandexMetrika";
|
||||
import { loadChatsList } from "@/entities/chats/loaders";
|
||||
import { loadUser, loadUserId } from "@/entities/user/loaders";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { AppUiStoreProvider } from "@/providers/app-ui-store-provider";
|
||||
import { ChatsInitializationProvider } from "@/providers/chats-initialization-provider";
|
||||
import { ChatsProvider } from "@/providers/chats-provider";
|
||||
import { RetainingStoreProvider } from "@/providers/retaining-store-provider";
|
||||
import SocketProvider from "@/providers/socket-provider";
|
||||
import { ToastProvider } from "@/providers/toast-provider";
|
||||
@ -60,6 +62,7 @@ export default async function RootLayout({
|
||||
|
||||
const user = await loadUser();
|
||||
const userId = await loadUserId();
|
||||
const chats = await loadChatsList();
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
@ -70,9 +73,11 @@ export default async function RootLayout({
|
||||
<SocketProvider userId={userId}>
|
||||
<RetainingStoreProvider>
|
||||
<ChatsInitializationProvider>
|
||||
<ToastProvider maxVisible={3}>
|
||||
<AppUiStoreProvider>{children}</AppUiStoreProvider>
|
||||
</ToastProvider>
|
||||
<ChatsProvider initialChats={chats}>
|
||||
<ToastProvider maxVisible={3}>
|
||||
<AppUiStoreProvider>{children}</AppUiStoreProvider>
|
||||
</ToastProvider>
|
||||
</ChatsProvider>
|
||||
</ChatsInitializationProvider>
|
||||
</RetainingStoreProvider>
|
||||
</SocketProvider>
|
||||
|
||||
@ -1,23 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { Chips } from "@/components/widgets";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
|
||||
import { CategoryChats, ChatItemsList } from "..";
|
||||
|
||||
const MAX_HIDE_VISIBLE_COUNT = 3;
|
||||
|
||||
interface ChatCategoriesProps {
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function ChatCategories({ chatsPromise }: ChatCategoriesProps) {
|
||||
const chats = use(chatsPromise);
|
||||
const { categorizedChats } = useChatsSocket({ initialChats: chats });
|
||||
export default function ChatCategories() {
|
||||
const { categorizedChats } = useChats();
|
||||
|
||||
const [activeChip, setActiveChip] = useState<string>("All");
|
||||
const [maxVisibleChats, setMaxVisibleChats] = useState<
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { use, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@ -13,26 +13,20 @@ import {
|
||||
UserAvatar,
|
||||
} from "@/components/ui";
|
||||
import { revalidateChatsPage } from "@/entities/chats/actions";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useChat } from "@/providers/chat-provider";
|
||||
import { useChatStore } from "@/providers/chat-store-provider";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
import { formatSecondsToHHMMSS } from "@/shared/utils/date";
|
||||
import { delay } from "@/shared/utils/delay";
|
||||
|
||||
import styles from "./ChatHeader.module.scss";
|
||||
|
||||
interface ChatHeaderProps {
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function ChatHeader({ chatsPromise }: ChatHeaderProps) {
|
||||
export default function ChatHeader() {
|
||||
const t = useTranslations("Chat");
|
||||
const router = useRouter();
|
||||
const currentChat = useChatStore(state => state.currentChat);
|
||||
const { isLoadingAdvisorMessage, isAvailableChatting } = useChat();
|
||||
const chats = use(chatsPromise);
|
||||
const { totalUnreadCount } = useChatsSocket({ initialChats: chats });
|
||||
const { totalUnreadCount } = useChats();
|
||||
const [timer, setTimer] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,25 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
|
||||
import { ChatItemsList, CorrespondenceStarted } from "..";
|
||||
|
||||
interface CorrespondenceStartedWrapperProps {
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function CorrespondenceStartedWrapper({
|
||||
chatsPromise,
|
||||
}: CorrespondenceStartedWrapperProps) {
|
||||
export default function CorrespondenceStartedWrapper() {
|
||||
const t = useTranslations("Chat");
|
||||
const chats = use(chatsPromise);
|
||||
const { startedChats } = useChatsSocket({ initialChats: chats });
|
||||
const { startedChats } = useChats();
|
||||
|
||||
const { isVisibleAll } = useAppUiStore(
|
||||
state => state.chats.correspondenceStarted
|
||||
|
||||
@ -1,25 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
|
||||
import { ChatItemsList, NewMessages } from "..";
|
||||
|
||||
interface NewMessagesWrapperProps {
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function NewMessagesWrapper({
|
||||
chatsPromise,
|
||||
}: NewMessagesWrapperProps) {
|
||||
export default function NewMessagesWrapper() {
|
||||
const t = useTranslations("Chat");
|
||||
const chats = use(chatsPromise);
|
||||
const { unreadChats } = useChatsSocket({ initialChats: chats });
|
||||
const { unreadChats } = useChats();
|
||||
|
||||
const { isVisibleAll } = useAppUiStore(state => state.chats.newMessages);
|
||||
const hasHydrated = useAppUiStore(state => state._hasHydrated);
|
||||
|
||||
@ -1,24 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
|
||||
import { NewMessages, ViewAll } from "@/components/domains/chat";
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useAppUiStore } from "@/providers/app-ui-store-provider";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
|
||||
import styles from "./NewMessagesSection.module.scss";
|
||||
|
||||
interface NewMessagesSectionProps {
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function NewMessagesSection({
|
||||
chatsPromise,
|
||||
}: NewMessagesSectionProps) {
|
||||
const chats = use(chatsPromise);
|
||||
const { unreadChats } = useChatsSocket({ initialChats: chats });
|
||||
export default function NewMessagesSection() {
|
||||
const { unreadChats } = useChats();
|
||||
|
||||
const { isVisibleAll } = useAppUiStore(state => state.home.newMessages);
|
||||
const hasHydrated = useAppUiStore(state => state._hasHydrated);
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import Link from "next/link";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Badge, Button, Icon, IconName, Typography } from "@/components/ui";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
|
||||
import { useDrawer } from "..";
|
||||
@ -16,13 +14,11 @@ import styles from "./Header.module.scss";
|
||||
|
||||
interface HeaderProps {
|
||||
className?: string;
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function Header({ className, chatsPromise }: HeaderProps) {
|
||||
export default function Header({ className }: HeaderProps) {
|
||||
const { open } = useDrawer();
|
||||
const chats = use(chatsPromise);
|
||||
const { totalUnreadCount } = useChatsSocket({ initialChats: chats });
|
||||
const { totalUnreadCount } = useChats();
|
||||
|
||||
return (
|
||||
<header className={clsx(styles.header, className)}>
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useLocale } from "next-intl";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Badge, Icon, Typography } from "@/components/ui";
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { useChatsSocket } from "@/hooks/chats/useChatsSocket";
|
||||
import { useChats } from "@/providers/chats-provider";
|
||||
import { ROUTES } from "@/shared/constants/client-routes";
|
||||
import { NavItem, navItems } from "@/shared/constants/navigation";
|
||||
import { stripLocale } from "@/shared/utils/path";
|
||||
@ -20,16 +18,11 @@ const getBadge = (item: NavItem, totalUnreadCount: number) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
interface NavigationBarProps {
|
||||
chatsPromise: Promise<IGetChatsListResponse>;
|
||||
}
|
||||
|
||||
export default function NavigationBar({ chatsPromise }: NavigationBarProps) {
|
||||
export default function NavigationBar() {
|
||||
const pathname = usePathname();
|
||||
const locale = useLocale();
|
||||
const pathnameWithoutLocale = stripLocale(pathname, locale);
|
||||
const chats = use(chatsPromise);
|
||||
const { totalUnreadCount } = useChatsSocket({ initialChats: chats });
|
||||
const { totalUnreadCount } = useChats();
|
||||
|
||||
return (
|
||||
<nav className={styles.container}>
|
||||
|
||||
37
src/hooks/audio/useAudio.ts
Normal file
37
src/hooks/audio/useAudio.ts
Normal file
@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
|
||||
import { audioService } from "@/services/audio";
|
||||
import { AudioUrls } from "@/shared/constants/audio";
|
||||
|
||||
interface UseAudioOptions {
|
||||
enabled?: boolean;
|
||||
preload?: AudioUrls[];
|
||||
}
|
||||
|
||||
export const useAudio = (options: UseAudioOptions = {}) => {
|
||||
const { enabled = true, preload = [] } = options;
|
||||
|
||||
useEffect(() => {
|
||||
audioService.setEnabled(enabled);
|
||||
}, [enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (preload.length > 0) {
|
||||
audioService.preload(preload);
|
||||
}
|
||||
}, [preload]);
|
||||
|
||||
const playNewMessageNotification = useCallback(() => {
|
||||
audioService.play(AudioUrls.NewMessage);
|
||||
}, []);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
playNewMessageNotification,
|
||||
isEnabled: audioService.isAudioEnabled(),
|
||||
}),
|
||||
[playNewMessageNotification]
|
||||
);
|
||||
};
|
||||
@ -3,14 +3,19 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { IGetChatsListResponse } from "@/entities/chats/types";
|
||||
import { AudioUrls } from "@/shared/constants/audio";
|
||||
|
||||
import { useAudio } from "../audio/useAudio";
|
||||
import { useSocketEvent } from "../socket/useSocketEvent";
|
||||
|
||||
interface UseChatsSocketOptions {
|
||||
export interface UseChatsSocketOptions {
|
||||
initialChats?: IGetChatsListResponse;
|
||||
enableNotificationSound?: boolean;
|
||||
}
|
||||
|
||||
export const useChatsSocket = (options: UseChatsSocketOptions = {}) => {
|
||||
const { enableNotificationSound = true } = options;
|
||||
|
||||
const initialChats = options.initialChats ?? {
|
||||
categorizedChats: {},
|
||||
startedChats: [],
|
||||
@ -18,14 +23,28 @@ export const useChatsSocket = (options: UseChatsSocketOptions = {}) => {
|
||||
totalUnreadCount: 0,
|
||||
};
|
||||
|
||||
const { playNewMessageNotification } = useAudio({
|
||||
enabled: enableNotificationSound,
|
||||
preload: [AudioUrls.NewMessage],
|
||||
});
|
||||
|
||||
const [isInChat, setIsInChat] = useState(false);
|
||||
const [chats, setChats] = useState<IGetChatsListResponse>(initialChats);
|
||||
const [unreadCount, setUnreadCount] = useState<number>(
|
||||
initialChats.totalUnreadCount
|
||||
);
|
||||
|
||||
useSocketEvent("chat_joined", () => setIsInChat(true));
|
||||
useSocketEvent("chat_left", () => setIsInChat(false));
|
||||
|
||||
useSocketEvent("chats_updated", chats => setChats(chats));
|
||||
useSocketEvent("unread_messages_count", count =>
|
||||
setUnreadCount(count.unreadCount)
|
||||
setUnreadCount(prev => {
|
||||
if (!isInChat && prev < count.unreadCount) {
|
||||
playNewMessageNotification();
|
||||
}
|
||||
return count.unreadCount;
|
||||
})
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
@ -35,7 +54,8 @@ export const useChatsSocket = (options: UseChatsSocketOptions = {}) => {
|
||||
startedChats: chats.startedChats,
|
||||
categorizedChats: chats.categorizedChats,
|
||||
totalUnreadCount: unreadCount,
|
||||
isInChat,
|
||||
}),
|
||||
[chats, unreadCount]
|
||||
[chats, unreadCount, isInChat]
|
||||
);
|
||||
};
|
||||
|
||||
34
src/providers/chats-provider.tsx
Normal file
34
src/providers/chats-provider.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, ReactNode, useContext } from "react";
|
||||
|
||||
import {
|
||||
useChatsSocket,
|
||||
UseChatsSocketOptions,
|
||||
} from "@/hooks/chats/useChatsSocket";
|
||||
|
||||
type ChatsContextValue = ReturnType<typeof useChatsSocket>;
|
||||
|
||||
const ChatsContext = createContext<ChatsContextValue | null>(null);
|
||||
|
||||
export function useChats() {
|
||||
const ctx = useContext(ChatsContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useChats must be used within <ChatsProvider>");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
interface ChatsProviderProps extends UseChatsSocketOptions {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ChatsProvider({ children, initialChats }: ChatsProviderProps) {
|
||||
const value = useChatsSocket({
|
||||
initialChats,
|
||||
});
|
||||
|
||||
return (
|
||||
<ChatsContext.Provider value={value}>{children}</ChatsContext.Provider>
|
||||
);
|
||||
}
|
||||
58
src/services/audio/index.ts
Normal file
58
src/services/audio/index.ts
Normal file
@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { AudioUrls, getAudioUrl } from "@/shared/constants/audio";
|
||||
|
||||
class AudioService {
|
||||
private audioElements = new Map<string, HTMLAudioElement>();
|
||||
private isEnabled = true;
|
||||
|
||||
private getAudioElement(url: string): HTMLAudioElement | undefined {
|
||||
if (!this.audioElements.has(url)) {
|
||||
const audio = new Audio(url);
|
||||
audio.preload = "auto";
|
||||
this.audioElements.set(url, audio);
|
||||
}
|
||||
return this.audioElements.get(url);
|
||||
}
|
||||
|
||||
play(key: AudioUrls): Promise<void> {
|
||||
if (!this.isEnabled) return Promise.resolve();
|
||||
|
||||
try {
|
||||
const audio = this.getAudioElement(getAudioUrl(key));
|
||||
if (audio) {
|
||||
audio.currentTime = 0;
|
||||
return audio.play();
|
||||
}
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Failed to play audio:", error);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
this.isEnabled = enabled;
|
||||
}
|
||||
|
||||
isAudioEnabled(): boolean {
|
||||
return this.isEnabled;
|
||||
}
|
||||
|
||||
preload(keys: AudioUrls[]): void {
|
||||
keys.forEach(key => {
|
||||
this.getAudioElement(getAudioUrl(key));
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.audioElements.forEach(audio => {
|
||||
audio.pause();
|
||||
audio.src = "";
|
||||
});
|
||||
this.audioElements.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const audioService = new AudioService();
|
||||
9
src/shared/constants/audio/index.ts
Normal file
9
src/shared/constants/audio/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
const audioUrls: Record<AudioUrls, string> = {
|
||||
"new-message": "/audio/notification-new-message-1.mp3",
|
||||
};
|
||||
|
||||
export enum AudioUrls {
|
||||
NewMessage = "new-message",
|
||||
}
|
||||
|
||||
export const getAudioUrl = (key: AudioUrls) => audioUrls[key];
|
||||
Loading…
Reference in New Issue
Block a user