Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 722 additions and 686 deletions
import { Channel } from "revolt.js/dist/maps/Channels";
import { createContext } from "preact"; import { createContext } from "preact";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "preact/hooks";
import type { ProduceType, VoiceUser } from "../lib/vortex/Types"; import type { ProduceType, VoiceUser } from "../lib/vortex/Types";
import type VoiceClient from "../lib/vortex/VoiceClient"; import type VoiceClient from "../lib/vortex/VoiceClient";
import { Children } from "../types/Preact"; import { Children } from "../types/Preact";
import { SoundContext } from "./Settings"; import { SoundContext } from "./Settings";
import { AppContext } from "./revoltjs/RevoltClient";
import { useForceUpdate } from "./revoltjs/hooks";
export enum VoiceStatus { export enum VoiceStatus {
LOADING = 0, LOADING = 0,
...@@ -22,7 +29,7 @@ export enum VoiceStatus { ...@@ -22,7 +29,7 @@ export enum VoiceStatus {
} }
export interface VoiceOperations { export interface VoiceOperations {
connect: (channelId: string) => Promise<void>; connect: (channel: Channel) => Promise<Channel>;
disconnect: () => void; disconnect: () => void;
isProducing: (type: ProduceType) => boolean; isProducing: (type: ProduceType) => boolean;
startProducing: (type: ProduceType) => Promise<void>; startProducing: (type: ProduceType) => Promise<void>;
...@@ -44,20 +51,22 @@ type Props = { ...@@ -44,20 +51,22 @@ type Props = {
}; };
export default function Voice({ children }: Props) { export default function Voice({ children }: Props) {
const revoltClient = useContext(AppContext);
const [client, setClient] = useState<VoiceClient | undefined>(undefined); const [client, setClient] = useState<VoiceClient | undefined>(undefined);
const [state, setState] = useState<VoiceState>({ const [state, setState] = useState<VoiceState>({
status: VoiceStatus.LOADING, status: VoiceStatus.LOADING,
participants: new Map(), participants: new Map(),
}); });
function setStatus(status: VoiceStatus, roomId?: string) { const setStatus = useCallback(
setState({ (status: VoiceStatus, roomId?: string) => {
status, setState({
roomId: roomId ?? client?.roomId, status,
participants: client?.participants ?? new Map(), roomId: roomId ?? client?.roomId,
}); participants: client?.participants ?? new Map(),
} });
},
[client?.participants, client?.roomId],
);
useEffect(() => { useEffect(() => {
import("../lib/vortex/VoiceClient") import("../lib/vortex/VoiceClient")
...@@ -75,32 +84,30 @@ export default function Voice({ children }: Props) { ...@@ -75,32 +84,30 @@ export default function Voice({ children }: Props) {
console.error("Failed to load voice library!", err); console.error("Failed to load voice library!", err);
setStatus(VoiceStatus.UNAVAILABLE); setStatus(VoiceStatus.UNAVAILABLE);
}); });
}, []); }, [setStatus]);
const isConnecting = useRef(false); const isConnecting = useRef(false);
const operations: VoiceOperations = useMemo(() => { const operations: VoiceOperations = useMemo(() => {
return { return {
connect: async (channelId) => { connect: async (channel) => {
if (!client?.supported()) throw new Error("RTC is unavailable"); if (!client?.supported()) throw new Error("RTC is unavailable");
isConnecting.current = true; isConnecting.current = true;
setStatus(VoiceStatus.CONNECTING, channelId); setStatus(VoiceStatus.CONNECTING, channel._id);
try { try {
const call = await revoltClient.channels.joinCall( const call = await channel.joinCall();
channelId,
);
if (!isConnecting.current) { if (!isConnecting.current) {
setStatus(VoiceStatus.READY); setStatus(VoiceStatus.READY);
return; return channel;
} }
// ! FIXME: use configuration to check if voso is enabled // ! TODO: use configuration to check if voso is enabled
// await client.connect("wss://voso.revolt.chat/ws"); // await client.connect("wss://voso.revolt.chat/ws");
await client.connect( await client.connect(
"wss://voso.revolt.chat/ws", "wss://voso.revolt.chat/ws",
channelId, channel._id,
); );
setStatus(VoiceStatus.AUTHENTICATING); setStatus(VoiceStatus.AUTHENTICATING);
...@@ -112,11 +119,12 @@ export default function Voice({ children }: Props) { ...@@ -112,11 +119,12 @@ export default function Voice({ children }: Props) {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setStatus(VoiceStatus.READY); setStatus(VoiceStatus.READY);
return; return channel;
} }
setStatus(VoiceStatus.CONNECTED); setStatus(VoiceStatus.CONNECTED);
isConnecting.current = false; isConnecting.current = false;
return channel;
}, },
disconnect: () => { disconnect: () => {
if (!client?.supported()) throw new Error("RTC is unavailable"); if (!client?.supported()) throw new Error("RTC is unavailable");
...@@ -138,9 +146,9 @@ export default function Voice({ children }: Props) { ...@@ -138,9 +146,9 @@ export default function Voice({ children }: Props) {
switch (type) { switch (type) {
case "audio": { case "audio": {
if (client?.audioProducer !== undefined) if (client?.audioProducer !== undefined)
return console.log("No audio producer."); // ! FIXME: let the user know return console.log("No audio producer."); // ! TODO: let the user know
if (navigator.mediaDevices === undefined) if (navigator.mediaDevices === undefined)
return console.log("No media devices."); // ! FIXME: let the user know return console.log("No media devices."); // ! TODO: let the user know
const mediaStream = const mediaStream =
await navigator.mediaDevices.getUserMedia({ await navigator.mediaDevices.getUserMedia({
audio: true, audio: true,
...@@ -158,44 +166,49 @@ export default function Voice({ children }: Props) { ...@@ -158,44 +166,49 @@ export default function Voice({ children }: Props) {
return client?.stopProduce(type); return client?.stopProduce(type);
}, },
}; };
}, [client]); }, [client, setStatus]);
const { forceUpdate } = useForceUpdate();
const playSound = useContext(SoundContext); const playSound = useContext(SoundContext);
useEffect(() => { useEffect(() => {
if (!client?.supported()) return; if (!client?.supported()) return;
// ! FIXME: message for fatal: // ! TODO: message for fatal:
// ! get rid of these force updates // ! get rid of these force updates
// ! handle it through state or smth // ! handle it through state or smth
client.on("startProduce", forceUpdate); function stateUpdate() {
client.on("stopProduce", forceUpdate); setStatus(state.status);
}
client.on("startProduce", stateUpdate);
client.on("stopProduce", stateUpdate);
client.on("userJoined", () => { client.on("userJoined", () => {
playSound("call_join"); playSound("call_join");
forceUpdate(); stateUpdate();
}); });
client.on("userLeft", () => { client.on("userLeft", () => {
playSound("call_leave"); playSound("call_leave");
forceUpdate(); stateUpdate();
}); });
client.on("userStartProduce", forceUpdate);
client.on("userStopProduce", forceUpdate); client.on("userStartProduce", stateUpdate);
client.on("close", forceUpdate); client.on("userStopProduce", stateUpdate);
client.on("close", stateUpdate);
return () => { return () => {
client.removeListener("startProduce", forceUpdate); client.removeListener("startProduce", stateUpdate);
client.removeListener("stopProduce", forceUpdate); client.removeListener("stopProduce", stateUpdate);
client.removeListener("userJoined", forceUpdate); client.removeListener("userJoined", stateUpdate);
client.removeListener("userLeft", forceUpdate); client.removeListener("userLeft", stateUpdate);
client.removeListener("userStartProduce", forceUpdate); client.removeListener("userStartProduce", stateUpdate);
client.removeListener("userStopProduce", forceUpdate); client.removeListener("userStopProduce", stateUpdate);
client.removeListener("close", forceUpdate); client.removeListener("close", stateUpdate);
}; };
}, [client, state]); }, [client, state, playSound, setStatus]);
return ( return (
<VoiceContext.Provider value={state}> <VoiceContext.Provider value={state}>
......
import { Prompt } from "react-router"; import { Prompt } from "react-router";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { import type { Attachment } from "revolt-api/types/Autumn";
Attachment, import type { EmbedImage } from "revolt-api/types/January";
Channels, import { Channel } from "revolt.js/dist/maps/Channels";
EmbedImage, import { Message } from "revolt.js/dist/maps/Messages";
Servers, import { Server } from "revolt.js/dist/maps/Servers";
Users, import { User } from "revolt.js/dist/maps/Users";
} from "revolt.js/dist/api/objects";
import { createContext } from "preact"; import { createContext } from "preact";
import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { useContext, useEffect, useMemo, useState } from "preact/hooks";
...@@ -32,21 +31,21 @@ export type Screen = ...@@ -32,21 +31,21 @@ export type Screen =
actions: Action[]; actions: Action[];
} }
| ({ id: "special_prompt" } & ( | ({ id: "special_prompt" } & (
| { type: "leave_group"; target: Channels.GroupChannel } | { type: "leave_group"; target: Channel }
| { type: "close_dm"; target: Channels.DirectMessageChannel } | { type: "close_dm"; target: Channel }
| { type: "leave_server"; target: Servers.Server } | { type: "leave_server"; target: Server }
| { type: "delete_server"; target: Servers.Server } | { type: "delete_server"; target: Server }
| { type: "delete_channel"; target: Channels.TextChannel } | { type: "delete_channel"; target: Channel }
| { type: "delete_message"; target: Channels.Message } | { type: "delete_message"; target: Message }
| { | {
type: "create_invite"; type: "create_invite";
target: Channels.TextChannel | Channels.GroupChannel; target: Channel;
} }
| { type: "kick_member"; target: Servers.Server; user: string } | { type: "kick_member"; target: Server; user: User }
| { type: "ban_member"; target: Servers.Server; user: string } | { type: "ban_member"; target: Server; user: User }
| { type: "unfriend_user"; target: Users.User } | { type: "unfriend_user"; target: User }
| { type: "block_user"; target: Users.User } | { type: "block_user"; target: User }
| { type: "create_channel"; target: Servers.Server } | { type: "create_channel"; target: Server }
)) ))
| ({ id: "special_input" } & ( | ({ id: "special_input" } & (
| { | {
...@@ -58,7 +57,7 @@ export type Screen = ...@@ -58,7 +57,7 @@ export type Screen =
} }
| { | {
type: "create_role"; type: "create_role";
server: string; server: Server;
callback: (id: string) => void; callback: (id: string) => void;
} }
)) ))
...@@ -81,8 +80,8 @@ export type Screen = ...@@ -81,8 +80,8 @@ export type Screen =
| { id: "image_viewer"; attachment?: Attachment; embed?: EmbedImage } | { id: "image_viewer"; attachment?: Attachment; embed?: EmbedImage }
| { id: "modify_account"; field: "username" | "email" | "password" } | { id: "modify_account"; field: "username" | "email" | "password" }
| { id: "profile"; user_id: string } | { id: "profile"; user_id: string }
| { id: "channel_info"; channel_id: string } | { id: "channel_info"; channel: Channel }
| { id: "pending_requests"; users: string[] } | { id: "pending_requests"; users: User[] }
| { | {
id: "user_picker"; id: "user_picker";
omit?: string[]; omit?: string[];
...@@ -90,13 +89,16 @@ export type Screen = ...@@ -90,13 +89,16 @@ export type Screen =
}; };
export const IntermediateContext = createContext({ export const IntermediateContext = createContext({
screen: { id: "none" } as Screen, screen: { id: "none" },
focusTaken: false, focusTaken: false,
}); });
export const IntermediateActionsContext = createContext({ export const IntermediateActionsContext = createContext<{
openScreen: (screen: Screen) => {}, openScreen: (screen: Screen) => void;
writeClipboard: (text: string) => {}, writeClipboard: (text: string) => void;
}>({
openScreen: null!,
writeClipboard: null!,
}); });
interface Props { interface Props {
...@@ -131,12 +133,20 @@ export default function Intermediate(props: Props) { ...@@ -131,12 +133,20 @@ export default function Intermediate(props: Props) {
const navigate = (path: string) => history.push(path); const navigate = (path: string) => history.push(path);
const subs = [ const subs = [
internalSubscribe("Intermediate", "openProfile", openProfile), internalSubscribe(
internalSubscribe("Intermediate", "navigate", navigate), "Intermediate",
"openProfile",
openProfile as (...args: unknown[]) => void,
),
internalSubscribe(
"Intermediate",
"navigate",
navigate as (...args: unknown[]) => void,
),
]; ];
return () => subs.map((unsub) => unsub()); return () => subs.map((unsub) => unsub());
}, []); }, [history]);
return ( return (
<IntermediateContext.Provider value={value}> <IntermediateContext.Provider value={value}>
......
import { isModalClosing } from "../../components/ui/Modal";
import { internalEmit } from "../../lib/eventEmitter"; import { internalEmit } from "../../lib/eventEmitter";
import { isModalClosing } from "../../components/ui/Modal";
import { Screen } from "./Intermediate"; import { Screen } from "./Intermediate";
import { ClipboardModal } from "./modals/Clipboard"; import { ClipboardModal } from "./modals/Clipboard";
import { ErrorModal } from "./modals/Error"; import { ErrorModal } from "./modals/Error";
...@@ -10,11 +12,14 @@ import { SignedOutModal } from "./modals/SignedOut"; ...@@ -10,11 +12,14 @@ import { SignedOutModal } from "./modals/SignedOut";
export interface Props { export interface Props {
screen: Screen; screen: Screen;
openScreen: (id: any) => void; openScreen: (screen: Screen) => void;
} }
export default function Modals({ screen, openScreen }: Props) { export default function Modals({ screen, openScreen }: Props) {
const onClose = () => isModalClosing ? openScreen({ id: "none" }) : internalEmit('Modal', 'close'); const onClose = () =>
isModalClosing || screen.id === "onboarding"
? openScreen({ id: "none" })
: internalEmit("Modal", "close");
switch (screen.id) { switch (screen.id) {
case "_prompt": case "_prompt":
......
import { useContext } from "preact/hooks"; import { useContext } from "preact/hooks";
import { isModalClosing } from "../../components/ui/Modal";
import { internalEmit } from "../../lib/eventEmitter"; import { internalEmit } from "../../lib/eventEmitter";
import { isModalClosing } from "../../components/ui/Modal";
import { IntermediateContext, useIntermediate } from "./Intermediate"; import { IntermediateContext, useIntermediate } from "./Intermediate";
import { SpecialInputModal } from "./modals/Input"; import { SpecialInputModal } from "./modals/Input";
import { SpecialPromptModal } from "./modals/Prompt"; import { SpecialPromptModal } from "./modals/Prompt";
...@@ -16,7 +18,10 @@ export default function Popovers() { ...@@ -16,7 +18,10 @@ export default function Popovers() {
const { screen } = useContext(IntermediateContext); const { screen } = useContext(IntermediateContext);
const { openScreen } = useIntermediate(); const { openScreen } = useIntermediate();
const onClose = () => isModalClosing ? openScreen({ id: "none" }) : internalEmit('Modal', 'close'); const onClose = () =>
isModalClosing
? openScreen({ id: "none" })
: internalEmit("Modal", "close");
switch (screen.id) { switch (screen.id) {
case "profile": case "profile":
......
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import { Server } from "revolt.js/dist/maps/Servers";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
...@@ -81,7 +82,7 @@ type SpecialProps = { onClose: () => void } & ( ...@@ -81,7 +82,7 @@ type SpecialProps = { onClose: () => void } & (
| "set_custom_status" | "set_custom_status"
| "add_friend"; | "add_friend";
} }
| { type: "create_role"; server: string; callback: (id: string) => void } | { type: "create_role"; server: Server; callback: (id: string) => void }
); );
export function SpecialInputModal(props: SpecialProps) { export function SpecialInputModal(props: SpecialProps) {
...@@ -134,10 +135,7 @@ export function SpecialInputModal(props: SpecialProps) { ...@@ -134,10 +135,7 @@ export function SpecialInputModal(props: SpecialProps) {
} }
field={<Text id="app.settings.permissions.role_name" />} field={<Text id="app.settings.permissions.role_name" />}
callback={async (name) => { callback={async (name) => {
const role = await client.servers.createRole( const role = await props.server.createRole(name);
props.server,
name,
);
props.callback(role.id); props.callback(role.id);
}} }}
/> />
...@@ -151,7 +149,7 @@ export function SpecialInputModal(props: SpecialProps) { ...@@ -151,7 +149,7 @@ export function SpecialInputModal(props: SpecialProps) {
field={<Text id="app.context_menu.custom_status" />} field={<Text id="app.context_menu.custom_status" />}
defaultValue={client.user?.status?.text} defaultValue={client.user?.status?.text}
callback={(text) => callback={(text) =>
client.users.editUser({ client.users.edit({
status: { status: {
...client.user?.status, ...client.user?.status,
text: text.trim().length > 0 ? text : undefined, text: text.trim().length > 0 ? text : undefined,
...@@ -166,7 +164,14 @@ export function SpecialInputModal(props: SpecialProps) { ...@@ -166,7 +164,14 @@ export function SpecialInputModal(props: SpecialProps) {
<InputModal <InputModal
onClose={onClose} onClose={onClose}
question={"Add Friend"} question={"Add Friend"}
callback={(username) => client.users.addFriend(username)} callback={(username) =>
client
.req(
"PUT",
`/users/${username}/friend` as "/users/id/friend",
)
.then(undefined)
}
/> />
); );
} }
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
margin: auto; margin: auto;
display: block; display: block;
max-height: 420px; max-height: 420px;
border-radius: 8px; border-radius: var(--border-radius);
} }
input { input {
......
...@@ -28,8 +28,8 @@ export function OnboardingModal({ onClose, callback }: Props) { ...@@ -28,8 +28,8 @@ export function OnboardingModal({ onClose, callback }: Props) {
const onSubmit: SubmitHandler<FormInputs> = ({ username }) => { const onSubmit: SubmitHandler<FormInputs> = ({ username }) => {
setLoading(true); setLoading(true);
callback(username, true) callback(username, true)
.then(onClose) .then(() => onClose())
.catch((err: any) => { .catch((err: unknown) => {
setError(takeError(err)); setError(takeError(err));
setLoading(false); setLoading(false);
}); });
...@@ -40,7 +40,7 @@ export function OnboardingModal({ onClose, callback }: Props) { ...@@ -40,7 +40,7 @@ export function OnboardingModal({ onClose, callback }: Props) {
<div className={styles.header}> <div className={styles.header}>
<h1> <h1>
<Text id="app.special.modals.onboarding.welcome" /> <Text id="app.special.modals.onboarding.welcome" />
<img src={wideSVG} /> <img src={wideSVG} loading="eager" />
</h1> </h1>
</div> </div>
<div className={styles.form}> <div className={styles.form}>
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
user-select: all; user-select: all;
font-size: 1.4em; font-size: 1.4em;
text-align: center; text-align: center;
font-family: var(--monoscape-font); font-family: var(--monospace-font);
} }
} }
......
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Channels, Servers, Users } from "revolt.js/dist/api/objects"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Message as MessageI } from "revolt.js/dist/maps/Messages";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import { ulid } from "ulid"; import { ulid } from "ulid";
import styles from "./Prompt.module.scss"; import styles from "./Prompt.module.scss";
...@@ -17,7 +21,7 @@ import Radio from "../../../components/ui/Radio"; ...@@ -17,7 +21,7 @@ import Radio from "../../../components/ui/Radio";
import { Children } from "../../../types/Preact"; import { Children } from "../../../types/Preact";
import { AppContext } from "../../revoltjs/RevoltClient"; import { AppContext } from "../../revoltjs/RevoltClient";
import { mapMessage, takeError } from "../../revoltjs/util"; import { takeError } from "../../revoltjs/util";
import { useIntermediate } from "../Intermediate"; import { useIntermediate } from "../Intermediate";
interface Props { interface Props {
...@@ -51,24 +55,24 @@ export function PromptModal({ ...@@ -51,24 +55,24 @@ export function PromptModal({
} }
type SpecialProps = { onClose: () => void } & ( type SpecialProps = { onClose: () => void } & (
| { type: "leave_group"; target: Channels.GroupChannel } | { type: "leave_group"; target: Channel }
| { type: "close_dm"; target: Channels.DirectMessageChannel } | { type: "close_dm"; target: Channel }
| { type: "leave_server"; target: Servers.Server } | { type: "leave_server"; target: Server }
| { type: "delete_server"; target: Servers.Server } | { type: "delete_server"; target: Server }
| { type: "delete_channel"; target: Channels.TextChannel } | { type: "delete_channel"; target: Channel }
| { type: "delete_message"; target: Channels.Message } | { type: "delete_message"; target: MessageI }
| { | {
type: "create_invite"; type: "create_invite";
target: Channels.TextChannel | Channels.GroupChannel; target: Channel;
} }
| { type: "kick_member"; target: Servers.Server; user: string } | { type: "kick_member"; target: Server; user: User }
| { type: "ban_member"; target: Servers.Server; user: string } | { type: "ban_member"; target: Server; user: User }
| { type: "unfriend_user"; target: Users.User } | { type: "unfriend_user"; target: User }
| { type: "block_user"; target: Users.User } | { type: "block_user"; target: User }
| { type: "create_channel"; target: Servers.Server } | { type: "create_channel"; target: Server }
); );
export function SpecialPromptModal(props: SpecialProps) { export const SpecialPromptModal = observer((props: SpecialProps) => {
const client = useContext(AppContext); const client = useContext(AppContext);
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [error, setError] = useState<undefined | string>(undefined); const [error, setError] = useState<undefined | string>(undefined);
...@@ -92,7 +96,7 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -92,7 +96,7 @@ export function SpecialPromptModal(props: SpecialProps) {
block_user: ["block_user", "block"], block_user: ["block_user", "block"],
}; };
let event = EVENTS[props.type]; const event = EVENTS[props.type];
let name; let name;
switch (props.type) { switch (props.type) {
case "unfriend_user": case "unfriend_user":
...@@ -100,9 +104,7 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -100,9 +104,7 @@ export function SpecialPromptModal(props: SpecialProps) {
name = props.target.username; name = props.target.username;
break; break;
case "close_dm": case "close_dm":
name = client.users.get( name = props.target.recipient?.username;
client.channels.getRecipient(props.target._id),
)?.username;
break; break;
default: default:
name = props.target.name; name = props.target.name;
...@@ -133,27 +135,19 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -133,27 +135,19 @@ export function SpecialPromptModal(props: SpecialProps) {
try { try {
switch (props.type) { switch (props.type) {
case "unfriend_user": case "unfriend_user":
await client.users.removeFriend( await props.target.removeFriend();
props.target._id,
);
break; break;
case "block_user": case "block_user":
await client.users.blockUser( await props.target.blockUser();
props.target._id,
);
break; break;
case "leave_group": case "leave_group":
case "close_dm": case "close_dm":
case "delete_channel": case "delete_channel":
await client.channels.delete( props.target.delete();
props.target._id,
);
break; break;
case "leave_server": case "leave_server":
case "delete_server": case "delete_server":
await client.servers.delete( props.target.delete();
props.target._id,
);
break; break;
} }
...@@ -199,11 +193,7 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -199,11 +193,7 @@ export function SpecialPromptModal(props: SpecialProps) {
setProcessing(true); setProcessing(true);
try { try {
await client.channels.deleteMessage( props.target.delete();
props.target.channel,
props.target._id,
);
onClose(); onClose();
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
...@@ -225,7 +215,7 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -225,7 +215,7 @@ export function SpecialPromptModal(props: SpecialProps) {
id={`app.special.modals.prompt.confirm_delete_message_long`} id={`app.special.modals.prompt.confirm_delete_message_long`}
/> />
<Message <Message
message={mapMessage(props.target)} message={props.target}
head={true} head={true}
contrast contrast
/> />
...@@ -243,12 +233,12 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -243,12 +233,12 @@ export function SpecialPromptModal(props: SpecialProps) {
useEffect(() => { useEffect(() => {
setProcessing(true); setProcessing(true);
client.channels props.target
.createInvite(props.target._id) .createInvite()
.then((code) => setCode(code)) .then((code) => setCode(code))
.catch((err) => setError(takeError(err))) .catch((err) => setError(takeError(err)))
.finally(() => setProcessing(false)); .finally(() => setProcessing(false));
}, []); }, [props.target]);
return ( return (
<PromptModal <PromptModal
...@@ -286,8 +276,6 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -286,8 +276,6 @@ export function SpecialPromptModal(props: SpecialProps) {
); );
} }
case "kick_member": { case "kick_member": {
const user = client.users.get(props.user);
return ( return (
<PromptModal <PromptModal
onClose={onClose} onClose={onClose}
...@@ -304,10 +292,13 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -304,10 +292,13 @@ export function SpecialPromptModal(props: SpecialProps) {
setProcessing(true); setProcessing(true);
try { try {
await client.servers.members.kickMember( client.members
props.target._id, .getKey({
props.user, server: props.target._id,
); user: props.user._id,
})
?.kick();
onClose(); onClose();
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
...@@ -324,10 +315,10 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -324,10 +315,10 @@ export function SpecialPromptModal(props: SpecialProps) {
]} ]}
content={ content={
<div className={styles.column}> <div className={styles.column}>
<UserIcon target={user} size={64} /> <UserIcon target={props.user} size={64} />
<Text <Text
id="app.special.modals.prompt.confirm_kick" id="app.special.modals.prompt.confirm_kick"
fields={{ name: user?.username }} fields={{ name: props.user?.username }}
/> />
</div> </div>
} }
...@@ -338,7 +329,6 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -338,7 +329,6 @@ export function SpecialPromptModal(props: SpecialProps) {
} }
case "ban_member": { case "ban_member": {
const [reason, setReason] = useState<string | undefined>(undefined); const [reason, setReason] = useState<string | undefined>(undefined);
const user = client.users.get(props.user);
return ( return (
<PromptModal <PromptModal
...@@ -356,11 +346,9 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -356,11 +346,9 @@ export function SpecialPromptModal(props: SpecialProps) {
setProcessing(true); setProcessing(true);
try { try {
await client.servers.banUser( await props.target.banUser(props.user._id, {
props.target._id, reason,
props.user, });
{ reason },
);
onClose(); onClose();
} catch (err) { } catch (err) {
setError(takeError(err)); setError(takeError(err));
...@@ -377,10 +365,10 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -377,10 +365,10 @@ export function SpecialPromptModal(props: SpecialProps) {
]} ]}
content={ content={
<div className={styles.column}> <div className={styles.column}>
<UserIcon target={user} size={64} /> <UserIcon target={props.user} size={64} />
<Text <Text
id="app.special.modals.prompt.confirm_ban" id="app.special.modals.prompt.confirm_ban"
fields={{ name: user?.username }} fields={{ name: props.user?.username }}
/> />
<Overline> <Overline>
<Text id="app.special.modals.prompt.confirm_ban_reason" /> <Text id="app.special.modals.prompt.confirm_ban_reason" />
...@@ -419,14 +407,11 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -419,14 +407,11 @@ export function SpecialPromptModal(props: SpecialProps) {
try { try {
const channel = const channel =
await client.servers.createChannel( await props.target.createChannel({
props.target._id, type,
{ name,
type, nonce: ulid(),
name, });
nonce: ulid(),
},
);
history.push( history.push(
`/server/${props.target._id}/channel/${channel._id}`, `/server/${props.target._id}/channel/${channel._id}`,
...@@ -477,4 +462,4 @@ export function SpecialPromptModal(props: SpecialProps) { ...@@ -477,4 +462,4 @@ export function SpecialPromptModal(props: SpecialProps) {
default: default:
return null; return null;
} }
} });
import { X } from "@styled-icons/boxicons-regular"; import { X } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import styles from "./ChannelInfo.module.scss"; import styles from "./ChannelInfo.module.scss";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import Markdown from "../../../components/markdown/Markdown"; import Markdown from "../../../components/markdown/Markdown";
import { useChannel, useForceUpdate } from "../../revoltjs/hooks";
import { getChannelName } from "../../revoltjs/util"; import { getChannelName } from "../../revoltjs/util";
interface Props { interface Props {
channel_id: string; channel: Channel;
onClose: () => void; onClose: () => void;
} }
export function ChannelInfo({ channel_id, onClose }: Props) { export const ChannelInfo = observer(({ channel, onClose }: Props) => {
const ctx = useForceUpdate();
const channel = useChannel(channel_id, ctx);
if (!channel) return null;
if ( if (
channel.channel_type === "DirectMessage" || channel.channel_type === "DirectMessage" ||
channel.channel_type === "SavedMessages" channel.channel_type === "SavedMessages"
...@@ -30,15 +27,15 @@ export function ChannelInfo({ channel_id, onClose }: Props) { ...@@ -30,15 +27,15 @@ export function ChannelInfo({ channel_id, onClose }: Props) {
<Modal visible={true} onClose={onClose}> <Modal visible={true} onClose={onClose}>
<div className={styles.info}> <div className={styles.info}>
<div className={styles.header}> <div className={styles.header}>
<h1>{getChannelName(ctx.client, channel, true)}</h1> <h1>{getChannelName(channel, true)}</h1>
<div onClick={onClose}> <div onClick={onClose}>
<X size={36} /> <X size={36} />
</div> </div>
</div> </div>
<p> <p>
<Markdown content={channel.description} /> <Markdown content={channel.description!} />
</p> </p>
</div> </div>
</Modal> </Modal>
); );
} });
import { /* eslint-disable react-hooks/rules-of-hooks */
Attachment, import { Attachment, AttachmentMetadata } from "revolt-api/types/Autumn";
AttachmentMetadata, import { EmbedImage } from "revolt-api/types/January";
EmbedImage,
} from "revolt.js/dist/api/objects";
import styles from "./ImageViewer.module.scss"; import styles from "./ImageViewer.module.scss";
import { useContext, useEffect } from "preact/hooks";
import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions"; import AttachmentActions from "../../../components/common/messaging/attachments/AttachmentActions";
import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions"; import EmbedMediaActions from "../../../components/common/messaging/embed/EmbedMediaActions";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import { AppContext } from "../../revoltjs/RevoltClient"; import { useClient } from "../../revoltjs/RevoltClient";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
...@@ -22,12 +19,6 @@ interface Props { ...@@ -22,12 +19,6 @@ interface Props {
type ImageMetadata = AttachmentMetadata & { type: "Image" }; type ImageMetadata = AttachmentMetadata & { type: "Image" };
export function ImageViewer({ attachment, embed, onClose }: Props) { export function ImageViewer({ attachment, embed, onClose }: Props) {
// ! FIXME: temp code
// ! add proxy function to client
function proxyImage(url: string) {
return "https://jan.revolt.chat/proxy?url=" + encodeURIComponent(url);
}
if (attachment && attachment.metadata.type !== "Image") { if (attachment && attachment.metadata.type !== "Image") {
console.warn( console.warn(
`Attempted to use a non valid attatchment type in the image viewer: ${attachment.metadata.type}`, `Attempted to use a non valid attatchment type in the image viewer: ${attachment.metadata.type}`,
...@@ -35,7 +26,7 @@ export function ImageViewer({ attachment, embed, onClose }: Props) { ...@@ -35,7 +26,7 @@ export function ImageViewer({ attachment, embed, onClose }: Props) {
return null; return null;
} }
const client = useContext(AppContext); const client = useClient();
return ( return (
<Modal visible={true} onClose={onClose} noBackground> <Modal visible={true} onClose={onClose} noBackground>
...@@ -43,6 +34,7 @@ export function ImageViewer({ attachment, embed, onClose }: Props) { ...@@ -43,6 +34,7 @@ export function ImageViewer({ attachment, embed, onClose }: Props) {
{attachment && ( {attachment && (
<> <>
<img <img
loading="eager"
src={client.generateFileURL(attachment)} src={client.generateFileURL(attachment)}
width={(attachment.metadata as ImageMetadata).width} width={(attachment.metadata as ImageMetadata).width}
height={ height={
...@@ -55,7 +47,8 @@ export function ImageViewer({ attachment, embed, onClose }: Props) { ...@@ -55,7 +47,8 @@ export function ImageViewer({ attachment, embed, onClose }: Props) {
{embed && ( {embed && (
<> <>
<img <img
src={proxyImage(embed.url)} loading="eager"
src={client.proxyFile(embed.url)}
width={embed.width} width={embed.width}
height={embed.height} height={embed.height}
/> />
......
...@@ -85,11 +85,13 @@ export function ModifyAccountModal({ onClose, field }: Props) { ...@@ -85,11 +85,13 @@ export function ModifyAccountModal({ onClose, field }: Props) {
]}> ]}>
{/* Preact / React typing incompatabilities */} {/* Preact / React typing incompatabilities */}
<form <form
onSubmit={ onSubmit={(e) => {
e.preventDefault();
handleSubmit( handleSubmit(
onSubmit, onSubmit,
) as JSX.GenericEventHandler<HTMLFormElement> // eslint-disable-next-line @typescript-eslint/no-explicit-any
}> )(e as any);
}}>
{field === "email" && ( {field === "email" && (
<FormField <FormField
type="email" type="email"
......
import { observer } from "mobx-react-lite";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./UserPicker.module.scss"; import styles from "./UserPicker.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import { Friend } from "../../../pages/friends/Friend"; import { Friend } from "../../../pages/friends/Friend";
import { useUsers } from "../../revoltjs/hooks";
interface Props { interface Props {
users: string[]; users: User[];
onClose: () => void; onClose: () => void;
} }
export function PendingRequests({ users: ids, onClose }: Props) { export const PendingRequests = observer(({ users, onClose }: Props) => {
const users = useUsers(ids);
return ( return (
<Modal <Modal
visible={true} visible={true}
title={<Text id="app.special.friends.pending" />} title={<Text id="app.special.friends.pending" />}
onClose={onClose}> onClose={onClose}>
<div className={styles.list}> <div className={styles.list}>
{users {users.map((x) => (
.filter((x) => typeof x !== "undefined") <Friend user={x!} key={x!._id} />
.map((x) => ( ))}
<Friend user={x!} key={x!._id} />
))}
</div> </div>
</Modal> </Modal>
); );
} });
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
max-height: 360px; max-height: 360px;
overflow-y: scroll; overflow-y: scroll;
// ! FIXME: very temporary code
> label { > label {
> span { > span {
align-items: flex-start !important; align-items: flex-start !important;
...@@ -18,4 +17,4 @@ ...@@ -18,4 +17,4 @@
} }
} }
} }
} }
\ No newline at end of file
import { User, Users } from "revolt.js/dist/api/objects"; import { RelationshipStatus } from "revolt-api/types/Users";
import styles from "./UserPicker.module.scss"; import styles from "./UserPicker.module.scss";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
...@@ -7,7 +7,7 @@ import { useState } from "preact/hooks"; ...@@ -7,7 +7,7 @@ import { useState } from "preact/hooks";
import UserCheckbox from "../../../components/common/user/UserCheckbox"; import UserCheckbox from "../../../components/common/user/UserCheckbox";
import Modal from "../../../components/ui/Modal"; import Modal from "../../../components/ui/Modal";
import { useUsers } from "../../revoltjs/hooks"; import { useClient } from "../../revoltjs/RevoltClient";
interface Props { interface Props {
omit?: string[]; omit?: string[];
...@@ -19,7 +19,7 @@ export function UserPicker(props: Props) { ...@@ -19,7 +19,7 @@ export function UserPicker(props: Props) {
const [selected, setSelected] = useState<string[]>([]); const [selected, setSelected] = useState<string[]>([]);
const omit = [...(props.omit || []), "00000000000000000000000000"]; const omit = [...(props.omit || []), "00000000000000000000000000"];
const users = useUsers(); const client = useClient();
return ( return (
<Modal <Modal
...@@ -33,24 +33,18 @@ export function UserPicker(props: Props) { ...@@ -33,24 +33,18 @@ export function UserPicker(props: Props) {
}, },
]}> ]}>
<div className={styles.list}> <div className={styles.list}>
{( {[...client.users.values()]
users.filter( .filter(
(x) => (x) =>
x && x &&
x.relationship === Users.Relationship.Friend && x.relationship === RelationshipStatus.Friend &&
!omit.includes(x._id), !omit.includes(x._id),
) as User[] )
)
.map((x) => {
return {
...x,
selected: selected.includes(x._id),
};
})
.map((x) => ( .map((x) => (
<UserCheckbox <UserCheckbox
key={x._id}
user={x} user={x}
checked={x.selected} checked={selected.includes(x._id)}
onChange={(v) => { onChange={(v) => {
if (v) { if (v) {
setSelected([...selected, x._id]); setSelected([...selected, x._id]);
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.header { .header {
background-size: cover; background-size: cover;
border-radius: 8px 8px 0 0; border-radius: var(--border-radius) var(--border-radius) 0 0;
background-position: center; background-position: center;
background-color: var(--secondary-background); background-color: var(--secondary-background);
...@@ -57,13 +57,13 @@ ...@@ -57,13 +57,13 @@
gap: 8px; gap: 8px;
display: flex; display: flex;
padding: 0 1.5em; padding: 0 1.5em;
font-size: .875rem; font-size: 0.875rem;
> div { > div {
padding: 8px; padding: 8px;
cursor: pointer; cursor: pointer;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
transition: border-bottom .3s; transition: border-bottom 0.3s;
&[data-active="true"] { &[data-active="true"] {
border-bottom: 2px solid var(--foreground); border-bottom: 2px solid var(--foreground);
...@@ -81,7 +81,10 @@ ...@@ -81,7 +81,10 @@
height: 100%; height: 100%;
display: flex; display: flex;
padding: 1em 1.5em; padding: 1em 1.5em;
max-width: 560px; max-width: 560px;
max-height: 240px;
overflow-y: auto; overflow-y: auto;
flex-direction: column; flex-direction: column;
background: var(--primary-background); background: var(--primary-background);
...@@ -140,11 +143,11 @@ ...@@ -140,11 +143,11 @@
padding: 12px; padding: 12px;
display: flex; display: flex;
cursor: pointer; cursor: pointer;
border-radius: 4px;
align-items: center; align-items: center;
transition: background-color 0.1s;
color: var(--secondary-foreground); color: var(--secondary-foreground);
border-radius: var(--border-radius);
background-color: var(--secondary-background); background-color: var(--secondary-background);
transition: background-color .1s;
&:hover { &:hover {
background-color: var(--primary-background); background-color: var(--primary-background);
......
import { Money } from "@styled-icons/boxicons-regular"; import { Money } from "@styled-icons/boxicons-regular";
import { Envelope, Edit, UserPlus, Shield } from "@styled-icons/boxicons-solid"; import { Envelope, Edit, UserPlus, Shield } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { Users } from "revolt.js/dist/api/objects"; import { Profile, RelationshipStatus } from "revolt-api/types/Users";
import { UserPermission } from "revolt.js/dist/api/permissions"; import { UserPermission } from "revolt.js/dist/api/permissions";
import { Route } from "revolt.js/dist/api/routes"; import { Route } from "revolt.js/dist/api/routes";
import { decodeTime } from "ulid";
import styles from "./UserProfile.module.scss"; import styles from "./UserProfile.module.scss";
import { Localizer, Text } from "preact-i18n"; import { Localizer, Text } from "preact-i18n";
...@@ -20,23 +20,17 @@ import Preloader from "../../../components/ui/Preloader"; ...@@ -20,23 +20,17 @@ import Preloader from "../../../components/ui/Preloader";
import Markdown from "../../../components/markdown/Markdown"; import Markdown from "../../../components/markdown/Markdown";
import { import {
AppContext,
ClientStatus, ClientStatus,
StatusContext, StatusContext,
useClient,
} from "../../revoltjs/RevoltClient"; } from "../../revoltjs/RevoltClient";
import {
useChannels,
useForceUpdate,
useUserPermission,
useUsers,
} from "../../revoltjs/hooks";
import { useIntermediate } from "../Intermediate"; import { useIntermediate } from "../Intermediate";
interface Props { interface Props {
user_id: string; user_id: string;
dummy?: boolean; dummy?: boolean;
onClose: () => void; onClose?: () => void;
dummyProfile?: Users.Profile; dummyProfile?: Profile;
} }
enum Badges { enum Badges {
...@@ -47,311 +41,313 @@ enum Badges { ...@@ -47,311 +41,313 @@ enum Badges {
EarlyAdopter = 256, EarlyAdopter = 256,
} }
export function UserProfile({ user_id, onClose, dummy, dummyProfile }: Props) { export const UserProfile = observer(
const { openScreen, writeClipboard } = useIntermediate(); ({ user_id, onClose, dummy, dummyProfile }: Props) => {
const { openScreen, writeClipboard } = useIntermediate();
const [profile, setProfile] = useState<undefined | null | Users.Profile>(
undefined,
);
const [mutual, setMutual] = useState<
undefined | null | Route<"GET", "/users/id/mutual">["response"]
>(undefined);
const history = useHistory(); const [profile, setProfile] = useState<undefined | null | Profile>(
const client = useContext(AppContext); undefined,
const status = useContext(StatusContext); );
const [tab, setTab] = useState("profile"); const [mutual, setMutual] = useState<
undefined | null | Route<"GET", "/users/id/mutual">["response"]
>(undefined);
const ctx = useForceUpdate(); const history = useHistory();
const all_users = useUsers(undefined, ctx); const client = useClient();
const channels = useChannels(undefined, ctx); const status = useContext(StatusContext);
const [tab, setTab] = useState("profile");
const user = all_users.find((x) => x!._id === user_id); const user = client.users.get(user_id);
const users = mutual?.users if (!user) {
? all_users.filter((x) => mutual.users.includes(x!._id)) if (onClose) useEffect(onClose, []);
: undefined; return null;
}
if (!user) {
useEffect(onClose, []);
return null;
}
const permissions = useUserPermission(user!._id, ctx); const users = mutual?.users.map((id) => client.users.get(id));
useLayoutEffect(() => { const mutualGroups = [...client.channels.values()].filter(
if (!user_id) return; (channel) =>
if (typeof profile !== "undefined") setProfile(undefined); channel?.channel_type === "Group" &&
if (typeof mutual !== "undefined") setMutual(undefined); channel.recipient_ids!.includes(user_id),
}, [user_id]); );
if (dummy) {
useLayoutEffect(() => { useLayoutEffect(() => {
setProfile(dummyProfile); if (!user_id) return;
}, [dummyProfile]); if (typeof profile !== "undefined") setProfile(undefined);
} if (typeof mutual !== "undefined") setMutual(undefined);
// eslint-disable-next-line
}, [user_id]);
useEffect(() => { useEffect(() => {
if (dummy) return; if (dummy) {
if (status === ClientStatus.ONLINE && typeof mutual === "undefined") { setProfile(dummyProfile);
setMutual(null); }
client.users.fetchMutual(user_id).then((data) => setMutual(data)); }, [dummy, dummyProfile]);
}
}, [mutual, status]);
useEffect(() => {
if (dummy) return;
if (status === ClientStatus.ONLINE && typeof profile === "undefined") {
setProfile(null);
if (permissions & UserPermission.ViewProfile) { useEffect(() => {
client.users if (dummy) return;
.fetchProfile(user_id) if (
.then((data) => setProfile(data)) status === ClientStatus.ONLINE &&
.catch(() => {}); typeof mutual === "undefined"
) {
setMutual(null);
user.fetchMutual().then(setMutual);
} }
} }, [mutual, status, dummy, user]);
}, [profile, status]);
const mutualGroups = channels.filter( useEffect(() => {
(channel) => if (dummy) return;
channel?.channel_type === "Group" && if (
channel.recipients.includes(user_id), status === ClientStatus.ONLINE &&
); typeof profile === "undefined"
) {
setProfile(null);
const backgroundURL = if (user.permission & UserPermission.ViewProfile) {
profile && user.fetchProfile().then(setProfile);
client.users.getBackgroundURL(profile, { width: 1000 }, true); }
const badges = }
(user.badges ?? 0) | }, [profile, status, dummy, user]);
(decodeTime(user._id) < 1623751765790 ? Badges.EarlyAdopter : 0);
return ( const backgroundURL =
<Modal profile &&
visible client.generateFileURL(profile.background, { width: 1000 }, true);
border={dummy} const badges = user.badges ?? 0;
padding={false}
onClose={onClose} return (
dontModal={dummy}> <Modal
<div visible
className={styles.header} border={dummy}
data-force={profile?.background ? "light" : undefined} padding={false}
style={{ onClose={onClose}
backgroundImage: dontModal={dummy}>
backgroundURL && <div
`linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`, className={styles.header}
}}> data-force={profile?.background ? "light" : undefined}
<div className={styles.profile}> style={{
<UserIcon size={80} target={user} status /> backgroundImage:
<div className={styles.details}> backgroundURL &&
<Localizer> `linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url('${backgroundURL}')`,
<span }}>
className={styles.username} <div className={styles.profile}>
onClick={() => writeClipboard(user.username)}> <UserIcon size={80} target={user} status animate />
@{user.username} <div className={styles.details}>
</span> <Localizer>
</Localizer> <span
{user.status?.text && ( className={styles.username}
<span className={styles.status}> onClick={() =>
<UserStatus user={user} tooltip /> writeClipboard(user.username)
</span> }>
)} @{user.username}
</div> </span>
{user.relationship === Users.Relationship.Friend && ( </Localizer>
<Localizer> {user.status?.text && (
<Tooltip <span className={styles.status}>
content={ <UserStatus user={user} tooltip />
<Text id="app.context_menu.message_user" /> </span>
}> )}
<IconButton </div>
onClick={() => { {user.relationship === RelationshipStatus.Friend && (
onClose(); <Localizer>
history.push(`/open/${user_id}`); <Tooltip
}}> content={
<Envelope size={30} /> <Text id="app.context_menu.message_user" />
</IconButton> }>
</Tooltip> <IconButton
</Localizer> onClick={() => {
)} onClose?.();
{user.relationship === Users.Relationship.User && ( history.push(`/open/${user_id}`);
<IconButton }}>
onClick={() => { <Envelope size={30} />
onClose(); </IconButton>
if (dummy) return; </Tooltip>
history.push(`/settings/profile`); </Localizer>
}}>
<Edit size={28} />
</IconButton>
)}
{(user.relationship === Users.Relationship.Incoming ||
user.relationship === Users.Relationship.None) && (
<IconButton
onClick={() =>
client.users.addFriend(user.username)
}>
<UserPlus size={28} />
</IconButton>
)}
</div>
<div className={styles.tabs}>
<div
data-active={tab === "profile"}
onClick={() => setTab("profile")}>
<Text id="app.special.popovers.user_profile.profile" />
</div>
{user.relationship !== Users.Relationship.User && (
<>
<div
data-active={tab === "friends"}
onClick={() => setTab("friends")}>
<Text id="app.special.popovers.user_profile.mutual_friends" />
</div>
<div
data-active={tab === "groups"}
onClick={() => setTab("groups")}>
<Text id="app.special.popovers.user_profile.mutual_groups" />
</div>
</>
)}
</div>
</div>
<div className={styles.content}>
{tab === "profile" && (
<div>
{!(profile?.content || badges > 0) && (
<div className={styles.empty}>
<Text id="app.special.popovers.user_profile.empty" />
</div>
)} )}
{badges > 0 && ( {user.relationship === RelationshipStatus.User && (
<div className={styles.category}> <IconButton
<Text id="app.special.popovers.user_profile.sub.badges" /> onClick={() => {
</div> onClose?.();
if (dummy) return;
history.push(`/settings/profile`);
}}>
<Edit size={28} />
</IconButton>
)} )}
{badges > 0 && ( {(user.relationship === RelationshipStatus.Incoming ||
<div className={styles.badges}> user.relationship === RelationshipStatus.None) && (
<Localizer> <IconButton onClick={() => user.addFriend()}>
{badges & Badges.Developer ? ( <UserPlus size={28} />
<Tooltip </IconButton>
content={
<Text id="app.navigation.tabs.dev" />
}>
<img src="/assets/badges/developer.svg" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.Translator ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.translator" />
}>
<img src="/assets/badges/translator.svg" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.EarlyAdopter ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.early_adopter" />
}>
<img src="/assets/badges/early_adopter.svg" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.Supporter ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.supporter" />
}>
<Money size={32} color="#efab44" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.ResponsibleDisclosure ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
}>
<Shield size={32} color="gray" />
</Tooltip>
) : (
<></>
)}
</Localizer>
</div>
)} )}
{profile?.content && ( </div>
<div className={styles.category}> <div className={styles.tabs}>
<Text id="app.special.popovers.user_profile.sub.information" /> <div
</div> data-active={tab === "profile"}
onClick={() => setTab("profile")}>
<Text id="app.special.popovers.user_profile.profile" />
</div>
{user.relationship !== RelationshipStatus.User && (
<>
<div
data-active={tab === "friends"}
onClick={() => setTab("friends")}>
<Text id="app.special.popovers.user_profile.mutual_friends" />
</div>
<div
data-active={tab === "groups"}
onClick={() => setTab("groups")}>
<Text id="app.special.popovers.user_profile.mutual_groups" />
</div>
</>
)} )}
<Markdown content={profile?.content} />
{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
</div> </div>
)} </div>
{tab === "friends" && <div className={styles.content}>
(users ? ( {tab === "profile" && (
<div>
{!(profile?.content || badges > 0) && (
<div className={styles.empty}>
<Text id="app.special.popovers.user_profile.empty" />
</div>
)}
{badges > 0 && (
<div className={styles.category}>
<Text id="app.special.popovers.user_profile.sub.badges" />
</div>
)}
{badges > 0 && (
<div className={styles.badges}>
<Localizer>
{badges & Badges.Developer ? (
<Tooltip
content={
<Text id="app.navigation.tabs.dev" />
}>
<img src="/assets/badges/developer.svg" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.Translator ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.translator" />
}>
<img src="/assets/badges/translator.svg" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.EarlyAdopter ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.early_adopter" />
}>
<img src="/assets/badges/early_adopter.svg" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.Supporter ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.supporter" />
}>
<Money
size={32}
color="#efab44"
/>
</Tooltip>
) : (
<></>
)}
{badges &
Badges.ResponsibleDisclosure ? (
<Tooltip
content={
<Text id="app.special.popovers.user_profile.badges.responsible_disclosure" />
}>
<Shield
size={32}
color="gray"
/>
</Tooltip>
) : (
<></>
)}
</Localizer>
</div>
)}
{profile?.content && (
<div className={styles.category}>
<Text id="app.special.popovers.user_profile.sub.information" />
</div>
)}
<Markdown content={profile?.content} />
{/*<div className={styles.category}><Text id="app.special.popovers.user_profile.sub.connections" /></div>*/}
</div>
)}
{tab === "friends" &&
(users ? (
<div className={styles.entries}>
{users.length === 0 ? (
<div className={styles.empty}>
<Text id="app.special.popovers.user_profile.no_users" />
</div>
) : (
users.map(
(x) =>
x && (
<div
onClick={() =>
openScreen({
id: "profile",
user_id: x._id,
})
}
className={styles.entry}
key={x._id}>
<UserIcon
size={32}
target={x}
/>
<span>{x.username}</span>
</div>
),
)
)}
</div>
) : (
<Preloader type="ring" />
))}
{tab === "groups" && (
<div className={styles.entries}> <div className={styles.entries}>
{users.length === 0 ? ( {mutualGroups.length === 0 ? (
<div className={styles.empty}> <div className={styles.empty}>
<Text id="app.special.popovers.user_profile.no_users" /> <Text id="app.special.popovers.user_profile.no_groups" />
</div> </div>
) : ( ) : (
users.map( mutualGroups.map(
(x) => (x) =>
x && ( x?.channel_type === "Group" && (
<div <Link to={`/channel/${x._id}`}>
onClick={() => <div
openScreen({ className={styles.entry}
id: "profile", key={x._id}>
user_id: x._id, <ChannelIcon
}) target={x}
} size={32}
className={styles.entry} />
key={x._id}> <span>{x.name}</span>
<UserIcon </div>
size={32} </Link>
target={x}
/>
<span>{x.username}</span>
</div>
), ),
) )
)} )}
</div> </div>
) : ( )}
<Preloader type="ring" /> </div>
))} </Modal>
{tab === "groups" && ( );
<div className={styles.entries}> },
{mutualGroups.length === 0 ? ( );
<div className={styles.empty}>
<Text id="app.special.popovers.user_profile.no_groups" />
</div>
) : (
mutualGroups.map(
(x) =>
x?.channel_type === "Group" && (
<Link to={`/channel/${x._id}`}>
<div
className={styles.entry}
key={x._id}>
<ChannelIcon
target={x}
size={32}
/>
<span>{x.name}</span>
</div>
</Link>
),
)
)}
</div>
)}
</div>
</Modal>
);
}
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
&.banner { &.banner {
.image { .image {
border-radius: 4px; border-radius: var(--border-radius);
} }
.modify { .modify {
......
import { Plus, X, XCircle } from "@styled-icons/boxicons-regular"; import { Plus } from "@styled-icons/boxicons-regular";
import { Pencil } from "@styled-icons/boxicons-solid"; import { Pencil } from "@styled-icons/boxicons-solid";
import Axios, { AxiosRequestConfig } from "axios"; import Axios, { AxiosRequestConfig } from "axios";
...@@ -55,7 +55,7 @@ export async function uploadFile( ...@@ -55,7 +55,7 @@ export async function uploadFile(
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
const res = await Axios.post(autumnURL + "/" + tag, formData, { const res = await Axios.post(`${autumnURL}/${tag}`, formData, {
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
...@@ -78,7 +78,7 @@ export function grabFiles( ...@@ -78,7 +78,7 @@ export function grabFiles(
input.onchange = async (e) => { input.onchange = async (e) => {
const files = (e.currentTarget as HTMLInputElement)?.files; const files = (e.currentTarget as HTMLInputElement)?.files;
if (!files) return; if (!files) return;
for (let file of files) { for (const file of files) {
if (file.size > maxFileSize) { if (file.size > maxFileSize) {
return tooLarge(); return tooLarge();
} }
...@@ -139,16 +139,15 @@ export function FileUploader(props: Props) { ...@@ -139,16 +139,15 @@ export function FileUploader(props: Props) {
} else { } else {
onClick(); onClick();
} }
} else if (props.previewURL) {
props.remove();
} else { } else {
if (props.previewURL) { onClick();
props.remove();
} else {
onClick();
}
} }
} }
if (props.behaviour === "multi" && props.append) { if (props.behaviour === "multi" && props.append) {
// eslint-disable-next-line
useEffect(() => { useEffect(() => {
// File pasting. // File pasting.
function paste(e: ClipboardEvent) { function paste(e: ClipboardEvent) {
...@@ -156,7 +155,7 @@ export function FileUploader(props: Props) { ...@@ -156,7 +155,7 @@ export function FileUploader(props: Props) {
if (typeof items === "undefined") return; if (typeof items === "undefined") return;
if (props.behaviour !== "multi" || !props.append) return; if (props.behaviour !== "multi" || !props.append) return;
let files = []; const files = [];
for (const item of items) { for (const item of items) {
if (!item.type.startsWith("text/")) { if (!item.type.startsWith("text/")) {
const blob = item.getAsFile(); const blob = item.getAsFile();
...@@ -190,7 +189,7 @@ export function FileUploader(props: Props) { ...@@ -190,7 +189,7 @@ export function FileUploader(props: Props) {
const dropped = e.dataTransfer?.files; const dropped = e.dataTransfer?.files;
if (dropped) { if (dropped) {
let files = []; const files = [];
for (const item of dropped) { for (const item of dropped) {
if (item.size > props.maxFileSize) { if (item.size > props.maxFileSize) {
openScreen({ id: "error", error: "FileTooLarge" }); openScreen({ id: "error", error: "FileTooLarge" });
...@@ -212,7 +211,7 @@ export function FileUploader(props: Props) { ...@@ -212,7 +211,7 @@ export function FileUploader(props: Props) {
document.removeEventListener("dragover", dragover); document.removeEventListener("dragover", dragover);
document.removeEventListener("drop", drop); document.removeEventListener("drop", drop);
}; };
}, [props.append]); }, [openScreen, props, props.append]);
} }
if (props.style === "icon" || props.style === "banner") { if (props.style === "icon" || props.style === "banner") {
......
import { Route, Switch, useHistory, useParams } from "react-router-dom"; import { Route, Switch, useHistory, useParams } from "react-router-dom";
import { Message, SYSTEM_USER_ID, User } from "revolt.js"; import { Presence, RelationshipStatus } from "revolt-api/types/Users";
import { Users } from "revolt.js/dist/api/objects"; import { SYSTEM_USER_ID } from "revolt.js";
import { Message } from "revolt.js/dist/maps/Messages";
import { User } from "revolt.js/dist/maps/Users";
import { decodeTime } from "ulid"; import { decodeTime } from "ulid";
import { useContext, useEffect } from "preact/hooks"; import { useCallback, useContext, useEffect } from "preact/hooks";
import { useTranslation } from "../../lib/i18n"; import { useTranslation } from "../../lib/i18n";
...@@ -32,7 +34,7 @@ async function createNotification( ...@@ -32,7 +34,7 @@ async function createNotification(
try { try {
return new Notification(title, options); return new Notification(title, options);
} catch (err) { } catch (err) {
let sw = await navigator.serviceWorker.getRegistration(); const sw = await navigator.serviceWorker.getRegistration();
sw?.showNotification(title, options); sw?.showNotification(title, options);
} }
} }
...@@ -49,195 +51,225 @@ function Notifier({ options, notifs }: Props) { ...@@ -49,195 +51,225 @@ function Notifier({ options, notifs }: Props) {
const history = useHistory(); const history = useHistory();
const playSound = useContext(SoundContext); const playSound = useContext(SoundContext);
async function message(msg: Message) { const message = useCallback(
if (msg.author === client.user!._id) return; async (msg: Message) => {
if (msg.channel === channel_id && document.hasFocus()) return; if (msg.author_id === client.user!._id) return;
if (client.user!.status?.presence === Users.Presence.Busy) return; if (msg.channel_id === channel_id && document.hasFocus()) return;
if (client.user!.status?.presence === Presence.Busy) return;
if (msg.author?.relationship === RelationshipStatus.Blocked) return;
const channel = client.channels.get(msg.channel); const notifState = getNotificationState(notifs, msg.channel!);
const author = client.users.get(msg.author); if (!shouldNotify(notifState, msg, client.user!._id)) return;
if (!channel) return;
if (author?.relationship === Users.Relationship.Blocked) return;
const notifState = getNotificationState(notifs, channel); playSound("message");
if (!shouldNotify(notifState, msg, client.user!._id)) return; if (!showNotification) return;
playSound("message"); let title;
if (!showNotification) return; switch (msg.channel?.channel_type) {
case "SavedMessages":
let title; return;
switch (channel.channel_type) { case "DirectMessage":
case "SavedMessages": title = `@${msg.author?.username}`;
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) {
case "user_added":
case "user_remove":
body = translate(
`app.main.channel.system.${
msg.content.type === "user_added"
? "added_by"
: "removed_by"
}`,
{
user: users.get(msg.content.id)?.username,
other_user: users.get(msg.content.by)?.username,
},
);
icon = client.users.getAvatarURL(msg.content.id, {
max_side: 256,
});
break; break;
case "user_joined": case "Group":
case "user_left": if (msg.author?._id === SYSTEM_USER_ID) {
case "user_kicked": title = msg.channel.name;
case "user_banned": } else {
body = translate( title = `@${msg.author?.username} - ${msg.channel.name}`;
`app.main.channel.system.${msg.content.type}`, }
{ user: users.get(msg.content.id)?.username },
);
icon = client.users.getAvatarURL(msg.content.id, {
max_side: 256,
});
break; break;
case "channel_renamed": case "TextChannel":
body = translate( title = `@${msg.author?.username} (#${msg.channel.name}, ${msg.channel.server?.name})`;
`app.main.channel.system.channel_renamed`,
{
user: users.get(msg.content.by)?.username,
name: msg.content.name,
},
);
icon = client.users.getAvatarURL(msg.content.by, {
max_side: 256,
});
break; break;
case "channel_description_changed": default:
case "channel_icon_changed": title = msg.channel?._id;
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: users.get(msg.content.by)?.username },
);
icon = client.users.getAvatarURL(msg.content.by, {
max_side: 256,
});
break; break;
} }
}
let notif = await createNotification(title, { let image;
icon, if (msg.attachments) {
image, const imageAttachment = msg.attachments.find(
body, (x) => x.metadata.type === "Image",
timestamp: decodeTime(msg._id), );
tag: msg.channel, if (imageAttachment) {
badge: "/assets/icons/android-chrome-512x512.png", image = client.generateFileURL(imageAttachment, {
silent: true, max_side: 720,
}); });
}
}
if (notif) { let body, icon;
notif.addEventListener("click", () => { if (typeof msg.content === "string") {
window.focus(); body = client.markdownToText(msg.content);
const id = msg.channel; icon = msg.author?.generateAvatarURL({ max_side: 256 });
if (id !== channel_id) { } else {
let channel = client.channels.get(id); const users = client.users;
if (channel) { switch (msg.content.type) {
if (channel.channel_type === "TextChannel") { case "user_added":
history.push( case "user_remove":
`/server/${channel.server}/channel/${id}`, {
const user = users.get(msg.content.id);
body = translate(
`app.main.channel.system.${
msg.content.type === "user_added"
? "added_by"
: "removed_by"
}`,
{
user: user?.username,
other_user: users.get(msg.content.by)
?.username,
},
); );
} else { icon = user?.generateAvatarURL({
history.push(`/channel/${id}`); max_side: 256,
});
} }
} break;
case "user_joined":
case "user_left":
case "user_kicked":
case "user_banned":
{
const user = users.get(msg.content.id);
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: user?.username },
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "channel_renamed":
{
const user = users.get(msg.content.by);
body = translate(
`app.main.channel.system.channel_renamed`,
{
user: users.get(msg.content.by)?.username,
name: msg.content.name,
},
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
case "channel_description_changed":
case "channel_icon_changed":
{
const user = users.get(msg.content.by);
body = translate(
`app.main.channel.system.${msg.content.type}`,
{ user: users.get(msg.content.by)?.username },
);
icon = user?.generateAvatarURL({
max_side: 256,
});
}
break;
} }
}
const notif = await createNotification(title!, {
icon,
image,
body,
timestamp: decodeTime(msg._id),
tag: msg.channel?._id,
badge: "/assets/icons/android-chrome-512x512.png",
silent: true,
}); });
notifications[msg.channel] = notif; if (notif) {
notif.addEventListener( notif.addEventListener("click", () => {
"close", window.focus();
() => delete notifications[msg.channel], const id = msg.channel_id;
); if (id !== channel_id) {
} const channel = client.channels.get(id);
} if (channel) {
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server_id}/channel/${id}`,
);
} else {
history.push(`/channel/${id}`);
}
}
}
});
notifications[msg.channel_id] = notif;
notif.addEventListener(
"close",
() => delete notifications[msg.channel_id],
);
}
},
[
history,
showNotification,
translate,
channel_id,
client,
notifs,
playSound,
],
);
async function relationship(user: User, property: string) { const relationship = useCallback(
if (client.user?.status?.presence === Users.Presence.Busy) return; async (user: User) => {
if (property !== "relationship") return; if (client.user?.status?.presence === Presence.Busy) return;
if (!showNotification) return; if (!showNotification) return;
let event; let event;
switch (user.relationship) { switch (user.relationship) {
case Users.Relationship.Incoming: case RelationshipStatus.Incoming:
event = translate("notifications.sent_request", { event = translate("notifications.sent_request", {
person: user.username, person: user.username,
}); });
break; break;
case Users.Relationship.Friend: case RelationshipStatus.Friend:
event = translate("notifications.now_friends", { event = translate("notifications.now_friends", {
person: user.username, person: user.username,
}); });
break; break;
default: default:
return; return;
} }
let notif = await createNotification(event, { const notif = await createNotification(event, {
icon: client.users.getAvatarURL(user._id, { max_side: 256 }), icon: user.generateAvatarURL({ max_side: 256 }),
badge: "/assets/icons/android-chrome-512x512.png", badge: "/assets/icons/android-chrome-512x512.png",
timestamp: +new Date(), timestamp: +new Date(),
}); });
notif?.addEventListener("click", () => { notif?.addEventListener("click", () => {
history.push(`/friends`); history.push(`/friends`);
}); });
} },
[client.user?.status?.presence, history, showNotification, translate],
);
useEffect(() => { useEffect(() => {
client.addListener("message", message); client.addListener("message", message);
client.users.addListener("mutation", relationship); client.addListener("user/relationship", relationship);
return () => { return () => {
client.removeListener("message", message); client.removeListener("message", message);
client.users.removeListener("mutation", relationship); client.removeListener("user/relationship", relationship);
}; };
}, [client, playSound, guild_id, channel_id, showNotification, notifs]); }, [
client,
playSound,
guild_id,
channel_id,
showNotification,
notifs,
message,
relationship,
]);
useEffect(() => { useEffect(() => {
function visChange() { function visChange() {
......