mirror of
https://github.com/revoltchat/revite.git
synced 2024-11-24 16:10:59 -05:00
Port sync, queue management and notifs.
This commit is contained in:
parent
3555e9a7bf
commit
0115ace3fa
20 changed files with 521 additions and 35 deletions
|
@ -34,6 +34,7 @@
|
||||||
"@styled-icons/simple-icons": "^10.33.0",
|
"@styled-icons/simple-icons": "^10.33.0",
|
||||||
"@traptitech/markdown-it-katex": "^3.4.3",
|
"@traptitech/markdown-it-katex": "^3.4.3",
|
||||||
"@traptitech/markdown-it-spoiler": "^1.1.6",
|
"@traptitech/markdown-it-spoiler": "^1.1.6",
|
||||||
|
"@types/lodash.isequal": "^4.5.5",
|
||||||
"@types/markdown-it": "^12.0.2",
|
"@types/markdown-it": "^12.0.2",
|
||||||
"@types/node": "^15.12.4",
|
"@types/node": "^15.12.4",
|
||||||
"@types/preact-i18n": "^2.3.0",
|
"@types/preact-i18n": "^2.3.0",
|
||||||
|
@ -53,6 +54,7 @@
|
||||||
"highlight.js": "^11.0.1",
|
"highlight.js": "^11.0.1",
|
||||||
"idb": "^6.1.2",
|
"idb": "^6.1.2",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
"markdown-it": "^12.0.6",
|
"markdown-it": "^12.0.6",
|
||||||
"markdown-it-emoji": "^2.0.0",
|
"markdown-it-emoji": "^2.0.0",
|
||||||
"markdown-it-sub": "^1.0.0",
|
"markdown-it-sub": "^1.0.0",
|
||||||
|
|
21
src/assets/sounds/Audio.ts
Normal file
21
src/assets/sounds/Audio.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import message from './message.mp3';
|
||||||
|
import call_join from './call_join.mp3';
|
||||||
|
import call_leave from './call_leave.mp3';
|
||||||
|
|
||||||
|
const SoundMap: { [key in Sounds]: string } = {
|
||||||
|
message,
|
||||||
|
call_join,
|
||||||
|
call_leave
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Sounds = 'message' | 'call_join' | 'call_leave';
|
||||||
|
|
||||||
|
export function playSound(sound: Sounds) {
|
||||||
|
let file = SoundMap[sound];
|
||||||
|
let el = new Audio(file);
|
||||||
|
try {
|
||||||
|
el.play();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to play audio file', file, err);
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,7 +33,7 @@ export default function Message({ attachContext, message, contrast, content: rep
|
||||||
</MessageInfo>
|
</MessageInfo>
|
||||||
<MessageContent>
|
<MessageContent>
|
||||||
{ head && <Username user={user} /> }
|
{ head && <Username user={user} /> }
|
||||||
{ content ?? <Markdown content={content} /> }
|
{ replacement ?? <Markdown content={content} /> }
|
||||||
{ message.attachments?.map((attachment, index) =>
|
{ message.attachments?.map((attachment, index) =>
|
||||||
<Attachment key={index} attachment={attachment} hasContent={ index > 0 || content.length > 0 } />) }
|
<Attachment key={index} attachment={attachment} hasContent={ index > 0 || content.length > 0 } />) }
|
||||||
{ message.embeds?.map((embed, index) =>
|
{ message.embeds?.map((embed, index) =>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { useContext } from "preact/hooks";
|
|
||||||
import { Channel } from "revolt.js";
|
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
import { Channel } from "revolt.js";
|
||||||
import { takeError } from "../../../context/revoltjs/util";
|
import TextArea from "../../ui/TextArea";
|
||||||
|
import { useContext } from "preact/hooks";
|
||||||
import { defer } from "../../../lib/defer";
|
import { defer } from "../../../lib/defer";
|
||||||
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
import IconButton from "../../ui/IconButton";
|
||||||
import { SingletonMessageRenderer, SMOOTH_SCROLL_ON_RECEIVE } from "../../../lib/renderer/Singleton";
|
import { Send } from '@styled-icons/feather';
|
||||||
import { connectState } from "../../../redux/connector";
|
import { connectState } from "../../../redux/connector";
|
||||||
import { WithDispatcher } from "../../../redux/reducers";
|
import { WithDispatcher } from "../../../redux/reducers";
|
||||||
import IconButton from "../../ui/IconButton";
|
import { takeError } from "../../../context/revoltjs/util";
|
||||||
import TextArea from "../../ui/TextArea";
|
import { AppContext } from "../../../context/revoltjs/RevoltClient";
|
||||||
import { Send } from '@styled-icons/feather';
|
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
|
||||||
|
import { SingletonMessageRenderer, SMOOTH_SCROLL_ON_RECEIVE } from "../../../lib/renderer/Singleton";
|
||||||
|
|
||||||
type Props = WithDispatcher & {
|
type Props = WithDispatcher & {
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
|
|
256
src/context/revoltjs/Notifications.tsx
Normal file
256
src/context/revoltjs/Notifications.tsx
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
import { decodeTime } from "ulid";
|
||||||
|
import { AppContext } from "./RevoltClient";
|
||||||
|
import { Users } from "revolt.js/dist/api/objects";
|
||||||
|
import { useContext, useEffect } from "preact/hooks";
|
||||||
|
import { IntlContext, translate } from "preact-i18n";
|
||||||
|
import { connectState } from "../../redux/connector";
|
||||||
|
import { playSound } from "../../assets/sounds/Audio";
|
||||||
|
import { Message, SYSTEM_USER_ID, User } from "revolt.js";
|
||||||
|
import { NotificationOptions } from "../../redux/reducers/settings";
|
||||||
|
import { Route, Switch, useHistory, useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options?: NotificationOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifications: { [key: string]: Notification } = {};
|
||||||
|
|
||||||
|
async function createNotification(title: string, options: globalThis.NotificationOptions) {
|
||||||
|
try {
|
||||||
|
return new Notification(title, options);
|
||||||
|
} catch (err) {
|
||||||
|
let sw = await navigator.serviceWorker.getRegistration();
|
||||||
|
sw?.showNotification(title, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Notifier(props: Props) {
|
||||||
|
const { intl } = useContext(IntlContext) as any;
|
||||||
|
const showNotification = props.options?.desktopEnabled ?? false;
|
||||||
|
// const playIncoming = props.options?.soundEnabled ?? true;
|
||||||
|
// const playOutgoing = props.options?.outgoingSoundEnabled ?? true;
|
||||||
|
|
||||||
|
const client = useContext(AppContext);
|
||||||
|
const { guild: guild_id, channel: channel_id } = useParams<{
|
||||||
|
guild: string;
|
||||||
|
channel: string;
|
||||||
|
}>();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
async function message(msg: Message) {
|
||||||
|
if (msg.author === client.user!._id) return;
|
||||||
|
if (msg.channel === channel_id && document.hasFocus()) return;
|
||||||
|
if (client.user?.status?.presence === Users.Presence.Busy) return;
|
||||||
|
|
||||||
|
// Sounds.playInbound();
|
||||||
|
playSound('message');
|
||||||
|
if (!showNotification) return;
|
||||||
|
|
||||||
|
const channel = client.channels.get(msg.channel);
|
||||||
|
const author = client.users.get(msg.author);
|
||||||
|
if (author?.relationship === Users.Relationship.Blocked) return;
|
||||||
|
|
||||||
|
let title;
|
||||||
|
switch (channel?.channel_type) {
|
||||||
|
case "SavedMessages":
|
||||||
|
return;
|
||||||
|
case "DirectMessage":
|
||||||
|
title = `@${author?.username}`;
|
||||||
|
break;
|
||||||
|
case "Group":
|
||||||
|
if (author?._id === SYSTEM_USER_ID) {
|
||||||
|
title = channel.name;
|
||||||
|
} else {
|
||||||
|
title = `@${author?.username} - ${channel.name}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "TextChannel":
|
||||||
|
const server = client.servers.get(channel.server);
|
||||||
|
title = `@${author?.username} (#${channel.name}, ${server?.name})`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = msg.channel;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image;
|
||||||
|
if (msg.attachments) {
|
||||||
|
let imageAttachment = msg.attachments.find(x => x.metadata.type === 'Image');
|
||||||
|
if (imageAttachment) {
|
||||||
|
image = client.generateFileURL(imageAttachment, { max_side: 720 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body, icon;
|
||||||
|
if (typeof msg.content === "string") {
|
||||||
|
body = client.markdownToText(msg.content);
|
||||||
|
icon = client.users.getAvatarURL(msg.author, { max_side: 256 });
|
||||||
|
} else {
|
||||||
|
let users = client.users;
|
||||||
|
switch (msg.content.type) {
|
||||||
|
// ! FIXME: update to support new replacements
|
||||||
|
case "user_added":
|
||||||
|
body = `${users.get(msg.content.id)?.username} ${translate(
|
||||||
|
"app.main.channel.system.user_joined",
|
||||||
|
"",
|
||||||
|
intl.dictionary
|
||||||
|
)} (${translate(
|
||||||
|
"app.main.channel.system.added_by",
|
||||||
|
"",
|
||||||
|
intl.dictionary
|
||||||
|
)} ${users.get(msg.content.by)?.username})`;
|
||||||
|
icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
|
||||||
|
break;
|
||||||
|
case "user_remove":
|
||||||
|
body = `${users.get(msg.content.id)?.username} ${translate(
|
||||||
|
"app.main.channel.system.user_left",
|
||||||
|
"",
|
||||||
|
intl.dictionary
|
||||||
|
)} (${translate(
|
||||||
|
"app.main.channel.system.added_by",
|
||||||
|
"",
|
||||||
|
intl.dictionary
|
||||||
|
)} ${users.get(msg.content.by)?.username})`;
|
||||||
|
icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
|
||||||
|
break;
|
||||||
|
case "user_left":
|
||||||
|
body = `${users.get(msg.content.id)?.username} ${translate(
|
||||||
|
"app.main.channel.system.user_left",
|
||||||
|
"",
|
||||||
|
intl.dictionary
|
||||||
|
)}`;
|
||||||
|
icon = client.users.getAvatarURL(msg.content.id, { max_side: 256 });
|
||||||
|
break;
|
||||||
|
case "channel_renamed":
|
||||||
|
body = `${users.get(msg.content.by)?.username} ${translate(
|
||||||
|
"app.main.channel.system.channel_renamed",
|
||||||
|
"",
|
||||||
|
intl.dictionary
|
||||||
|
)} ${msg.content.name}`;
|
||||||
|
icon = client.users.getAvatarURL(msg.content.by, { max_side: 256 });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let notif = await createNotification(title, {
|
||||||
|
icon,
|
||||||
|
image,
|
||||||
|
body,
|
||||||
|
timestamp: decodeTime(msg._id),
|
||||||
|
tag: msg.channel,
|
||||||
|
badge: '/assets/icons/android-chrome-512x512.png',
|
||||||
|
silent: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (notif) {
|
||||||
|
notif.addEventListener("click", () => {
|
||||||
|
const id = msg.channel;
|
||||||
|
if (id !== channel_id) {
|
||||||
|
let channel = client.channels.get(id);
|
||||||
|
if (channel) {
|
||||||
|
if (channel.channel_type === 'TextChannel') {
|
||||||
|
history.push(`/server/${channel.server}/channel/${id}`);
|
||||||
|
} else {
|
||||||
|
history.push(`/channel/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
notifications[msg.channel] = notif;
|
||||||
|
notif.addEventListener(
|
||||||
|
"close",
|
||||||
|
() => delete notifications[msg.channel]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function relationship(user: User, property: string) {
|
||||||
|
if (client.user?.status?.presence === Users.Presence.Busy) return;
|
||||||
|
if (property !== "relationship") return;
|
||||||
|
if (!showNotification) return;
|
||||||
|
|
||||||
|
let event;
|
||||||
|
switch (user.relationship) {
|
||||||
|
case Users.Relationship.Incoming:
|
||||||
|
event = translate(
|
||||||
|
"notifications.sent_request",
|
||||||
|
"",
|
||||||
|
intl.dictionary,
|
||||||
|
{ person: user.username }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case Users.Relationship.Friend:
|
||||||
|
event = translate(
|
||||||
|
"notifications.now_friends",
|
||||||
|
"",
|
||||||
|
intl.dictionary,
|
||||||
|
{ person: user.username }
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let notif = await createNotification(event, {
|
||||||
|
icon: client.users.getAvatarURL(user._id, { max_side: 256 }),
|
||||||
|
badge: '/assets/icons/android-chrome-512x512.png',
|
||||||
|
timestamp: +new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
notif?.addEventListener("click", () => {
|
||||||
|
history.push(`/friends`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
client.addListener("message", message);
|
||||||
|
client.users.addListener("mutation", relationship);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
client.removeListener("message", message);
|
||||||
|
client.users.removeListener("mutation", relationship);
|
||||||
|
};
|
||||||
|
}, [client, guild_id, channel_id, showNotification]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function visChange() {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
if (notifications[channel_id]) {
|
||||||
|
notifications[channel_id].close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visChange();
|
||||||
|
|
||||||
|
document.addEventListener("visibilitychange", visChange);
|
||||||
|
return () =>
|
||||||
|
document.removeEventListener("visibilitychange", visChange);
|
||||||
|
}, [guild_id, channel_id]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotifierComponent = connectState(
|
||||||
|
Notifier,
|
||||||
|
state => {
|
||||||
|
return {
|
||||||
|
options: state.settings.notification
|
||||||
|
};
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Notifications() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route path="/channel/:channel">
|
||||||
|
<NotifierComponent />
|
||||||
|
</Route>
|
||||||
|
<Route path="/">
|
||||||
|
<NotifierComponent />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { WithDispatcher } from "../../redux/reducers";
|
||||||
import { AuthState } from "../../redux/reducers/auth";
|
import { AuthState } from "../../redux/reducers/auth";
|
||||||
import { SyncOptions } from "../../redux/reducers/sync";
|
import { SyncOptions } from "../../redux/reducers/sync";
|
||||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||||
|
import { useIntermediate } from '../intermediate/Intermediate';
|
||||||
import { registerEvents, setReconnectDisallowed } from "./events";
|
import { registerEvents, setReconnectDisallowed } from "./events";
|
||||||
import { SingletonMessageRenderer } from '../../lib/renderer/Singleton';
|
import { SingletonMessageRenderer } from '../../lib/renderer/Singleton';
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ type Props = WithDispatcher & {
|
||||||
};
|
};
|
||||||
|
|
||||||
function Context({ auth, sync, children, dispatcher }: Props) {
|
function Context({ auth, sync, children, dispatcher }: Props) {
|
||||||
|
const { openScreen } = useIntermediate();
|
||||||
const [status, setStatus] = useState(ClientStatus.INIT);
|
const [status, setStatus] = useState(ClientStatus.INIT);
|
||||||
const [client, setClient] = useState<Client>(undefined as unknown as Client);
|
const [client, setClient] = useState<Client>(undefined as unknown as Client);
|
||||||
|
|
||||||
|
@ -92,13 +94,13 @@ function Context({ auth, sync, children, dispatcher }: Props) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onboarding) {
|
if (onboarding) {
|
||||||
/*openScreen({
|
openScreen({
|
||||||
id: "onboarding",
|
id: "onboarding",
|
||||||
callback: async (username: string) => {
|
callback: async (username: string) => {
|
||||||
await (onboarding as any)(username, true);
|
await (onboarding as any)(username, true);
|
||||||
login();
|
login();
|
||||||
}
|
}
|
||||||
});*/
|
});
|
||||||
} else {
|
} else {
|
||||||
login();
|
login();
|
||||||
}
|
}
|
||||||
|
@ -113,7 +115,7 @@ function Context({ auth, sync, children, dispatcher }: Props) {
|
||||||
delete client.user;
|
delete client.user;
|
||||||
dispatcher({ type: "RESET" });
|
dispatcher({ type: "RESET" });
|
||||||
|
|
||||||
// openScreen({ id: "none" });
|
openScreen({ id: "none" });
|
||||||
setStatus(ClientStatus.READY);
|
setStatus(ClientStatus.READY);
|
||||||
|
|
||||||
client.websocket.disconnect();
|
client.websocket.disconnect();
|
||||||
|
@ -168,32 +170,17 @@ function Context({ auth, sync, children, dispatcher }: Props) {
|
||||||
active.session
|
active.session
|
||||||
);
|
);
|
||||||
|
|
||||||
//if (callback) {
|
if (callback) {
|
||||||
/*openScreen({ id: "onboarding", callback });*/
|
openScreen({ id: "onboarding", callback });
|
||||||
//} else {
|
}
|
||||||
/*
|
|
||||||
// ! FIXME: all this code needs to be re-written
|
|
||||||
(async () => {
|
|
||||||
// ! FIXME: should be included in Ready payload
|
|
||||||
props.dispatcher({
|
|
||||||
type: 'SYNC_UPDATE',
|
|
||||||
// ! FIXME: write a procedure to resolve merge conflicts
|
|
||||||
update: mapSync(
|
|
||||||
await client.syncFetchSettings(DEFAULT_ENABLED_SYNC.filter(x => !props.sync?.disabled?.includes(x)))
|
|
||||||
)
|
|
||||||
});
|
|
||||||
})()
|
|
||||||
|
|
||||||
props.dispatcher({ type: 'UNREADS_SET', unreads: await client.syncFetchUnreads() });*/
|
|
||||||
//}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatus(ClientStatus.DISCONNECTED);
|
setStatus(ClientStatus.DISCONNECTED);
|
||||||
const error = takeError(err);
|
const error = takeError(err);
|
||||||
if (error === "Forbidden") {
|
if (error === "Forbidden" || error === "Unauthorized") {
|
||||||
operations.logout(true);
|
operations.logout(true);
|
||||||
// openScreen({ id: "signed_out" });
|
openScreen({ id: "signed_out" });
|
||||||
} else {
|
} else {
|
||||||
// openScreen({ id: "error", error });
|
openScreen({ id: "error", error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
78
src/context/revoltjs/StateMonitor.tsx
Normal file
78
src/context/revoltjs/StateMonitor.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* This file monitors the message cache to delete any queued messages that have already sent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Message } from "revolt.js";
|
||||||
|
import { AppContext } from "./RevoltClient";
|
||||||
|
import { Typing } from "../../redux/reducers/typing";
|
||||||
|
import { useContext, useEffect } from "preact/hooks";
|
||||||
|
import { connectState } from "../../redux/connector";
|
||||||
|
import { WithDispatcher } from "../../redux/reducers";
|
||||||
|
import { QueuedMessage } from "../../redux/reducers/queue";
|
||||||
|
|
||||||
|
type Props = WithDispatcher & {
|
||||||
|
messages: QueuedMessage[];
|
||||||
|
typing: Typing
|
||||||
|
};
|
||||||
|
|
||||||
|
function StateMonitor(props: Props) {
|
||||||
|
const client = useContext(AppContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
props.dispatcher({
|
||||||
|
type: 'QUEUE_DROP_ALL'
|
||||||
|
});
|
||||||
|
}, [ ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function add(msg: Message) {
|
||||||
|
if (!msg.nonce) return;
|
||||||
|
if (!props.messages.find(x => x.id === msg.nonce)) return;
|
||||||
|
|
||||||
|
props.dispatcher({
|
||||||
|
type: 'QUEUE_REMOVE',
|
||||||
|
nonce: msg.nonce
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
client.addListener('message', add);
|
||||||
|
return () => client.removeListener('message', add);
|
||||||
|
}, [ props.messages ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function removeOld() {
|
||||||
|
if (!props.typing) return;
|
||||||
|
for (let channel of Object.keys(props.typing)) {
|
||||||
|
let users = props.typing[channel];
|
||||||
|
|
||||||
|
for (let user of users) {
|
||||||
|
if (+ new Date() > user.started + 5000) {
|
||||||
|
props.dispatcher({
|
||||||
|
type: 'TYPING_STOP',
|
||||||
|
channel,
|
||||||
|
user: user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeOld();
|
||||||
|
|
||||||
|
let interval = setInterval(removeOld, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [ props.typing ]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connectState(
|
||||||
|
StateMonitor,
|
||||||
|
state => {
|
||||||
|
return {
|
||||||
|
messages: [...state.queue],
|
||||||
|
typing: state.typing
|
||||||
|
};
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
124
src/context/revoltjs/SyncManager.tsx
Normal file
124
src/context/revoltjs/SyncManager.tsx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
/**
|
||||||
|
* This file monitors changes to settings and syncs them to the server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import isEqual from "lodash.isequal";
|
||||||
|
import { Language } from "../Locale";
|
||||||
|
import { Sync } from "revolt.js/dist/api/objects";
|
||||||
|
import { useContext, useEffect } from "preact/hooks";
|
||||||
|
import { connectState } from "../../redux/connector";
|
||||||
|
import { WithDispatcher } from "../../redux/reducers";
|
||||||
|
import { Settings } from "../../redux/reducers/settings";
|
||||||
|
import { AppContext, ClientStatus, StatusContext } from "./RevoltClient";
|
||||||
|
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
|
||||||
|
import { DEFAULT_ENABLED_SYNC, SyncData, SyncKeys, SyncOptions } from "../../redux/reducers/sync";
|
||||||
|
|
||||||
|
type Props = WithDispatcher & {
|
||||||
|
settings: Settings,
|
||||||
|
locale: Language,
|
||||||
|
sync: SyncOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
var lastValues: { [key in SyncKeys]?: any } = { };
|
||||||
|
|
||||||
|
export function mapSync(packet: Sync.UserSettings, revision?: { [key: string]: number }) {
|
||||||
|
let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = {};
|
||||||
|
for (let key of Object.keys(packet)) {
|
||||||
|
let [ timestamp, obj ] = packet[key];
|
||||||
|
if (timestamp < (revision ?? {} as any)[key] ?? 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let object;
|
||||||
|
if (obj[0] === '{') {
|
||||||
|
object = JSON.parse(obj)
|
||||||
|
} else {
|
||||||
|
object = obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastValues[key as SyncKeys] = object;
|
||||||
|
update[key as SyncKeys] = [ timestamp, object ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SyncManager(props: Props) {
|
||||||
|
const client = useContext(AppContext);
|
||||||
|
const status = useContext(StatusContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === ClientStatus.ONLINE) {
|
||||||
|
client
|
||||||
|
.syncFetchSettings(DEFAULT_ENABLED_SYNC.filter(x => !props.sync?.disabled?.includes(x)))
|
||||||
|
.then(data => {
|
||||||
|
props.dispatcher({
|
||||||
|
type: 'SYNC_UPDATE',
|
||||||
|
update: mapSync(data)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client
|
||||||
|
.syncFetchUnreads()
|
||||||
|
.then(unreads => props.dispatcher({ type: 'UNREADS_SET', unreads }));
|
||||||
|
}
|
||||||
|
}, [ status ]);
|
||||||
|
|
||||||
|
function syncChange(key: SyncKeys, data: any) {
|
||||||
|
let timestamp = + new Date();
|
||||||
|
props.dispatcher({
|
||||||
|
type: 'SYNC_SET_REVISION',
|
||||||
|
key,
|
||||||
|
timestamp
|
||||||
|
});
|
||||||
|
|
||||||
|
client.syncSetSettings({
|
||||||
|
[key]: data
|
||||||
|
}, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
let disabled = props.sync.disabled ?? [];
|
||||||
|
for (let [key, object] of [ ['appearance', props.settings.appearance], ['theme', props.settings.theme], ['locale', props.locale] ] as [SyncKeys, any][]) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (disabled.indexOf(key) === -1) {
|
||||||
|
if (typeof lastValues[key] !== 'undefined') {
|
||||||
|
if (!isEqual(lastValues[key], object)) {
|
||||||
|
syncChange(key, object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastValues[key] = object;
|
||||||
|
}, [ disabled, object ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onPacket(packet: ClientboundNotification) {
|
||||||
|
if (packet.type === 'UserSettingsUpdate') {
|
||||||
|
let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = mapSync(packet.update, props.sync.revision);
|
||||||
|
|
||||||
|
props.dispatcher({
|
||||||
|
type: 'SYNC_UPDATE',
|
||||||
|
update
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.addListener('packet', onPacket);
|
||||||
|
return () => client.removeListener('packet', onPacket);
|
||||||
|
}, [ disabled, props.sync ]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connectState(
|
||||||
|
SyncManager,
|
||||||
|
state => {
|
||||||
|
return {
|
||||||
|
settings: state.settings,
|
||||||
|
locale: state.locale,
|
||||||
|
sync: state.sync
|
||||||
|
};
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
|
@ -3,8 +3,11 @@ import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
|
||||||
import { Switch, Route } from "react-router-dom";
|
import { Switch, Route } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import Popovers from "../context/intermediate/Popovers";
|
|
||||||
import ContextMenus from "../lib/ContextMenus";
|
import ContextMenus from "../lib/ContextMenus";
|
||||||
|
import Popovers from "../context/intermediate/Popovers";
|
||||||
|
import SyncManager from "../context/revoltjs/SyncManager";
|
||||||
|
import StateMonitor from "../context/revoltjs/StateMonitor";
|
||||||
|
import Notifications from "../context/revoltjs/Notifications";
|
||||||
|
|
||||||
import LeftSidebar from "../components/navigation/LeftSidebar";
|
import LeftSidebar from "../components/navigation/LeftSidebar";
|
||||||
import RightSidebar from "../components/navigation/RightSidebar";
|
import RightSidebar from "../components/navigation/RightSidebar";
|
||||||
|
@ -57,6 +60,9 @@ export default function App() {
|
||||||
</Routes>
|
</Routes>
|
||||||
<ContextMenus />
|
<ContextMenus />
|
||||||
<Popovers />
|
<Popovers />
|
||||||
|
<Notifications />
|
||||||
|
<StateMonitor />
|
||||||
|
<SyncManager />
|
||||||
</OverlappingPanels>
|
</OverlappingPanels>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -1167,6 +1167,18 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.1.tgz#4d26a9efe3aa2caf829234ec5a39580fc88b6001"
|
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.1.tgz#4d26a9efe3aa2caf829234ec5a39580fc88b6001"
|
||||||
integrity sha512-pQv3Sygwxxh6jYQzXaiyWDAHevJqWtqDUv6t11Sa9CPGiXny66II7Pl6PR8QO5OVysD6HYOkHMeBgIjLnk9SkQ==
|
integrity sha512-pQv3Sygwxxh6jYQzXaiyWDAHevJqWtqDUv6t11Sa9CPGiXny66II7Pl6PR8QO5OVysD6HYOkHMeBgIjLnk9SkQ==
|
||||||
|
|
||||||
|
"@types/lodash.isequal@^4.5.5":
|
||||||
|
version "4.5.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz#4fed1b1b00bef79e305de0352d797e9bb816c8ff"
|
||||||
|
integrity sha512-4IKbinG7MGP131wRfceK6W4E/Qt3qssEFLF30LnJbjYiSfHGGRU/Io8YxXrZX109ir+iDETC8hw8QsDijukUVg==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash@*":
|
||||||
|
version "4.14.170"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
|
||||||
|
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
|
||||||
|
|
||||||
"@types/markdown-it@^12.0.2":
|
"@types/markdown-it@^12.0.2":
|
||||||
version "12.0.2"
|
version "12.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.0.2.tgz#153e5477970ed2a47b2f619ed4ab66f870de8a04"
|
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-12.0.2.tgz#153e5477970ed2a47b2f619ed4ab66f870de8a04"
|
||||||
|
|
Loading…
Reference in a new issue