Merge branch 'master' into 'cleanup'

# Conflicts:
#   src/components/common/LocaleSelector.tsx
This commit is contained in:
insert
2021-07-05 10:17:57 +00:00
28 changed files with 322 additions and 208 deletions

View File

@@ -1,9 +1,9 @@
import ComboBox from "../ui/ComboBox";
import { dispatch } from "../../redux";
import { connectState } from "../../redux/connector";
import { WithDispatcher } from "../../redux/reducers";
import { Language, LanguageEntry, Languages } from "../../context/Locale";
type Props = WithDispatcher & {
type Props = {
locale: string;
};
@@ -12,8 +12,7 @@ export function LocaleSelector(props: Props) {
<ComboBox
value={props.locale}
onChange={e =>
props.dispatcher &&
props.dispatcher({
dispatch({
type: "SET_LOCALE",
locale: e.currentTarget.value as Language
})
@@ -37,6 +36,5 @@ export default connectState(
return {
locale: state.locale
};
},
true
}
);

View File

@@ -1,11 +1,11 @@
import { ulid } from "ulid";
import { Text } from "preact-i18n";
import Tooltip, { PermissionTooltip } from "../Tooltip";
import { Channel } from "revolt.js";
import styled from "styled-components";
import { dispatch } from "../../../redux";
import { defer } from "../../../lib/defer";
import IconButton from "../../ui/IconButton";
import { X } from '@styled-icons/boxicons-regular';
import { PermissionTooltip } from "../Tooltip";
import { Send } from '@styled-icons/boxicons-solid';
import { debounce } from "../../../lib/debounce";
import Axios, { CancelTokenSource } from "axios";
@@ -13,7 +13,6 @@ import { useTranslation } from "../../../lib/i18n";
import { Reply } from "../../../redux/reducers/queue";
import { connectState } from "../../../redux/connector";
import { SoundContext } from "../../../context/Settings";
import { WithDispatcher } from "../../../redux/reducers";
import { takeError } from "../../../context/revoltjs/util";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import AutoComplete, { useAutoComplete } from "../AutoComplete";
@@ -30,9 +29,8 @@ import { ShieldX } from "@styled-icons/boxicons-regular";
import ReplyBar from "./bars/ReplyBar";
import FilePreview from './bars/FilePreview';
import { Styleshare } from "@styled-icons/simple-icons";
type Props = WithDispatcher & {
type Props = {
channel: Channel;
draft?: string;
};
@@ -77,7 +75,7 @@ const Action = styled.div`
// ! FIXME: add to app config and load from app config
export const CAN_UPLOAD_AT_ONCE = 5;
function MessageBox({ channel, draft, dispatcher }: Props) {
function MessageBox({ channel, draft }: Props) {
const [ uploadState, setUploadState ] = useState<UploadState>({ type: 'none' });
const [ typing, setTyping ] = useState<boolean | number>(false);
const [ replies, setReplies ] = useState<Reply[]>([]);
@@ -102,13 +100,13 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
function setMessage(content?: string) {
if (content) {
dispatcher({
dispatch({
type: "SET_DRAFT",
channel: channel._id,
content
});
} else {
dispatcher({
dispatch({
type: "CLEAR_DRAFT",
channel: channel._id
});
@@ -148,7 +146,7 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
playSound('outbound');
const nonce = ulid();
dispatcher({
dispatch({
type: "QUEUE_ADD",
nonce,
channel: channel._id,
@@ -171,7 +169,7 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
replies
});
} catch (error) {
dispatcher({
dispatch({
type: "QUEUE_FAIL",
error: takeError(error),
nonce
@@ -383,7 +381,7 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
)
}
export default connectState<Omit<Props, "dispatcher" | "draft">>(MessageBox, (state, { channel }) => {
export default connectState<Omit<Props, "dispatch" | "draft">>(MessageBox, (state, { channel }) => {
return {
draft: state.drafts[channel._id]
}

View File

@@ -1,14 +1,13 @@
.attachment {
display: grid;
grid-auto-columns: min(100%, 480px);
grid-auto-flow: row dense;
width: max-content;
border-radius: 6px;
margin: .125rem 0 .125rem;
height: auto;
max-height: 640px;
max-width: min(480px, 100%);
object-fit: contain;
&[data-spoiler="true"] {
filter: blur(30px);
pointer-events: none;
@@ -20,6 +19,16 @@
&.image {
cursor: pointer;
max-height: 640px;
max-width: min(480px, 100%);
object-fit: contain;
&.loaded {
width: auto;
height: auto;
}
}
&.video {
@@ -29,8 +38,15 @@
}
video {
width: 100%;
border-radius: 0 0 6px 6px;
max-height: 640px;
max-width: min(480px, 100%);
}
video.loaded {
width: auto;
height: auto;
}
}
@@ -59,11 +75,12 @@
}
&.text {
display: flex;
overflow: hidden;
width: 100%;
max-width: 800px;
overflow: hidden;
grid-auto-columns: unset;
border-radius: 6px;
flex-direction: column;
.textContent {
height: 140px;
@@ -92,35 +109,48 @@
}
}
.actions.imageAction {
grid-template:
"name icon download" auto
"size icon download" auto
/ minmax(20px, 1fr) min-content min-content;
}
.actions {
gap: 8px;
padding: 8px;
display: flex;
overflow: none;
max-width: 100%;
display: grid;
grid-template:
"icon name download" auto
"icon size download" auto
/ min-content minmax(20px, 1fr) min-content;
align-items: center;
flex-direction: row;
column-gap: 8px;
width: 100%;
padding: 8px;
overflow: none;
color: var(--foreground);
background: var(--secondary-background);
> svg {
flex-shrink: 0;
span {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
.filesize {
grid-area: size;
> span {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
font-size: 10px;
color: var(--secondary-foreground);
}
.filesize {
font-size: 10px;
color: var(--secondary-foreground);
}
.downloadIcon {
grid-area: download;
}
.iconType {
grid-area: icon;
}
}

View File

@@ -21,6 +21,7 @@ export default function Attachment({ attachment, hasContent }: Props) {
const { openScreen } = useIntermediate();
const { filename, metadata } = attachment;
const [ spoiler, setSpoiler ] = useState(filename.startsWith("SPOILER_"));
const [ loaded, setLoaded ] = useState(false)
const url = client.generateFileURL(attachment, { width: MAX_ATTACHMENT_WIDTH * 1.5 }, true);
@@ -44,7 +45,7 @@ export default function Attachment({ attachment, hasContent }: Props) {
height={metadata.height}
data-spoiler={spoiler}
data-has-content={hasContent}
className={classNames(styles.attachment, styles.image)}
className={classNames(styles.attachment, styles.image, loaded && styles.loaded)}
onClick={() =>
openScreen({ id: "image_viewer", attachment })
}
@@ -52,6 +53,7 @@ export default function Attachment({ attachment, hasContent }: Props) {
ev.button === 1 &&
window.open(url, "_blank")
}
onLoad={() => setLoaded(true)}
/>
</div>
);
@@ -85,11 +87,15 @@ export default function Attachment({ attachment, hasContent }: Props) {
<AttachmentActions attachment={attachment} />
<video
src={url}
width={metadata.width}
height={metadata.height}
className={classNames(loaded && styles.loaded)}
controls
onMouseDown={ev =>
ev.button === 1 &&
window.open(url, "_blank")
}
onLoadedMetadata={() => setLoaded(true)}
/>
</div>
</div>

View File

@@ -5,6 +5,7 @@ import { Attachment } from "revolt.js/dist/api/objects";
import { determineFileSize } from '../../../../lib/fileSize';
import { AppContext } from '../../../../context/revoltjs/RevoltClient';
import { Download, LinkExternal, File, Headphone, Video } from '@styled-icons/boxicons-regular';
import classNames from 'classnames';
interface Props {
attachment: Attachment;
@@ -24,17 +25,15 @@ export default function AttachmentActions({ attachment }: Props) {
switch (metadata.type) {
case 'Image':
return (
<div className={styles.actions}>
<div className={styles.info}>
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{metadata.width + 'x' + metadata.height} ({filesize})</span>
</div>
<a href={open_url} target="_blank">
<div className={classNames(styles.actions, styles.imageAction)}>
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{metadata.width + 'x' + metadata.height} ({filesize})</span>
<a href={open_url} target="_blank" className={styles.iconType} >
<IconButton>
<LinkExternal size={24} />
</IconButton>
</a>
<a href={download_url} download target="_blank">
<a href={download_url} className={styles.downloadIcon} download target="_blank">
<IconButton>
<Download size={24} />
</IconButton>
@@ -43,13 +42,11 @@ export default function AttachmentActions({ attachment }: Props) {
)
case 'Audio':
return (
<div className={styles.actions}>
<Headphone size={24} />
<div className={styles.info}>
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{filesize}</span>
</div>
<a href={download_url} download target="_blank">
<div className={classNames(styles.actions, styles.audioAction)}>
<Headphone size={24} className={styles.iconType} />
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{filesize}</span>
<a href={download_url} className={styles.downloadIcon} download target="_blank">
<IconButton>
<Download size={24} />
</IconButton>
@@ -58,13 +55,11 @@ export default function AttachmentActions({ attachment }: Props) {
)
case 'Video':
return (
<div className={styles.actions}>
<Video size={24} />
<div className={styles.info}>
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{metadata.width + 'x' + metadata.height} ({filesize})</span>
</div>
<a href={download_url} download target="_blank">
<div className={classNames(styles.actions, styles.videoAction)}>
<Video size={24} className={styles.iconType} />
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{metadata.width + 'x' + metadata.height} ({filesize})</span>
<a href={download_url} className={styles.downloadIcon} download target="_blank">
<IconButton>
<Download size={24} />
</IconButton>
@@ -74,12 +69,10 @@ export default function AttachmentActions({ attachment }: Props) {
default:
return (
<div className={styles.actions}>
<File size={24} />
<div className={styles.info}>
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{filesize}</span>
</div>
<a href={download_url} download target="_blank">
<File size={24} className={styles.iconType} />
<span className={styles.filename}>{filename}</span>
<span className={styles.filesize}>{filesize}</span>
<a href={download_url} className={styles.downloadIcon} download target="_blank">
<IconButton>
<Download size={24} />
</IconButton>

View File

@@ -3,12 +3,12 @@ import { useContext, useEffect } from "preact/hooks";
import { Home, UserDetail, Wrench, Notepad } from "@styled-icons/boxicons-solid";
import Category from '../../ui/Category';
import { dispatch } from "../../../redux";
import PaintCounter from "../../../lib/PaintCounter";
import UserHeader from "../../common/user/UserHeader";
import { Channels } from "revolt.js/dist/api/objects";
import { connectState } from "../../../redux/connector";
import ConnectionStatus from '../items/ConnectionStatus';
import { WithDispatcher } from "../../../redux/reducers";
import { Unreads } from "../../../redux/reducers/unreads";
import ConditionalLink from "../../../lib/ConditionalLink";
import { mapChannelWithUnread, useUnreads } from "./common";
@@ -23,7 +23,7 @@ import { useDMs, useForceUpdate, useUsers } from "../../../context/revoltjs/hook
import placeholderSVG from "../items/placeholder.svg";
type Props = WithDispatcher & {
type Props = {
unreads: Unreads;
}
@@ -43,7 +43,7 @@ function HomeSidebar(props: Props) {
useEffect(() => {
if (!channel) return;
props.dispatcher({
dispatch({
type: 'LAST_OPENED_SET',
parent: 'home',
child: channel
@@ -148,6 +148,5 @@ export default connectState(
unreads: state.unreads
};
},
true,
true
);

View File

@@ -2,7 +2,6 @@ import { Redirect, useParams } from "react-router";
import { ChannelButton } from "../items/ButtonItem";
import { Channels } from "revolt.js/dist/api/objects";
import { Unreads } from "../../../redux/reducers/unreads";
import { WithDispatcher } from "../../../redux/reducers";
import { useChannels, useForceUpdate, useServer } from "../../../context/revoltjs/hooks";
import { mapChannelWithUnread, useUnreads } from "./common";
import ConnectionStatus from '../items/ConnectionStatus';
@@ -13,6 +12,7 @@ import { attachContextMenu } from 'preact-context-menu';
import ServerHeader from "../../common/ServerHeader";
import { useEffect } from "preact/hooks";
import Category from "../../ui/Category";
import { dispatch } from "../../../redux";
import ConditionalLink from "../../../lib/ConditionalLink";
import CollapsibleSection from "../../common/CollapsibleSection";
@@ -43,7 +43,7 @@ const ServerList = styled.div`
}
`;
function ServerSidebar(props: Props & WithDispatcher) {
function ServerSidebar(props: Props) {
const { server: server_id, channel: channel_id } = useParams<{ server?: string, channel?: string }>();
const ctx = useForceUpdate();
@@ -61,7 +61,7 @@ function ServerSidebar(props: Props & WithDispatcher) {
useEffect(() => {
if (!channel_id) return;
props.dispatcher({
dispatch({
type: 'LAST_OPENED_SET',
parent: server_id!,
child: channel_id!
@@ -130,6 +130,5 @@ export default connectState(
return {
unreads: state.unreads
};
},
true
}
);

View File

@@ -1,15 +1,15 @@
import { Channel } from "revolt.js";
import { dispatch } from "../../../redux";
import { useLayoutEffect } from "preact/hooks";
import { WithDispatcher } from "../../../redux/reducers";
import { Unreads } from "../../../redux/reducers/unreads";
import { HookContext, useForceUpdate } from "../../../context/revoltjs/hooks";
type UnreadProps = WithDispatcher & {
type UnreadProps = {
channel: Channel;
unreads: Unreads;
}
export function useUnreads({ channel, unreads, dispatcher }: UnreadProps, context?: HookContext) {
export function useUnreads({ channel, unreads }: UnreadProps, context?: HookContext) {
const ctx = useForceUpdate(context);
useLayoutEffect(() => {
@@ -23,7 +23,7 @@ export function useUnreads({ channel, unreads, dispatcher }: UnreadProps, contex
if (target.last_message) {
const message = typeof target.last_message === 'string' ? target.last_message : target.last_message._id;
if (!unread || (unread && message.localeCompare(unread) > 0)) {
dispatcher({
dispatch({
type: "UNREADS_MARK_READ",
channel: channel._id,
message

View File

@@ -2,7 +2,6 @@ import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { User } from "revolt.js";
import Details from "../../../components/ui/Details";
import Category from "../../ui/Category";
import { useParams } from "react-router";
import { UserButton } from "../items/ButtonItem";
@@ -15,6 +14,7 @@ import { AppContext, ClientStatus, StatusContext } from "../../../context/revolt
import { HookContext, useChannel, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
import placeholderSVG from "../items/placeholder.svg";
import Preloader from "../../ui/Preloader";
interface Props {
ctx: HookContext
@@ -126,6 +126,7 @@ export function GroupMemberSidebar({ channel, ctx }: Props & { channel: Channels
export function ServerMemberSidebar({ channel, ctx }: Props & { channel: Channels.TextChannel }) {
const [members, setMembers] = useState<Servers.Member[] | undefined>(undefined);
const users = useUsers(members?.map(x => x._id.user) ?? []).filter(x => typeof x !== 'undefined', ctx) as Users.User[];
const { openScreen } = useIntermediate();
const status = useContext(StatusContext);
const client = useContext(AppContext);
@@ -180,17 +181,16 @@ export function ServerMemberSidebar({ channel, ctx }: Props & { channel: Channel
</span>
}
/>
{users.length === 0 && <img src={placeholderSVG} />}
{!members && <Preloader type="ring" />}
{members && users.length === 0 && <img src={placeholderSVG} />}
{users.map(
user =>
user && (
// <LinkProfile user_id={user._id}>
<UserButton
key={user._id}
user={user}
context={channel}
/>
// </LinkProfile>
<UserButton
key={user._id}
user={user}
context={channel}
onClick={() => openScreen({ id: 'profile', user_id: user._id })} />
)
)}
</GenericSidebarList>

View File

@@ -195,6 +195,9 @@ export const MONOSCAPE_FONTS: Record<MonoscapeFonts, { name: string, load: () =>
export const FONT_KEYS = Object.keys(FONTS).sort();
export const MONOSCAPE_FONT_KEYS = Object.keys(MONOSCAPE_FONTS).sort();
export const DEFAULT_FONT = 'Open Sans';
export const DEFAULT_MONO_FONT = 'Fira Code';
// Generated from https://gitlab.insrt.uk/revolt/community/themes
export const PRESETS: Record<string, Theme> = {
light: {
@@ -281,13 +284,13 @@ function Theme({ children, options }: Props) {
const root = document.documentElement.style;
useEffect(() => {
const font = theme.font ?? 'Inter';
const font = theme.font ?? DEFAULT_FONT;
root.setProperty('--font', `"${font}"`);
FONTS[font].load();
}, [ theme.font ]);
useEffect(() => {
const font = theme.monoscapeFont ?? 'Fira Code';
const font = theme.monoscapeFont ?? DEFAULT_MONO_FONT;
root.setProperty('--monoscape-font', `"${font}"`);
MONOSCAPE_FONTS[font].load();
}, [ theme.monoscapeFont ]);

View File

@@ -2,12 +2,12 @@ import { openDB } from 'idb';
import { Client } from "revolt.js";
import { takeError } from "./util";
import { createContext } from "preact";
import { dispatch } from '../../redux';
import { Children } from "../../types/Preact";
import { useHistory } from 'react-router-dom';
import { Route } from "revolt.js/dist/api/routes";
import { connectState } from "../../redux/connector";
import Preloader from "../../components/ui/Preloader";
import { WithDispatcher } from "../../redux/reducers";
import { AuthState } from "../../redux/reducers/auth";
import { useEffect, useMemo, useState } from "preact/hooks";
import { useIntermediate } from '../intermediate/Intermediate';
@@ -41,12 +41,12 @@ export const AppContext = createContext<Client>(null!);
export const StatusContext = createContext<ClientStatus>(null!);
export const OperationsContext = createContext<ClientOperations>(null!);
type Props = WithDispatcher & {
type Props = {
auth: AuthState;
children: Children;
};
function Context({ auth, children, dispatcher }: Props) {
function Context({ auth, children }: Props) {
const history = useHistory();
const { openScreen } = useIntermediate();
const [status, setStatus] = useState(ClientStatus.INIT);
@@ -94,7 +94,7 @@ function Context({ auth, children, dispatcher }: Props) {
const onboarding = await client.login(data);
setReconnectDisallowed(false);
const login = () =>
dispatcher({
dispatch({
type: "LOGIN",
session: client.session! // This [null assertion] is ok, we should have a session by now. - insert's words
});
@@ -114,10 +114,10 @@ function Context({ auth, children, dispatcher }: Props) {
}
},
logout: async shouldRequest => {
dispatcher({ type: "LOGOUT" });
dispatch({ type: "LOGOUT" });
client.reset();
dispatcher({ type: "RESET" });
dispatch({ type: "RESET" });
openScreen({ id: "none" });
setStatus(ClientStatus.READY);
@@ -145,7 +145,7 @@ function Context({ auth, children, dispatcher }: Props) {
}
}, [ client, auth.active ]);
useEffect(() => registerEvents({ operations, dispatcher }, setStatus, client), [ client ]);
useEffect(() => registerEvents({ operations }, setStatus, client), [ client ]);
useEffect(() => {
(async () => {
@@ -154,7 +154,7 @@ function Context({ auth, children, dispatcher }: Props) {
}
if (auth.active) {
dispatcher({ type: "QUEUE_FAIL_ALL" });
dispatch({ type: "QUEUE_FAIL_ALL" });
const active = auth.accounts[auth.active];
client.user = client.users.get(active.session.user_id);
@@ -226,6 +226,5 @@ export default connectState<{ children: Children }>(
auth: state.auth,
sync: state.sync
};
},
true
}
);

View File

@@ -7,10 +7,10 @@ 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";
import { dispatch } from "../../redux";
type Props = WithDispatcher & {
type Props = {
messages: QueuedMessage[];
typing: Typing
};
@@ -19,7 +19,7 @@ function StateMonitor(props: Props) {
const client = useContext(AppContext);
useEffect(() => {
props.dispatcher({
dispatch({
type: 'QUEUE_DROP_ALL'
});
}, [ ]);
@@ -29,7 +29,7 @@ function StateMonitor(props: Props) {
if (!msg.nonce) return;
if (!props.messages.find(x => x.id === msg.nonce)) return;
props.dispatcher({
dispatch({
type: 'QUEUE_REMOVE',
nonce: msg.nonce
});
@@ -47,7 +47,7 @@ function StateMonitor(props: Props) {
for (let user of users) {
if (+ new Date() > user.started + 5000) {
props.dispatcher({
dispatch({
type: 'TYPING_STOP',
channel,
user: user.id
@@ -73,6 +73,5 @@ export default connectState(
messages: [...state.queue],
typing: state.typing
};
},
true
}
);

View File

@@ -7,14 +7,14 @@ 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 { Notifications } from "../../redux/reducers/notifications";
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";
import { dispatch } from "../../redux";
type Props = WithDispatcher & {
type Props = {
settings: Settings,
locale: Language,
sync: SyncOptions,
@@ -54,7 +54,7 @@ function SyncManager(props: Props) {
client
.syncFetchSettings(DEFAULT_ENABLED_SYNC.filter(x => !props.sync?.disabled?.includes(x)))
.then(data => {
props.dispatcher({
dispatch({
type: 'SYNC_UPDATE',
update: mapSync(data)
});
@@ -62,13 +62,13 @@ function SyncManager(props: Props) {
client
.syncFetchUnreads()
.then(unreads => props.dispatcher({ type: 'UNREADS_SET', unreads }));
.then(unreads => dispatch({ type: 'UNREADS_SET', unreads }));
}
}, [ status ]);
function syncChange(key: SyncKeys, data: any) {
let timestamp = + new Date();
props.dispatcher({
dispatch({
type: 'SYNC_SET_REVISION',
key,
timestamp
@@ -99,7 +99,7 @@ function SyncManager(props: Props) {
if (packet.type === 'UserSettingsUpdate') {
let update: { [key in SyncKeys]?: [ number, SyncData[key] ] } = mapSync(packet.update, props.sync.revision);
props.dispatcher({
dispatch({
type: 'SYNC_UPDATE',
update
});
@@ -122,6 +122,5 @@ export default connectState(
sync: state.sync,
notifications: state.notifications
};
},
true
}
);

View File

@@ -1,11 +1,11 @@
import { ClientboundNotification } from "revolt.js/dist/websocket/notifications";
import { WithDispatcher } from "../../redux/reducers";
import { Client, Message } from "revolt.js/dist";
import {
ClientOperations,
ClientStatus
} from "./RevoltClient";
import { StateUpdater } from "preact/hooks";
import { dispatch } from "../../redux";
export var preventReconnect = false;
let preventUntil = 0;
@@ -15,9 +15,8 @@ export function setReconnectDisallowed(allowed: boolean) {
}
export function registerEvents({
operations,
dispatcher
}: { operations: ClientOperations } & WithDispatcher, setStatus: StateUpdater<ClientStatus>, client: Client) {
operations
}: { operations: ClientOperations }, setStatus: StateUpdater<ClientStatus>, client: Client) {
function attemptReconnect() {
if (preventReconnect) return;
function reconnect() {
@@ -47,7 +46,7 @@ export function registerEvents({
switch (packet.type) {
case "ChannelStartTyping": {
if (packet.user === client.user?._id) return;
dispatcher({
dispatch({
type: "TYPING_START",
channel: packet.id,
user: packet.user
@@ -56,7 +55,7 @@ export function registerEvents({
}
case "ChannelStopTyping": {
if (packet.user === client.user?._id) return;
dispatcher({
dispatch({
type: "TYPING_STOP",
channel: packet.id,
user: packet.user
@@ -64,7 +63,7 @@ export function registerEvents({
break;
}
case "ChannelAck": {
dispatcher({
dispatch({
type: "UNREADS_MARK_READ",
channel: packet.id,
message: packet.message_id
@@ -76,7 +75,7 @@ export function registerEvents({
message: (message: Message) => {
if (message.mentions?.includes(client.user!._id)) {
dispatcher({
dispatch({
type: "UNREADS_MENTION",
channel: message.channel,
message: message._id

View File

@@ -10,7 +10,6 @@ import {
} from "preact-context-menu";
import { ChannelPermission, ServerPermission, UserPermission } from "revolt.js/dist/api/permissions";
import { QueuedMessage } from "../redux/reducers/queue";
import { WithDispatcher } from "../redux/reducers";
import { useIntermediate } from "../context/intermediate/Intermediate";
import { AppContext, ClientStatus, StatusContext } from "../context/revoltjs/RevoltClient";
import { takeError } from "../context/revoltjs/util";
@@ -24,6 +23,7 @@ import { Cog } from "@styled-icons/boxicons-solid";
import { getNotificationState, Notifications, NotificationState } from "../redux/reducers/notifications";
import UserStatus from "../components/common/user/UserStatus";
import IconButton from "../components/ui/IconButton";
import { dispatch } from "../redux";
interface ContextMenuData {
user?: string;
@@ -81,7 +81,7 @@ type Action =
| { action: "open_server_channel_settings", server: string, id: string }
| { action: "set_notification_state", key: string, state?: NotificationState };
type Props = WithDispatcher & {
type Props = {
notifications: Notifications
};
@@ -110,7 +110,7 @@ function ContextMenus(props: Props) {
data.channel.channel_type === 'VoiceChannel') return;
let message = data.channel.channel_type === 'TextChannel' ? data.channel.last_message : data.channel.last_message._id;
props.dispatcher({
dispatch({
type: "UNREADS_MARK_READ",
channel: data.channel._id,
message
@@ -124,7 +124,7 @@ function ContextMenus(props: Props) {
{
const nonce = data.message.id;
const fail = (error: any) =>
props.dispatcher({
dispatch({
type: "QUEUE_FAIL",
nonce,
error
@@ -141,7 +141,7 @@ function ContextMenus(props: Props) {
)
.catch(fail);
props.dispatcher({
dispatch({
type: "QUEUE_START",
nonce
});
@@ -150,7 +150,7 @@ function ContextMenus(props: Props) {
case "cancel_message":
{
props.dispatcher({
dispatch({
type: "QUEUE_REMOVE",
nonce: data.message.id
});
@@ -330,9 +330,9 @@ function ContextMenus(props: Props) {
case "set_notification_state": {
const { key, state } = data;
if (state) {
props.dispatcher({ type: "NOTIFICATIONS_SET", key, state });
dispatch({ type: "NOTIFICATIONS_SET", key, state });
} else {
props.dispatcher({ type: "NOTIFICATIONS_REMOVE", key });
dispatch({ type: "NOTIFICATIONS_REMOVE", key });
}
break;
}
@@ -760,6 +760,5 @@ export default connectState(
return {
notifications: state.notifications
};
},
true
}
);

View File

@@ -6,14 +6,13 @@ import Checkbox from "../../../components/ui/Checkbox";
import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import ColourSwatches from "../../../components/ui/ColourSwatches";
import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import CollapsibleSection from "../../../components/common/CollapsibleSection";
import { FONTS, FONT_KEYS, MONOSCAPE_FONTS, MONOSCAPE_FONT_KEYS, Theme, ThemeContext, ThemeOptions } from "../../../context/Theme";
import { DEFAULT_FONT, DEFAULT_MONO_FONT, FONTS, FONT_KEYS, MONOSCAPE_FONTS, MONOSCAPE_FONT_KEYS, Theme, ThemeContext, ThemeOptions } from "../../../context/Theme";
// @ts-ignore
import pSBC from 'shade-blend-color';
@@ -25,25 +24,26 @@ import mutantSVG from '../assets/mutant_emoji.svg';
import notoSVG from '../assets/noto_emoji.svg';
import openmojiSVG from '../assets/openmoji_emoji.svg';
import twemojiSVG from '../assets/twemoji_emoji.svg';
import { dispatch } from "../../../redux";
interface Props {
settings: Settings;
}
// ! FIXME: code needs to be rewritten to fix jittering
export function Component(props: Props & WithDispatcher) {
export function Component(props: Props) {
const theme = useContext(ThemeContext);
const { writeClipboard, openScreen } = useIntermediate();
function setTheme(theme: ThemeOptions) {
props.dispatcher({
dispatch({
type: "SETTINGS_SET_THEME",
theme
});
}
function pushOverride(custom: Partial<Theme>) {
props.dispatcher({
dispatch({
type: "SETTINGS_SET_THEME_OVERRIDE",
custom
});
@@ -58,7 +58,7 @@ export function Component(props: Props & WithDispatcher) {
const emojiPack = props.settings.appearance?.emojiPack ?? 'mutant';
function setEmojiPack(emojiPack: EmojiPacks) {
props.dispatcher({
dispatch({
type: 'SETTINGS_SET_APPEARANCE',
options: {
emojiPack
@@ -135,7 +135,7 @@ export function Component(props: Props & WithDispatcher) {
<h3>
<Text id="app.settings.pages.appearance.font" />
</h3>
<ComboBox value={theme.font} onChange={e => setTheme({ custom: { font: e.currentTarget.value as any } })}>
<ComboBox value={theme.font ?? DEFAULT_FONT} onChange={e => setTheme({ custom: { font: e.currentTarget.value as any } })}>
{
FONT_KEYS
.map(key =>
@@ -284,7 +284,7 @@ export function Component(props: Props & WithDispatcher) {
<h3>
<Text id="app.settings.pages.appearance.mono_font" />
</h3>
<ComboBox value={theme.monoscapeFont} onChange={e => setTheme({ custom: { monoscapeFont: e.currentTarget.value as any } })}>
<ComboBox value={theme.monoscapeFont ?? DEFAULT_MONO_FONT} onChange={e => setTheme({ custom: { monoscapeFont: e.currentTarget.value as any } })}>
{
MONOSCAPE_FONT_KEYS
.map(key =>
@@ -313,6 +313,5 @@ export const Appearance = connectState(
return {
settings: state.settings
};
},
true
}
);

View File

@@ -1,15 +1,15 @@
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import { dispatch } from "../../../redux";
import Checkbox from "../../../components/ui/Checkbox";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import { AVAILABLE_EXPERIMENTS, ExperimentOptions } from "../../../redux/reducers/experiments";
interface Props {
options?: ExperimentOptions;
}
export function Component(props: Props & WithDispatcher) {
export function Component(props: Props) {
return (
<div className={styles.experiments}>
<h3>
@@ -20,12 +20,12 @@ export function Component(props: Props & WithDispatcher) {
key =>
<Checkbox
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
onChange={enabled => {
props.dispatcher({
onChange={enabled =>
dispatch({
type: enabled ? 'EXPERIMENTS_ENABLE' : 'EXPERIMENTS_DISABLE',
key
});
}}
})
}
>
<Text id={`app.settings.pages.experiments.titles.${key}`} />
<p>
@@ -51,6 +51,5 @@ export const ExperimentsPage = connectState(
return {
options: state.experiments
};
},
true
}
);

View File

@@ -1,19 +1,19 @@
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import { dispatch } from "../../../redux";
import Tip from "../../../components/ui/Tip";
import Emoji from "../../../components/common/Emoji";
import Checkbox from "../../../components/ui/Checkbox";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import { Language, LanguageEntry, Languages as Langs } from "../../../context/Locale";
type Props = WithDispatcher & {
type Props = {
locale: Language;
}
type Key = [ string, LanguageEntry ];
function Entry({ entry: [ x, lang ], locale, dispatcher }: { entry: Key } & Props) {
function Entry({ entry: [ x, lang ], locale }: { entry: Key } & Props) {
return (
<Checkbox
key={x}
@@ -21,7 +21,7 @@ function Entry({ entry: [ x, lang ], locale, dispatcher }: { entry: Key } & Prop
checked={locale === x}
onChange={v => {
if (v) {
dispatcher({
dispatch({
type: "SET_LOCALE",
locale: x as Language
});
@@ -80,6 +80,5 @@ export const Languages = connectState(
return {
locale: state.locale
};
},
true
}
);

View File

@@ -1,9 +1,9 @@
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import { dispatch } from "../../../redux";
import defaultsDeep from "lodash.defaultsdeep";
import Checkbox from "../../../components/ui/Checkbox";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio";
import { useContext, useEffect, useState } from "preact/hooks";
import { urlBase64ToUint8Array } from "../../../lib/conversion";
@@ -15,7 +15,7 @@ interface Props {
options?: NotificationOptions;
}
export function Component({ options, dispatcher }: Props & WithDispatcher) {
export function Component({ options }: Props) {
const client = useContext(AppContext);
const { openScreen } = useIntermediate();
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
@@ -51,7 +51,7 @@ export function Component({ options, dispatcher }: Props & WithDispatcher) {
}
}
dispatcher({
dispatch({
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
options: { desktopEnabled }
});
@@ -107,7 +107,7 @@ export function Component({ options, dispatcher }: Props & WithDispatcher) {
<Checkbox
checked={enabledSounds[key] ? true : false}
onChange={enabled =>
dispatcher({
dispatch({
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
options: {
sounds: {
@@ -131,6 +131,5 @@ export const Notifications = connectState(
return {
options: state.settings.notification
};
},
true
}
);

View File

@@ -1,15 +1,15 @@
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import { dispatch } from "../../../redux";
import Checkbox from "../../../components/ui/Checkbox";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import { SyncKeys, SyncOptions } from "../../../redux/reducers/sync";
interface Props {
options?: SyncOptions;
}
export function Component(props: Props & WithDispatcher) {
export function Component(props: Props) {
return (
<div className={styles.notifications}>
<h3>
@@ -26,12 +26,12 @@ export function Component(props: Props & WithDispatcher) {
<Checkbox
checked={(props.options?.disabled ?? []).indexOf(key) === -1}
description={<Text id={`app.settings.pages.sync.descriptions.${key}`} />}
onChange={enabled => {
props.dispatcher({
onChange={enabled =>
dispatch({
type: enabled ? 'SYNC_ENABLE_KEY' : 'SYNC_DISABLE_KEY',
key
});
}}
})
}
>
<Text id={`app.settings.pages.${title}`} />
</Checkbox>
@@ -47,6 +47,5 @@ export const Sync = connectState(
return {
options: state.sync
};
},
true
}
);

View File

@@ -1,7 +1,7 @@
import { store } from ".";
import localForage from "localforage";
import { Provider } from "react-redux";
import { Children } from "../types/Preact";
import { dispatch, State, store } from ".";
import { useEffect, useState } from "preact/hooks";
interface Props {
@@ -15,7 +15,7 @@ export default function State(props: Props) {
localForage.getItem("state")
.then(state => {
if (state !== null) {
store.dispatch({ type: "__INIT", state });
dispatch({ type: "__INIT", state: state as State });
}
setLoaded(true);

View File

@@ -8,16 +8,8 @@ import { connect, ConnectedComponent } from "react-redux";
export function connectState<T>(
component: (props: any) => h.JSX.Element | null,
mapKeys: (state: State, props: T) => any,
useDispatcher?: boolean,
memoize?: boolean
): ConnectedComponent<(props: any) => h.JSX.Element | null, T> {
let c = (
useDispatcher
? connect(mapKeys, (dispatcher) => {
return { dispatcher };
})
: connect(mapKeys)
)(component);
let c = connect(mapKeys)(component);
return memoize ? memo(c) : c;
}

View File

@@ -1,6 +1,6 @@
import { createStore } from "redux";
import rootReducer from "./reducers";
import localForage from "localforage";
import rootReducer, { Action } from "./reducers";
import { Core } from "revolt.js/dist/api/objects";
import { Typing } from "./reducers/typing";
@@ -77,3 +77,7 @@ store.subscribe(() => {
sectionToggle
});
});
export function dispatch(action: Action) {
store.dispatch(action);
}

View File

@@ -47,8 +47,6 @@ export type Action =
| SectionToggleAction
| { type: "__INIT"; state: State };
export type WithDispatcher = { dispatcher: (action: Action) => void };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function filter(obj: any, keys: string[]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any