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 351 additions and 156 deletions
// eslint-disable-next-line @typescript-eslint/no-unused-vars
/* eslint-disable */
import JSX = preact.JSX;
import { store } from ".";
import localForage from "localforage";
import { Provider } from "react-redux";
import { Children } from "../types/Preact";
import { useEffect, useState } from "preact/hooks";
import { dispatch, State, store } from ".";
import { Children } from "../types/Preact";
interface Props {
children: Children;
}
export default function State(props: Props) {
export default function StateLoader(props: Props) {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
localForage.getItem("state")
.then(state => {
if (state !== null) {
store.dispatch({ type: "__INIT", state });
}
setLoaded(true);
});
localForage.getItem("state").then((state) => {
if (state !== null) {
dispatch({ type: "__INIT", state: state as State });
}
setLoaded(true);
});
}, []);
if (!loaded) return null;
......
/* eslint-disable @typescript-eslint/no-explicit-any */
import { connect, ConnectedComponent } from "react-redux";
import { State } from ".";
import { h } from "preact";
import { memo } from "preact/compat";
import { connect, ConnectedComponent } from "react-redux";
import { State } from ".";
export function connectState<T>(
component: (props: any) => h.JSX.Element | null,
mapKeys: (state: State, props: T) => any,
useDispatcher?: boolean,
memoize?: boolean
memoize?: boolean,
): ConnectedComponent<(props: any) => h.JSX.Element | null, T> {
let c = (
useDispatcher
? connect(mapKeys, (dispatcher) => {
return { dispatcher };
})
: connect(mapKeys)
)(component);
const c = connect(mapKeys)(component);
return memoize ? memo(c) : c;
}
import { createStore } from "redux";
import rootReducer from "./reducers";
import localForage from "localforage";
import { createStore } from "redux";
import { RevoltConfiguration } from "revolt-api/types/Core";
import { Core } from "revolt.js/dist/api/objects";
import { Typing } from "./reducers/typing";
import { Drafts } from "./reducers/drafts";
import { AuthState } from "./reducers/auth";
import { Language } from "../context/Locale";
import { Unreads } from "./reducers/unreads";
import { SyncOptions } from "./reducers/sync";
import { Settings } from "./reducers/settings";
import { QueuedMessage } from "./reducers/queue";
import rootReducer, { Action } from "./reducers";
import { AuthState } from "./reducers/auth";
import { Drafts } from "./reducers/drafts";
import { ExperimentOptions } from "./reducers/experiments";
import { LastOpened } from "./reducers/last_opened";
import { Notifications } from "./reducers/notifications";
import { QueuedMessage } from "./reducers/queue";
import { SectionToggle } from "./reducers/section_toggle";
import { Settings } from "./reducers/settings";
import { SyncOptions } from "./reducers/sync";
import { Unreads } from "./reducers/unreads";
export type State = {
config: Core.RevoltNodeConfiguration,
config: RevoltConfiguration;
locale: Language;
auth: AuthState;
settings: Settings;
unreads: Unreads;
queue: QueuedMessage[];
typing: Typing;
drafts: Drafts;
sync: SyncOptions;
experiments: ExperimentOptions;
lastOpened: LastOpened;
notifications: Notifications;
sectionToggle: SectionToggle;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
......@@ -51,6 +56,9 @@ store.subscribe(() => {
drafts,
sync,
experiments,
lastOpened,
notifications,
sectionToggle,
} = store.getState() as State;
localForage.setItem("state", {
......@@ -63,5 +71,16 @@ store.subscribe(() => {
drafts,
sync,
experiments,
lastOpened,
notifications,
sectionToggle,
});
});
export function dispatch(action: Action) {
store.dispatch(action);
}
export function getState(): State {
return store.getState();
}
import { Auth } from "revolt.js/dist/api/objects";
import { Session } from "revolt-api/types/Auth";
export interface AuthState {
accounts: {
[key: string]: {
session: Auth.Session;
session: Session;
};
};
active?: string;
......@@ -13,7 +13,7 @@ export type AuthAction =
| { type: undefined }
| {
type: "LOGIN";
session: Auth.Session;
session: Session;
}
| {
type: "LOGOUT";
......@@ -22,7 +22,7 @@ export type AuthAction =
export function auth(
state = { accounts: {} } as AuthState,
action: AuthAction
action: AuthAction,
): AuthState {
switch (action.type) {
case "LOGIN":
......
export type Experiments = never;
export const AVAILABLE_EXPERIMENTS: Experiments[] = [];
export type Experiments = "search";
export const AVAILABLE_EXPERIMENTS: Experiments[] = ["search"];
export const EXPERIMENTS: {
[key in Experiments]: { title: string; description: string };
} = {
search: {
title: "Search",
description: "Allows you to search for messages in channels.",
},
};
export interface ExperimentOptions {
enabled?: Experiments[];
......@@ -18,7 +26,7 @@ export type ExperimentsAction =
export function experiments(
state = {} as ExperimentOptions,
action: ExperimentsAction
action: ExperimentsAction,
): ExperimentOptions {
switch (action.type) {
case "EXPERIMENTS_ENABLE":
......
import { State } from "..";
import { combineReducers } from "redux";
import { config, ConfigAction } from "./server_config";
import { settings, SettingsAction } from "./settings";
import { locale, LocaleAction } from "./locale";
import { State } from "..";
import { auth, AuthAction } from "./auth";
import { unreads, UnreadsAction } from "./unreads";
import { queue, QueueAction } from "./queue";
import { typing, TypingAction } from "./typing";
import { drafts, DraftAction } from "./drafts";
import { sync, SyncAction } from "./sync";
import { experiments, ExperimentsAction } from "./experiments";
import { lastOpened, LastOpenedAction } from "./last_opened";
import { locale, LocaleAction } from "./locale";
import { notifications, NotificationsAction } from "./notifications";
import { queue, QueueAction } from "./queue";
import { sectionToggle, SectionToggleAction } from "./section_toggle";
import { config, ConfigAction } from "./server_config";
import { settings, SettingsAction } from "./settings";
import { sync, SyncAction } from "./sync";
import { unreads, UnreadsAction } from "./unreads";
export default combineReducers({
config,
......@@ -19,10 +21,12 @@ export default combineReducers({
settings,
unreads,
queue,
typing,
drafts,
sync,
experiments,
lastOpened,
notifications,
sectionToggle,
});
export type Action =
......@@ -32,22 +36,10 @@ export type Action =
| SettingsAction
| UnreadsAction
| QueueAction
| TypingAction
| DraftAction
| SyncAction
| ExperimentsAction
| LastOpenedAction
| NotificationsAction
| 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
const newObj: any = {};
for (const key of keys) {
const v = obj[key];
if (v) newObj[key] = v;
}
return newObj;
}
export interface LastOpened {
[key: string]: string;
}
export type LastOpenedAction =
| { type: undefined }
| {
type: "LAST_OPENED_SET";
parent: string;
child: string;
}
| {
type: "RESET";
};
export function lastOpened(
state = {} as LastOpened,
action: LastOpenedAction,
): LastOpened {
switch (action.type) {
case "LAST_OPENED_SET": {
return {
...state,
[action.parent]: action.child,
};
}
case "RESET":
return {};
default:
return state;
}
}
import { Language } from "../../context/Locale";
import { SyncUpdateAction } from "./sync";
import type { SyncUpdateAction } from "./sync";
export type LocaleAction =
| { type: undefined }
......
import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import type { SyncUpdateAction } from "./sync";
export type NotificationState = "all" | "mention" | "none" | "muted";
export type Notifications = {
[key: string]: NotificationState;
};
export const DEFAULT_STATES: {
[key in Channel["channel_type"]]: NotificationState;
} = {
SavedMessages: "all",
DirectMessage: "all",
Group: "all",
TextChannel: "mention",
VoiceChannel: "mention",
};
export function getNotificationState(
notifications: Notifications,
channel: Channel,
) {
return notifications[channel._id] ?? DEFAULT_STATES[channel.channel_type];
}
export function shouldNotify(
state: NotificationState,
message: Message,
user_id: string,
) {
switch (state) {
case "muted":
case "none":
return false;
case "mention": {
if (!message.mention_ids?.includes(user_id)) return false;
}
}
return true;
}
export type NotificationsAction =
| { type: undefined }
| {
type: "NOTIFICATIONS_SET";
key: string;
state: NotificationState;
}
| {
type: "NOTIFICATIONS_REMOVE";
key: string;
}
| SyncUpdateAction
| {
type: "RESET";
};
export function notifications(
state = {} as Notifications,
action: NotificationsAction,
): Notifications {
switch (action.type) {
case "NOTIFICATIONS_SET":
return {
...state,
[action.key]: action.state,
};
case "NOTIFICATIONS_REMOVE": {
const { [action.key]: _, ...newState } = state;
return newState;
}
case "SYNC_UPDATE":
return action.update.notifications?.[1] ?? state;
case "RESET":
return {};
default:
return state;
}
}
import { MessageObject } from "../../context/revoltjs/util";
export enum QueueStatus {
SENDING = "sending",
ERRORED = "errored",
}
export interface Reply {
id: string;
mention: boolean;
}
export type QueuedMessageData = {
_id: string;
author: string;
channel: string;
content: string;
replies: Reply[];
};
export interface QueuedMessage {
id: string;
channel: string;
data: MessageObject;
data: QueuedMessageData;
status: QueueStatus;
error?: string;
}
......@@ -19,7 +31,7 @@ export type QueueAction =
type: "QUEUE_ADD";
nonce: string;
channel: string;
message: MessageObject;
message: QueuedMessageData;
}
| {
type: "QUEUE_FAIL";
......@@ -46,7 +58,7 @@ export type QueueAction =
export function queue(
state: QueuedMessage[] = [],
action: QueueAction
action: QueueAction,
): QueuedMessage[] {
switch (action.type) {
case "QUEUE_ADD": {
......@@ -62,7 +74,7 @@ export function queue(
}
case "QUEUE_FAIL": {
const entry = state.find(
(x) => x.id === action.nonce
(x) => x.id === action.nonce,
) as QueuedMessage;
return [
...state.filter((x) => x.id !== action.nonce),
......@@ -75,7 +87,7 @@ export function queue(
}
case "QUEUE_START": {
const entry = state.find(
(x) => x.id === action.nonce
(x) => x.id === action.nonce,
) as QueuedMessage;
return [
...state.filter((x) => x.id !== action.nonce),
......
export interface SectionToggle {
[key: string]: boolean;
}
export type SectionToggleAction =
| { type: undefined }
| {
type: "SECTION_TOGGLE_SET";
id: string;
state: boolean;
}
| {
type: "SECTION_TOGGLE_UNSET";
id: string;
}
| {
type: "RESET";
};
export function sectionToggle(
state = {} as SectionToggle,
action: SectionToggleAction,
): SectionToggle {
switch (action.type) {
case "SECTION_TOGGLE_SET": {
return {
...state,
[action.id]: action.state,
};
}
case "SECTION_TOGGLE_UNSET": {
const { [action.id]: _, ...newState } = state;
return newState;
}
case "RESET":
return {};
default:
return state;
}
}
import { Core } from "revolt.js/dist/api/objects";
import type { RevoltConfiguration } from "revolt-api/types/Core";
export type ConfigAction =
| { type: undefined }
| {
type: "SET_CONFIG";
config: Core.RevoltNodeConfiguration;
type: "SET_CONFIG";
config: RevoltConfiguration;
};
export function config(
state = { } as Core.RevoltNodeConfiguration,
action: ConfigAction
): Core.RevoltNodeConfiguration {
state = {} as RevoltConfiguration,
action: ConfigAction,
): RevoltConfiguration {
switch (action.type) {
case "SET_CONFIG":
return action.config;
......
import { filter } from ".";
import { SyncUpdateAction } from "./sync";
import { Theme, ThemeOptions } from "../../context/Theme";
import type { Theme, ThemeOptions } from "../../context/Theme";
import { setEmojiPack } from "../../components/common/Emoji";
import type { Sounds } from "../../assets/sounds/Audio";
import type { SyncUpdateAction } from "./sync";
export type SoundOptions = {
[key in Sounds]?: boolean;
};
export const DEFAULT_SOUNDS: SoundOptions = {
message: true,
outbound: false,
call_join: true,
call_leave: true,
};
export interface NotificationOptions {
desktopEnabled?: boolean;
soundEnabled?: boolean;
outgoingSoundEnabled?: boolean;
sounds?: SoundOptions;
}
export type EmojiPacks = "mutant" | "twemoji" | "noto" | "openmoji";
......@@ -44,16 +57,16 @@ export type SettingsAction =
export function settings(
state = {} as Settings,
action: SettingsAction
action: SettingsAction,
): Settings {
// setEmojiPack(state.appearance?.emojiPack ?? 'mutant');
setEmojiPack(state.appearance?.emojiPack ?? "mutant");
switch (action.type) {
case "SETTINGS_SET_THEME":
return {
...state,
theme: {
...filter(state.theme, ["custom", "preset"]),
...state.theme,
...action.theme,
},
};
......@@ -80,7 +93,7 @@ export function settings(
return {
...state,
appearance: {
...filter(state.appearance, ["emojiPack"]),
...state.appearance,
...action.options,
},
};
......
import { AppearanceOptions } from "./settings";
import { Language } from "../../context/Locale";
import { ThemeOptions } from "../../context/Theme";
import type { Language } from "../../context/Locale";
import type { ThemeOptions } from "../../context/Theme";
export type SyncKeys = "theme" | "appearance" | "locale";
import type { Notifications } from "./notifications";
import type { AppearanceOptions } from "./settings";
export type SyncKeys = "theme" | "appearance" | "locale" | "notifications";
export interface SyncData {
locale?: Language;
theme?: ThemeOptions;
appearance?: AppearanceOptions;
notifications?: Notifications;
}
export const DEFAULT_ENABLED_SYNC: SyncKeys[] = [
"theme",
"appearance",
"locale",
"notifications",
];
export interface SyncOptions {
disabled?: SyncKeys[];
......@@ -46,7 +50,7 @@ export type SyncAction =
export function sync(
state = {} as SyncOptions,
action: SyncAction
action: SyncAction,
): SyncOptions {
switch (action.type) {
case "SYNC_DISABLE_KEY":
......
export type TypingUser = { id: string; started: number };
export type Typing = { [key: string]: TypingUser[] };
export type TypingAction =
| { type: undefined }
| {
type: "TYPING_START";
channel: string;
user: string;
}
| {
type: "TYPING_STOP";
channel: string;
user: string;
}
| {
type: "RESET";
};
export function typing(state: Typing = {}, action: TypingAction): Typing {
switch (action.type) {
case "TYPING_START":
return {
...state,
[action.channel]: [
...(state[action.channel] ?? []).filter(
(x) => x.id !== action.user
),
{
id: action.user,
started: +new Date(),
},
],
};
case "TYPING_STOP":
return {
...state,
[action.channel]:
state[action.channel]?.filter(
(x) => x.id !== action.user
) ?? [],
};
case "RESET":
return {};
default:
return state;
}
}
import { Sync } from "revolt.js/dist/api/objects";
import type { ChannelUnread } from "revolt-api/types/Sync";
export interface Unreads {
[key: string]: Partial<Omit<Sync.ChannelUnread, "_id">>;
[key: string]: Partial<Omit<ChannelUnread, "_id">>;
}
export type UnreadsAction =
......@@ -10,11 +10,10 @@ export type UnreadsAction =
type: "UNREADS_MARK_READ";
channel: string;
message: string;
request: boolean;
}
| {
type: "UNREADS_SET";
unreads: Sync.ChannelUnread[];
unreads: ChannelUnread[];
}
| {
type: "UNREADS_MENTION";
......@@ -28,10 +27,6 @@ export type UnreadsAction =
export function unreads(state = {} as Unreads, action: UnreadsAction): Unreads {
switch (action.type) {
case "UNREADS_MARK_READ":
if (action.request) {
// client.req('PUT', `/channels/${action.channel}/ack/${action.message}` as '/channels/id/ack/id');
}
return {
...state,
[action.channel]: {
......
/* eslint-disable */
// Strings needs to be explictly stated here as they can cause type issues elsewhere.
export const REPO_URL: string =
"https://gitlab.insrt.uk/revolt/revite/-/commit";
export const GIT_REVISION: string = "__GIT_REVISION__";
export const GIT_BRANCH: string = "__GIT_BRANCH__";
.preact-context-menu .context-menu {
z-index: 100;
min-width: 180px;
z-index: 5000;
min-width: 190px;
padding: 6px 8px;
user-select: none;
border-radius: 4px;
font-size: .875rem;
color: var(--secondary-foreground);
border-radius: var(--border-radius);
background: var(--primary-background) !important;
box-shadow: 0px 0px 8px 8px rgba(0, 0, 0, 0.05);
> span {
> span, .main > span {
gap: 6px;
margin: 2px 0;
display: flex;
padding: 6px 8px;
border-radius: 3px;
font-size: .875rem;
align-items: center;
white-space: nowrap;
......@@ -33,6 +33,41 @@
color: var(--tertiary-foreground);
}
}
}
.context-menu.Status {
.header {
gap: 8px;
display: flex;
padding: 6px 8px;
font-weight: 600;
align-items: center;
color: var(--foreground);
.main {
flex-grow: 1;
display: flex;
flex-direction: column;
.username {
> div {
cursor: pointer;
width: fit-content;
}
}
}
.status {
cursor: pointer;
max-width: 132px;
font-size: .625rem;
color: var(--secondary-foreground);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.indicator {
width: 8px;
......
......@@ -16,6 +16,10 @@
background: var(--scrollbar-thumb);
}
::-webkit-scrollbar-corner {
background: transparent;
}
::selection {
background: var(--accent);
color: var(--foreground);
......@@ -44,3 +48,7 @@ hr {
height: 1px;
flex-grow: 1;
}
foreignObject > svg {
vertical-align: top !important;
}