mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-12 18:29:57 -05:00
Settings: Link notification sounds to playSound.
Fix: Restore hooks.ts patch, additionally use numbers.
This commit is contained in:
parent
352c0e880c
commit
8f62625506
10 changed files with 104 additions and 62 deletions
2
external/lang
vendored
2
external/lang
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit be021b37763b2b0f8f0367b49f9912add845aa21
|
Subproject commit 5af0f9c8092382aa9608ec39bf5149194da9161c
|
|
@ -1,14 +1,17 @@
|
||||||
import message from './message.mp3';
|
import message from './message.mp3';
|
||||||
|
import outbound from './outbound.mp3';
|
||||||
import call_join from './call_join.mp3';
|
import call_join from './call_join.mp3';
|
||||||
import call_leave from './call_leave.mp3';
|
import call_leave from './call_leave.mp3';
|
||||||
|
|
||||||
const SoundMap: { [key in Sounds]: string } = {
|
const SoundMap: { [key in Sounds]: string } = {
|
||||||
message,
|
message,
|
||||||
|
outbound,
|
||||||
call_join,
|
call_join,
|
||||||
call_leave
|
call_leave
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Sounds = 'message' | 'call_join' | 'call_leave';
|
export type Sounds = 'message' | 'outbound' | 'call_join' | 'call_leave';
|
||||||
|
export const SOUNDS_ARRAY: Sounds[] = [ 'message', 'outbound', 'call_join', 'call_leave' ];
|
||||||
|
|
||||||
export function playSound(sound: Sounds) {
|
export function playSound(sound: Sounds) {
|
||||||
let file = SoundMap[sound];
|
let file = SoundMap[sound];
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { SingletonMessageRenderer, SMOOTH_SCROLL_ON_RECEIVE } from "../../../lib
|
||||||
import ReplyBar from "./bars/ReplyBar";
|
import ReplyBar from "./bars/ReplyBar";
|
||||||
import FilePreview from './bars/FilePreview';
|
import FilePreview from './bars/FilePreview';
|
||||||
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
import AutoComplete, { useAutoComplete } from "../AutoComplete";
|
||||||
|
import { SoundContext } from "../../../context/Settings";
|
||||||
|
|
||||||
type Props = WithDispatcher & {
|
type Props = WithDispatcher & {
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
|
@ -59,6 +60,7 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
|
||||||
const [ uploadState, setUploadState ] = useState<UploadState>({ type: 'none' });
|
const [ uploadState, setUploadState ] = useState<UploadState>({ type: 'none' });
|
||||||
const [ typing, setTyping ] = useState<boolean | number>(false);
|
const [ typing, setTyping ] = useState<boolean | number>(false);
|
||||||
const [ replies, setReplies ] = useState<Reply[]>([]);
|
const [ replies, setReplies ] = useState<Reply[]>([]);
|
||||||
|
const playSound = useContext(SoundContext);
|
||||||
const { openScreen } = useIntermediate();
|
const { openScreen } = useIntermediate();
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
const translate = useTranslation();
|
const translate = useTranslation();
|
||||||
|
@ -108,6 +110,7 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
|
||||||
stopTyping();
|
stopTyping();
|
||||||
setMessage();
|
setMessage();
|
||||||
setReplies([]);
|
setReplies([]);
|
||||||
|
playSound('outbound');
|
||||||
|
|
||||||
const nonce = ulid();
|
const nonce = ulid();
|
||||||
dispatcher({
|
dispatcher({
|
||||||
|
@ -208,6 +211,7 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
|
||||||
|
|
||||||
setMessage();
|
setMessage();
|
||||||
setReplies([]);
|
setReplies([]);
|
||||||
|
playSound('outbound');
|
||||||
|
|
||||||
if (files.length > CAN_UPLOAD_AT_ONCE) {
|
if (files.length > CAN_UPLOAD_AT_ONCE) {
|
||||||
setUploadState({
|
setUploadState({
|
||||||
|
|
|
@ -4,28 +4,47 @@
|
||||||
//
|
//
|
||||||
// Replace references to SettingsContext with connectState in the future
|
// Replace references to SettingsContext with connectState in the future
|
||||||
// if it does cause problems though.
|
// if it does cause problems though.
|
||||||
|
//
|
||||||
|
// This now also supports Audio stuff.
|
||||||
|
|
||||||
import { Settings } from "../redux/reducers/settings";
|
import { DEFAULT_SOUNDS, Settings, SoundOptions } from "../redux/reducers/settings";
|
||||||
|
import { playSound, Sounds } from "../assets/sounds/Audio";
|
||||||
import { connectState } from "../redux/connector";
|
import { connectState } from "../redux/connector";
|
||||||
|
import defaultsDeep from "lodash.defaultsdeep";
|
||||||
import { Children } from "../types/Preact";
|
import { Children } from "../types/Preact";
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
|
import { useMemo } from "preact/hooks";
|
||||||
|
|
||||||
export const SettingsContext = createContext<Settings>({} as any);
|
export const SettingsContext = createContext<Settings>({} as any);
|
||||||
|
export const SoundContext = createContext<(sound: Sounds) => void>({} as any);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: Children,
|
children?: Children,
|
||||||
settings: Settings
|
settings: Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
function Settings(props: Props) {
|
function Settings({ settings, children }: Props) {
|
||||||
|
console.info(settings.notification);
|
||||||
|
const play = useMemo(() => {
|
||||||
|
const enabled: SoundOptions = defaultsDeep(settings.notification ?? {}, DEFAULT_SOUNDS);
|
||||||
|
return (sound: Sounds) => {
|
||||||
|
console.info('check if we can play sound', enabled[sound]);
|
||||||
|
if (enabled[sound]) {
|
||||||
|
playSound(sound);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [ settings.notification ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContext.Provider value={props.settings}>
|
<SettingsContext.Provider value={settings}>
|
||||||
{ props.children }
|
<SoundContext.Provider value={play}>
|
||||||
|
{ children }
|
||||||
|
</SoundContext.Provider>
|
||||||
</SettingsContext.Provider>
|
</SettingsContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connectState(Settings, state => {
|
export default connectState<Omit<Props, 'settings'>>(Settings, state => {
|
||||||
return {
|
return {
|
||||||
settings: state.settings
|
settings: state.settings
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { AppContext } from "./revoltjs/RevoltClient";
|
||||||
import type VoiceClient from "../lib/vortex/VoiceClient";
|
import type VoiceClient from "../lib/vortex/VoiceClient";
|
||||||
import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
|
import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
|
||||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
|
import { SoundContext } from "./Settings";
|
||||||
|
|
||||||
export enum VoiceStatus {
|
export enum VoiceStatus {
|
||||||
LOADING = 0,
|
LOADING = 0,
|
||||||
|
@ -106,6 +107,7 @@ export default function Voice({ children }: Props) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setStatus(VoiceStatus.READY);
|
setStatus(VoiceStatus.READY);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus(VoiceStatus.CONNECTED);
|
setStatus(VoiceStatus.CONNECTED);
|
||||||
|
@ -154,6 +156,8 @@ export default function Voice({ children }: Props) {
|
||||||
}, [ client ]);
|
}, [ client ]);
|
||||||
|
|
||||||
const { forceUpdate } = useForceUpdate();
|
const { forceUpdate } = useForceUpdate();
|
||||||
|
const playSound = useContext(SoundContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!client?.supported()) return;
|
if (!client?.supported()) return;
|
||||||
|
|
||||||
|
@ -164,8 +168,14 @@ export default function Voice({ children }: Props) {
|
||||||
client.on("startProduce", forceUpdate);
|
client.on("startProduce", forceUpdate);
|
||||||
client.on("stopProduce", forceUpdate);
|
client.on("stopProduce", forceUpdate);
|
||||||
|
|
||||||
client.on("userJoined", forceUpdate);
|
client.on("userJoined", () => {
|
||||||
client.on("userLeft", forceUpdate);
|
playSound('call_join');
|
||||||
|
forceUpdate();
|
||||||
|
});
|
||||||
|
client.on("userLeft", () => {
|
||||||
|
playSound('call_leave');
|
||||||
|
forceUpdate();
|
||||||
|
});
|
||||||
client.on("userStartProduce", forceUpdate);
|
client.on("userStartProduce", forceUpdate);
|
||||||
client.on("userStopProduce", forceUpdate);
|
client.on("userStopProduce", forceUpdate);
|
||||||
client.on("close", forceUpdate);
|
client.on("close", forceUpdate);
|
||||||
|
|
|
@ -4,23 +4,26 @@ import { BrowserRouter as Router } from "react-router-dom";
|
||||||
|
|
||||||
import Intermediate from './intermediate/Intermediate';
|
import Intermediate from './intermediate/Intermediate';
|
||||||
import Client from './revoltjs/RevoltClient';
|
import Client from './revoltjs/RevoltClient';
|
||||||
import Voice from "./Voice";
|
import Settings from "./Settings";
|
||||||
import Locale from "./Locale";
|
import Locale from "./Locale";
|
||||||
|
import Voice from "./Voice";
|
||||||
import Theme from "./Theme";
|
import Theme from "./Theme";
|
||||||
|
|
||||||
export default function Context({ children }: { children: Children }) {
|
export default function Context({ children }: { children: Children }) {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<State>
|
<State>
|
||||||
<Locale>
|
<Settings>
|
||||||
<Intermediate>
|
<Locale>
|
||||||
<Client>
|
<Intermediate>
|
||||||
<Voice>
|
<Client>
|
||||||
<Theme>{children}</Theme>
|
<Voice>
|
||||||
</Voice>
|
<Theme>{children}</Theme>
|
||||||
</Client>
|
</Voice>
|
||||||
</Intermediate>
|
</Client>
|
||||||
</Locale>
|
</Intermediate>
|
||||||
|
</Locale>
|
||||||
|
</Settings>
|
||||||
</State>
|
</State>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { decodeTime } from "ulid";
|
import { decodeTime } from "ulid";
|
||||||
|
import { SoundContext } from "../Settings";
|
||||||
import { AppContext } from "./RevoltClient";
|
import { AppContext } from "./RevoltClient";
|
||||||
import { useTranslation } from "../../lib/i18n";
|
import { useTranslation } from "../../lib/i18n";
|
||||||
import { Users } from "revolt.js/dist/api/objects";
|
import { Users } from "revolt.js/dist/api/objects";
|
||||||
import { useContext, useEffect } from "preact/hooks";
|
import { useContext, useEffect } from "preact/hooks";
|
||||||
import { connectState } from "../../redux/connector";
|
import { connectState } from "../../redux/connector";
|
||||||
import { playSound } from "../../assets/sounds/Audio";
|
|
||||||
import { Message, SYSTEM_USER_ID, User } from "revolt.js";
|
import { Message, SYSTEM_USER_ID, User } from "revolt.js";
|
||||||
import { NotificationOptions } from "../../redux/reducers/settings";
|
import { NotificationOptions } from "../../redux/reducers/settings";
|
||||||
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
||||||
|
@ -27,8 +27,6 @@ async function createNotification(title: string, options: globalThis.Notificatio
|
||||||
function Notifier(props: Props) {
|
function Notifier(props: Props) {
|
||||||
const translate = useTranslation();
|
const translate = useTranslation();
|
||||||
const showNotification = props.options?.desktopEnabled ?? false;
|
const showNotification = props.options?.desktopEnabled ?? false;
|
||||||
// const playIncoming = props.options?.soundEnabled ?? true;
|
|
||||||
// const playOutgoing = props.options?.outgoingSoundEnabled ?? true;
|
|
||||||
|
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
const { guild: guild_id, channel: channel_id } = useParams<{
|
const { guild: guild_id, channel: channel_id } = useParams<{
|
||||||
|
@ -36,13 +34,13 @@ function Notifier(props: Props) {
|
||||||
channel: string;
|
channel: string;
|
||||||
}>();
|
}>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const playSound = useContext(SoundContext);
|
||||||
|
|
||||||
async function message(msg: Message) {
|
async function message(msg: Message) {
|
||||||
if (msg.author === client.user!._id) return;
|
if (msg.author === client.user!._id) return;
|
||||||
if (msg.channel === channel_id && document.hasFocus()) return;
|
if (msg.channel === channel_id && document.hasFocus()) return;
|
||||||
if (client.user?.status?.presence === Users.Presence.Busy) return;
|
if (client.user?.status?.presence === Users.Presence.Busy) return;
|
||||||
|
|
||||||
// Sounds.playInbound();
|
|
||||||
playSound('message');
|
playSound('message');
|
||||||
if (!showNotification) return;
|
if (!showNotification) return;
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,9 @@ export interface HookContext {
|
||||||
export function useForceUpdate(context?: HookContext): HookContext {
|
export function useForceUpdate(context?: HookContext): HookContext {
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
if (context) return context;
|
if (context) return context;
|
||||||
/*const H = useState(undefined);
|
|
||||||
var updateState: (_: undefined) => void;
|
const H = useState(0);
|
||||||
|
var updateState: (_: number) => void;
|
||||||
if (Array.isArray(H)) {
|
if (Array.isArray(H)) {
|
||||||
let [, u] = H;
|
let [, u] = H;
|
||||||
updateState = u;
|
updateState = u;
|
||||||
|
@ -20,9 +21,8 @@ export function useForceUpdate(context?: HookContext): HookContext {
|
||||||
console.warn('Failed to construct using useState.');
|
console.warn('Failed to construct using useState.');
|
||||||
console.warn(H);
|
console.warn(H);
|
||||||
updateState = ()=>{};
|
updateState = ()=>{};
|
||||||
}*/
|
}
|
||||||
|
|
||||||
const [, updateState] = useState(0);
|
|
||||||
return { client, forceUpdate: () => updateState(Math.random()) };
|
return { client, forceUpdate: () => updateState(Math.random()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ export function useDMs(context?: HookContext) {
|
||||||
|
|
||||||
return map
|
return map
|
||||||
.toArray()
|
.toArray()
|
||||||
.filter(x => x.channel_type === 'DirectMessage' || x.channel_type === 'Group' || x.channel_type === 'SavedMessages') as (Channels.GroupChannel | Channels.DirectMessageChannel | Channels.SavedMessagesChannel)[];
|
.filter(x => (x.channel_type === 'DirectMessage' && x.active) || x.channel_type === 'Group' || x.channel_type === 'SavedMessages') as (Channels.GroupChannel | Channels.DirectMessageChannel | Channels.SavedMessagesChannel)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUserPermission(id: string, context?: HookContext) {
|
export function useUserPermission(id: string, context?: HookContext) {
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import styles from "./Panes.module.scss";
|
import styles from "./Panes.module.scss";
|
||||||
|
import defaultsDeep from "lodash.defaultsdeep";
|
||||||
import Checkbox from "../../../components/ui/Checkbox";
|
import Checkbox from "../../../components/ui/Checkbox";
|
||||||
import { connectState } from "../../../redux/connector";
|
import { connectState } from "../../../redux/connector";
|
||||||
import { WithDispatcher } from "../../../redux/reducers";
|
import { WithDispatcher } from "../../../redux/reducers";
|
||||||
|
import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio";
|
||||||
import { useContext, useEffect, useState } from "preact/hooks";
|
import { useContext, useEffect, useState } from "preact/hooks";
|
||||||
import { urlBase64ToUint8Array } from "../../../lib/conversion";
|
import { urlBase64ToUint8Array } from "../../../lib/conversion";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||||
import { NotificationOptions } from "../../../redux/reducers/settings";
|
|
||||||
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
import { useIntermediate } from "../../../context/intermediate/Intermediate";
|
||||||
|
import { DEFAULT_SOUNDS, NotificationOptions, SoundOptions } from "../../../redux/reducers/settings";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
options?: NotificationOptions;
|
options?: NotificationOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Component(props: Props & WithDispatcher) {
|
export function Component({ options, dispatcher }: Props & WithDispatcher) {
|
||||||
const client = useContext(AppContext);
|
const client = useContext(AppContext);
|
||||||
const { openScreen } = useIntermediate();
|
const { openScreen } = useIntermediate();
|
||||||
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
|
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
|
||||||
|
@ -28,6 +30,7 @@ export function Component(props: Props & WithDispatcher) {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const enabledSounds: SoundOptions = defaultsDeep(options?.sounds ?? {}, DEFAULT_SOUNDS);
|
||||||
return (
|
return (
|
||||||
<div className={styles.notifications}>
|
<div className={styles.notifications}>
|
||||||
<h3>
|
<h3>
|
||||||
|
@ -35,7 +38,7 @@ export function Component(props: Props & WithDispatcher) {
|
||||||
</h3>
|
</h3>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={!("Notification" in window)}
|
disabled={!("Notification" in window)}
|
||||||
checked={props.options?.desktopEnabled ?? false}
|
checked={options?.desktopEnabled ?? false}
|
||||||
onChange={async desktopEnabled => {
|
onChange={async desktopEnabled => {
|
||||||
if (desktopEnabled) {
|
if (desktopEnabled) {
|
||||||
let permission = await Notification.requestPermission();
|
let permission = await Notification.requestPermission();
|
||||||
|
@ -47,7 +50,7 @@ export function Component(props: Props & WithDispatcher) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
props.dispatcher({
|
dispatcher({
|
||||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||||
options: { desktopEnabled }
|
options: { desktopEnabled }
|
||||||
});
|
});
|
||||||
|
@ -103,34 +106,25 @@ export function Component(props: Props & WithDispatcher) {
|
||||||
<h3>
|
<h3>
|
||||||
<Text id="app.settings.pages.notifications.sounds" />
|
<Text id="app.settings.pages.notifications.sounds" />
|
||||||
</h3>
|
</h3>
|
||||||
<Checkbox
|
{
|
||||||
checked={props.options?.soundEnabled ?? true}
|
SOUNDS_ARRAY.map(key =>
|
||||||
onChange={soundEnabled =>
|
<Checkbox
|
||||||
props.dispatcher({
|
checked={enabledSounds[key] ? true : false}
|
||||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
onChange={enabled =>
|
||||||
options: { soundEnabled }
|
dispatcher({
|
||||||
})
|
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
||||||
}
|
options: {
|
||||||
>
|
sounds: {
|
||||||
<Text id="app.settings.pages.notifications.enable_sound" />
|
...options?.sounds,
|
||||||
<p>
|
[key]: enabled
|
||||||
<Text id="app.settings.pages.notifications.descriptions.enable_sound" />
|
}
|
||||||
</p>
|
}
|
||||||
</Checkbox>
|
})
|
||||||
<Checkbox
|
}>
|
||||||
checked={props.options?.outgoingSoundEnabled ?? true}
|
<Text id={`app.settings.pages.notifications.sound.${key}`} />
|
||||||
onChange={outgoingSoundEnabled =>
|
</Checkbox>
|
||||||
props.dispatcher({
|
)
|
||||||
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
|
}
|
||||||
options: { outgoingSoundEnabled }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text id="app.settings.pages.notifications.enable_outgoing_sound" />
|
|
||||||
<p>
|
|
||||||
<Text id="app.settings.pages.notifications.descriptions.enable_outgoing_sound" />
|
|
||||||
</p>
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
import { filter } from ".";
|
import { filter } from ".";
|
||||||
import { SyncUpdateAction } from "./sync";
|
import { SyncUpdateAction } from "./sync";
|
||||||
|
import { Sounds } from "../../assets/sounds/Audio";
|
||||||
import { Theme, ThemeOptions } from "../../context/Theme";
|
import { Theme, ThemeOptions } from "../../context/Theme";
|
||||||
|
|
||||||
|
export type SoundOptions = {
|
||||||
|
[key in Sounds]?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SOUNDS: SoundOptions = {
|
||||||
|
message: true,
|
||||||
|
outbound: false,
|
||||||
|
call_join: true,
|
||||||
|
call_leave: true
|
||||||
|
};
|
||||||
|
|
||||||
export interface NotificationOptions {
|
export interface NotificationOptions {
|
||||||
desktopEnabled?: boolean;
|
desktopEnabled?: boolean;
|
||||||
soundEnabled?: boolean;
|
sounds?: SoundOptions
|
||||||
outgoingSoundEnabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EmojiPacks = "mutant" | "twemoji" | "noto" | "openmoji";
|
export type EmojiPacks = "mutant" | "twemoji" | "noto" | "openmoji";
|
||||||
|
|
Loading…
Reference in a new issue