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 477 additions and 194 deletions
import { Plus } from "@styled-icons/boxicons-regular";
import isEqual from "lodash.isequal";
import { observer } from "mobx-react-lite";
import { ChannelPermission, ServerPermission } from "revolt.js";
import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import ColourSwatches from "../../../components/ui/ColourSwatches";
import InputBox from "../../../components/ui/InputBox";
import Overline from "../../../components/ui/Overline";
import ButtonItem from "../../../components/navigation/items/ButtonItem";
interface Props {
server: Server;
}
const I32ToU32 = (arr: number[]) => arr.map((x) => x >>> 0);
// ! FIXME: bad code :)
export const Roles = observer(({ server }: Props) => {
const [role, setRole] = useState("default");
const { openScreen } = useIntermediate();
const roles = useMemo(() => server.roles ?? {}, [server]);
if (role !== "default" && typeof roles[role] === "undefined") {
useEffect(() => setRole("default"), [role]);
return null;
}
const {
name: roleName,
colour: roleColour,
permissions,
} = roles[role] ?? {};
const getPermissions = useCallback(
(id: string) => {
return I32ToU32(
id === "default"
? server.default_permissions
: roles[id].permissions,
);
},
[roles, server],
);
const [perm, setPerm] = useState(getPermissions(role));
const [name, setName] = useState(roleName);
const [colour, setColour] = useState(roleColour);
useEffect(
() => setPerm(getPermissions(role)),
[getPermissions, role, permissions],
);
useEffect(() => setName(roleName), [role, roleName]);
useEffect(() => setColour(roleColour), [role, roleColour]);
const modified =
!isEqual(perm, getPermissions(role)) ||
!isEqual(name, roleName) ||
!isEqual(colour, roleColour);
const save = () => {
if (!isEqual(perm, getPermissions(role))) {
server.setPermissions(role, {
server: perm[0],
channel: perm[1],
});
}
if (!isEqual(name, roleName) || !isEqual(colour, roleColour)) {
server.editRole(role, { name, colour });
}
};
const deleteRole = () => {
setRole("default");
server.deleteRole(role);
};
return (
<div className={styles.roles}>
<div className={styles.list}>
<div className={styles.title}>
<h1>
<Text id="app.settings.server_pages.roles.title" />
</h1>
<Plus
size={22}
onClick={() =>
openScreen({
id: "special_input",
type: "create_role",
server,
callback: (id) => setRole(id),
})
}
/>
</div>
{["default", ...Object.keys(roles)].map((id) => {
if (id === "default") {
return (
<ButtonItem
active={role === "default"}
onClick={() => setRole("default")}>
<Text id="app.settings.permissions.default_role" />
</ButtonItem>
);
}
return (
<ButtonItem
key={id}
active={role === id}
onClick={() => setRole(id)}
style={{ color: roles[id].colour }}>
{roles[id].name}
</ButtonItem>
);
})}
</div>
<div className={styles.permissions}>
<div className={styles.title}>
<h2>
{role === "default" ? (
<Text id="app.settings.permissions.default_role" />
) : (
roles[role].name
)}
</h2>
<Button contrast disabled={!modified} onClick={save}>
Save
</Button>
</div>
{role !== "default" && (
<>
<section>
<Overline type="subtle">Role Name</Overline>
<p>
<InputBox
value={name}
onChange={(e) =>
setName(e.currentTarget.value)
}
contrast
/>
</p>
</section>
<section>
<Overline type="subtle">Role Colour</Overline>
<p>
<ColourSwatches
value={colour ?? "gray"}
onChange={(value) => setColour(value)}
/>
</p>
</section>
</>
)}
<section>
<Overline type="subtle">
<Text id="app.settings.permissions.server" />
</Overline>
{Object.keys(ServerPermission).map((key) => {
if (key === "View") return;
const value =
ServerPermission[
key as keyof typeof ServerPermission
];
return (
<Checkbox
key={key}
checked={(perm[0] & value) > 0}
onChange={() =>
setPerm([perm[0] ^ value, perm[1]])
}
description={
<Text id={`permissions.server.${key}.d`} />
}>
<Text id={`permissions.server.${key}.t`} />
</Checkbox>
);
})}
</section>
<section>
<Overline type="subtle">
<Text id="app.settings.permissions.channel" />
</Overline>
{Object.keys(ChannelPermission).map((key) => {
if (key === "ManageChannel") return;
const value =
ChannelPermission[
key as keyof typeof ChannelPermission
];
return (
<Checkbox
key={key}
checked={((perm[1] >>> 0) & value) > 0}
onChange={() =>
setPerm([perm[0], perm[1] ^ value])
}
disabled={key === "View"}
description={
<Text id={`permissions.channel.${key}.d`} />
}>
<Text id={`permissions.channel.${key}.t`} />
</Checkbox>
);
})}
</section>
<div className={styles.actions}>
<Button contrast disabled={!modified} onClick={save}>
Save
</Button>
{role !== "default" && (
<Button contrast error onClick={deleteRole}>
Delete
</Button>
)}
</div>
</div>
</div>
);
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars /* eslint-disable */
import JSX = preact.JSX; import JSX = preact.JSX;
import { store } from ".";
import localForage from "localforage"; import localForage from "localforage";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { Children } from "../types/Preact";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { dispatch, State, store } from ".";
import { Children } from "../types/Preact";
interface Props { interface Props {
children: Children; children: Children;
} }
export default function State(props: Props) { export default function StateLoader(props: Props) {
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
useEffect(() => { useEffect(() => {
localForage.getItem("state") localForage.getItem("state").then((state) => {
.then(state => { if (state !== null) {
if (state !== null) { dispatch({ type: "__INIT", state: state as State });
store.dispatch({ type: "__INIT", state }); }
}
setLoaded(true);
setLoaded(true); });
});
}, []); }, []);
if (!loaded) return null; if (!loaded) return null;
......
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { connect, ConnectedComponent } from "react-redux";
import { State } from ".";
import { h } from "preact"; import { h } from "preact";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
import { connect, ConnectedComponent } from "react-redux";
import { State } from ".";
export function connectState<T>( export function connectState<T>(
component: (props: any) => h.JSX.Element | null, component: (props: any) => h.JSX.Element | null,
mapKeys: (state: State, props: T) => any, mapKeys: (state: State, props: T) => any,
useDispatcher?: boolean, memoize?: boolean,
memoize?: boolean
): ConnectedComponent<(props: any) => h.JSX.Element | null, T> { ): ConnectedComponent<(props: any) => h.JSX.Element | null, T> {
let c = ( const c = connect(mapKeys)(component);
useDispatcher
? connect(mapKeys, (dispatcher) => {
return { dispatcher };
})
: connect(mapKeys)
)(component);
return memoize ? memo(c) : c; return memoize ? memo(c) : c;
} }
import { createStore } from "redux";
import rootReducer from "./reducers";
import localForage from "localforage"; 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 { Language } from "../context/Locale";
import { Unreads } from "./reducers/unreads";
import { SyncOptions } from "./reducers/sync"; import rootReducer, { Action } from "./reducers";
import { Settings } from "./reducers/settings"; import { AuthState } from "./reducers/auth";
import { QueuedMessage } from "./reducers/queue"; import { Drafts } from "./reducers/drafts";
import { ExperimentOptions } from "./reducers/experiments"; import { ExperimentOptions } from "./reducers/experiments";
import { LastOpened } from "./reducers/last_opened"; import { LastOpened } from "./reducers/last_opened";
import { Notifications } from "./reducers/notifications"; 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 = { export type State = {
config: Core.RevoltNodeConfiguration, config: RevoltConfiguration;
locale: Language; locale: Language;
auth: AuthState; auth: AuthState;
settings: Settings; settings: Settings;
unreads: Unreads; unreads: Unreads;
queue: QueuedMessage[]; queue: QueuedMessage[];
typing: Typing;
drafts: Drafts; drafts: Drafts;
sync: SyncOptions; sync: SyncOptions;
experiments: ExperimentOptions; experiments: ExperimentOptions;
lastOpened: LastOpened; lastOpened: LastOpened;
notifications: Notifications; notifications: Notifications;
sectionToggle: SectionToggle;
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
...@@ -56,7 +57,8 @@ store.subscribe(() => { ...@@ -56,7 +57,8 @@ store.subscribe(() => {
sync, sync,
experiments, experiments,
lastOpened, lastOpened,
notifications notifications,
sectionToggle,
} = store.getState() as State; } = store.getState() as State;
localForage.setItem("state", { localForage.setItem("state", {
...@@ -70,6 +72,15 @@ store.subscribe(() => { ...@@ -70,6 +72,15 @@ store.subscribe(() => {
sync, sync,
experiments, experiments,
lastOpened, lastOpened,
notifications notifications,
sectionToggle,
}); });
}); });
export function dispatch(action: Action) {
store.dispatch(action);
}
export function getState(): State {
return store.getState();
}
import type { Auth } from "revolt.js/dist/api/objects"; import { Session } from "revolt-api/types/Auth";
export interface AuthState { export interface AuthState {
accounts: { accounts: {
[key: string]: { [key: string]: {
session: Auth.Session; session: Session;
}; };
}; };
active?: string; active?: string;
...@@ -13,7 +13,7 @@ export type AuthAction = ...@@ -13,7 +13,7 @@ export type AuthAction =
| { type: undefined } | { type: undefined }
| { | {
type: "LOGIN"; type: "LOGIN";
session: Auth.Session; session: Session;
} }
| { | {
type: "LOGOUT"; type: "LOGOUT";
...@@ -22,7 +22,7 @@ export type AuthAction = ...@@ -22,7 +22,7 @@ export type AuthAction =
export function auth( export function auth(
state = { accounts: {} } as AuthState, state = { accounts: {} } as AuthState,
action: AuthAction action: AuthAction,
): AuthState { ): AuthState {
switch (action.type) { switch (action.type) {
case "LOGIN": case "LOGIN":
......
export type Experiments = never; export type Experiments = "search";
export const AVAILABLE_EXPERIMENTS: Experiments[] = []; 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 { export interface ExperimentOptions {
enabled?: Experiments[]; enabled?: Experiments[];
...@@ -18,7 +26,7 @@ export type ExperimentsAction = ...@@ -18,7 +26,7 @@ export type ExperimentsAction =
export function experiments( export function experiments(
state = {} as ExperimentOptions, state = {} as ExperimentOptions,
action: ExperimentsAction action: ExperimentsAction,
): ExperimentOptions { ): ExperimentOptions {
switch (action.type) { switch (action.type) {
case "EXPERIMENTS_ENABLE": case "EXPERIMENTS_ENABLE":
......
import { State } from "..";
import { combineReducers } from "redux"; import { combineReducers } from "redux";
import { config, ConfigAction } from "./server_config"; import { State } from "..";
import { settings, SettingsAction } from "./settings";
import { locale, LocaleAction } from "./locale";
import { auth, AuthAction } from "./auth"; 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 { drafts, DraftAction } from "./drafts";
import { sync, SyncAction } from "./sync";
import { experiments, ExperimentsAction } from "./experiments"; import { experiments, ExperimentsAction } from "./experiments";
import { lastOpened, LastOpenedAction } from "./last_opened"; import { lastOpened, LastOpenedAction } from "./last_opened";
import { locale, LocaleAction } from "./locale";
import { notifications, NotificationsAction } from "./notifications"; 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({ export default combineReducers({
config, config,
...@@ -21,12 +21,12 @@ export default combineReducers({ ...@@ -21,12 +21,12 @@ export default combineReducers({
settings, settings,
unreads, unreads,
queue, queue,
typing,
drafts, drafts,
sync, sync,
experiments, experiments,
lastOpened, lastOpened,
notifications notifications,
sectionToggle,
}); });
export type Action = export type Action =
...@@ -36,24 +36,10 @@ export type Action = ...@@ -36,24 +36,10 @@ export type Action =
| SettingsAction | SettingsAction
| UnreadsAction | UnreadsAction
| QueueAction | QueueAction
| TypingAction
| DraftAction | DraftAction
| SyncAction | SyncAction
| ExperimentsAction | ExperimentsAction
| LastOpenedAction | LastOpenedAction
| NotificationsAction | NotificationsAction
| SectionToggleAction
| { type: "__INIT"; state: State }; | { 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 { export interface LastOpened {
[key: string]: string [key: string]: string;
} }
export type LastOpenedAction = export type LastOpenedAction =
| { type: undefined } | { type: undefined }
| { | {
type: "LAST_OPENED_SET"; type: "LAST_OPENED_SET";
parent: string; parent: string;
child: string; child: string;
} }
| { | {
type: "RESET"; type: "RESET";
}; };
export function lastOpened(state = {} as LastOpened, action: LastOpenedAction): LastOpened { export function lastOpened(
state = {} as LastOpened,
action: LastOpenedAction,
): LastOpened {
switch (action.type) { switch (action.type) {
case "LAST_OPENED_SET": { case "LAST_OPENED_SET": {
return { return {
...state, ...state,
[action.parent]: action.child [action.parent]: action.child,
} };
} }
case "RESET": case "RESET":
return {}; return {};
......
import { Language } from "../../context/Locale"; import { Language } from "../../context/Locale";
import type { SyncUpdateAction } from "./sync"; import type { SyncUpdateAction } from "./sync";
export type LocaleAction = export type LocaleAction =
......
import type { Channel, Message } from "revolt.js"; import { Channel } from "revolt.js/dist/maps/Channels";
import { Message } from "revolt.js/dist/maps/Messages";
import type { SyncUpdateAction } from "./sync"; import type { SyncUpdateAction } from "./sync";
export type NotificationState = 'all' | 'mention' | 'none' | 'muted'; export type NotificationState = "all" | "mention" | "none" | "muted";
export type Notifications = { export type Notifications = {
[key: string]: NotificationState [key: string]: NotificationState;
} };
export const DEFAULT_STATES: { [key in Channel['channel_type']]: NotificationState } = { export const DEFAULT_STATES: {
'SavedMessages': 'all', [key in Channel["channel_type"]]: NotificationState;
'DirectMessage': 'all', } = {
'Group': 'all', SavedMessages: "all",
'TextChannel': 'mention', DirectMessage: "all",
'VoiceChannel': 'mention' Group: "all",
TextChannel: "mention",
VoiceChannel: "mention",
}; };
export function getNotificationState(notifications: Notifications, channel: Channel) { export function getNotificationState(
notifications: Notifications,
channel: Channel,
) {
return notifications[channel._id] ?? DEFAULT_STATES[channel.channel_type]; return notifications[channel._id] ?? DEFAULT_STATES[channel.channel_type];
} }
export function shouldNotify(state: NotificationState, message: Message, user_id: string) { export function shouldNotify(
state: NotificationState,
message: Message,
user_id: string,
) {
switch (state) { switch (state) {
case 'muted': case "muted":
case 'none': return false; case "none":
case 'mention': { return false;
if (!message.mentions?.includes(user_id)) return false; case "mention": {
if (!message.mention_ids?.includes(user_id)) return false;
} }
} }
...@@ -34,34 +46,33 @@ export function shouldNotify(state: NotificationState, message: Message, user_id ...@@ -34,34 +46,33 @@ export function shouldNotify(state: NotificationState, message: Message, user_id
export type NotificationsAction = export type NotificationsAction =
| { type: undefined } | { type: undefined }
| { | {
type: "NOTIFICATIONS_SET"; type: "NOTIFICATIONS_SET";
key: string; key: string;
state: NotificationState; state: NotificationState;
} }
| { | {
type: "NOTIFICATIONS_REMOVE"; type: "NOTIFICATIONS_REMOVE";
key: string; key: string;
} }
| SyncUpdateAction | SyncUpdateAction
| { | {
type: "RESET"; type: "RESET";
}; };
export function notifications( export function notifications(
state = {} as Notifications, state = {} as Notifications,
action: NotificationsAction action: NotificationsAction,
): Notifications { ): Notifications {
switch (action.type) { switch (action.type) {
case "NOTIFICATIONS_SET": case "NOTIFICATIONS_SET":
return { return {
...state, ...state,
[action.key]: action.state [action.key]: action.state,
}; };
case "NOTIFICATIONS_REMOVE": case "NOTIFICATIONS_REMOVE": {
{ const { [action.key]: _, ...newState } = state;
const { [action.key]: _, ...newState } = state; return newState;
return newState; }
}
case "SYNC_UPDATE": case "SYNC_UPDATE":
return action.update.notifications?.[1] ?? state; return action.update.notifications?.[1] ?? state;
case "RESET": case "RESET":
......
import type { MessageObject } from "../../context/revoltjs/util";
export enum QueueStatus { export enum QueueStatus {
SENDING = "sending", SENDING = "sending",
ERRORED = "errored", ERRORED = "errored",
} }
export interface Reply { export interface Reply {
id: string, id: string;
mention: boolean mention: boolean;
} }
export type QueuedMessageData = Omit<MessageObject, 'content' | 'replies'> & { export type QueuedMessageData = {
_id: string;
author: string;
channel: string;
content: string; content: string;
replies: Reply[]; replies: Reply[];
} };
export interface QueuedMessage { export interface QueuedMessage {
id: string; id: string;
...@@ -56,7 +58,7 @@ export type QueueAction = ...@@ -56,7 +58,7 @@ export type QueueAction =
export function queue( export function queue(
state: QueuedMessage[] = [], state: QueuedMessage[] = [],
action: QueueAction action: QueueAction,
): QueuedMessage[] { ): QueuedMessage[] {
switch (action.type) { switch (action.type) {
case "QUEUE_ADD": { case "QUEUE_ADD": {
...@@ -72,7 +74,7 @@ export function queue( ...@@ -72,7 +74,7 @@ export function queue(
} }
case "QUEUE_FAIL": { case "QUEUE_FAIL": {
const entry = state.find( const entry = state.find(
(x) => x.id === action.nonce (x) => x.id === action.nonce,
) as QueuedMessage; ) as QueuedMessage;
return [ return [
...state.filter((x) => x.id !== action.nonce), ...state.filter((x) => x.id !== action.nonce),
...@@ -85,7 +87,7 @@ export function queue( ...@@ -85,7 +87,7 @@ export function queue(
} }
case "QUEUE_START": { case "QUEUE_START": {
const entry = state.find( const entry = state.find(
(x) => x.id === action.nonce (x) => x.id === action.nonce,
) as QueuedMessage; ) as QueuedMessage;
return [ return [
...state.filter((x) => x.id !== action.nonce), ...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 type { Core } from "revolt.js/dist/api/objects"; import type { RevoltConfiguration } from "revolt-api/types/Core";
export type ConfigAction = export type ConfigAction =
| { type: undefined } | { type: undefined }
| { | {
type: "SET_CONFIG"; type: "SET_CONFIG";
config: Core.RevoltNodeConfiguration; config: RevoltConfiguration;
}; };
export function config( export function config(
state = { } as Core.RevoltNodeConfiguration, state = {} as RevoltConfiguration,
action: ConfigAction action: ConfigAction,
): Core.RevoltNodeConfiguration { ): RevoltConfiguration {
switch (action.type) { switch (action.type) {
case "SET_CONFIG": case "SET_CONFIG":
return action.config; return action.config;
......
import { filter } from ".";
import type { SyncUpdateAction } from "./sync";
import type { Sounds } from "../../assets/sounds/Audio";
import type { Theme, ThemeOptions } from "../../context/Theme"; import type { Theme, ThemeOptions } from "../../context/Theme";
import { setEmojiPack } from "../../components/common/Emoji"; import { setEmojiPack } from "../../components/common/Emoji";
import type { Sounds } from "../../assets/sounds/Audio";
import type { SyncUpdateAction } from "./sync";
export type SoundOptions = { export type SoundOptions = {
[key in Sounds]?: boolean [key in Sounds]?: boolean;
} };
export const DEFAULT_SOUNDS: SoundOptions = { export const DEFAULT_SOUNDS: SoundOptions = {
message: true, message: true,
outbound: false, outbound: false,
call_join: true, call_join: true,
call_leave: true call_leave: true,
}; };
export interface NotificationOptions { export interface NotificationOptions {
desktopEnabled?: boolean; desktopEnabled?: boolean;
sounds?: SoundOptions sounds?: SoundOptions;
} }
export type EmojiPacks = "mutant" | "twemoji" | "noto" | "openmoji"; export type EmojiPacks = "mutant" | "twemoji" | "noto" | "openmoji";
...@@ -56,16 +57,16 @@ export type SettingsAction = ...@@ -56,16 +57,16 @@ export type SettingsAction =
export function settings( export function settings(
state = {} as Settings, state = {} as Settings,
action: SettingsAction action: SettingsAction,
): Settings { ): Settings {
setEmojiPack(state.appearance?.emojiPack ?? 'mutant'); setEmojiPack(state.appearance?.emojiPack ?? "mutant");
switch (action.type) { switch (action.type) {
case "SETTINGS_SET_THEME": case "SETTINGS_SET_THEME":
return { return {
...state, ...state,
theme: { theme: {
...filter(state.theme, ["custom", "preset"]), ...state.theme,
...action.theme, ...action.theme,
}, },
}; };
...@@ -92,7 +93,7 @@ export function settings( ...@@ -92,7 +93,7 @@ export function settings(
return { return {
...state, ...state,
appearance: { appearance: {
...filter(state.appearance, ["emojiPack"]), ...state.appearance,
...action.options, ...action.options,
}, },
}; };
......
import type { AppearanceOptions } from "./settings";
import type { Language } from "../../context/Locale"; import type { Language } from "../../context/Locale";
import type { ThemeOptions } from "../../context/Theme"; import type { ThemeOptions } from "../../context/Theme";
import type { Notifications } from "./notifications"; import type { Notifications } from "./notifications";
import type { AppearanceOptions } from "./settings";
export type SyncKeys = "theme" | "appearance" | "locale" | "notifications"; export type SyncKeys = "theme" | "appearance" | "locale" | "notifications";
...@@ -16,7 +17,7 @@ export const DEFAULT_ENABLED_SYNC: SyncKeys[] = [ ...@@ -16,7 +17,7 @@ export const DEFAULT_ENABLED_SYNC: SyncKeys[] = [
"theme", "theme",
"appearance", "appearance",
"locale", "locale",
"notifications" "notifications",
]; ];
export interface SyncOptions { export interface SyncOptions {
disabled?: SyncKeys[]; disabled?: SyncKeys[];
...@@ -49,7 +50,7 @@ export type SyncAction = ...@@ -49,7 +50,7 @@ export type SyncAction =
export function sync( export function sync(
state = {} as SyncOptions, state = {} as SyncOptions,
action: SyncAction action: SyncAction,
): SyncOptions { ): SyncOptions {
switch (action.type) { switch (action.type) {
case "SYNC_DISABLE_KEY": 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 type { Sync } from "revolt.js/dist/api/objects"; import type { ChannelUnread } from "revolt-api/types/Sync";
export interface Unreads { export interface Unreads {
[key: string]: Partial<Omit<Sync.ChannelUnread, "_id">>; [key: string]: Partial<Omit<ChannelUnread, "_id">>;
} }
export type UnreadsAction = export type UnreadsAction =
...@@ -13,7 +13,7 @@ export type UnreadsAction = ...@@ -13,7 +13,7 @@ export type UnreadsAction =
} }
| { | {
type: "UNREADS_SET"; type: "UNREADS_SET";
unreads: Sync.ChannelUnread[]; unreads: ChannelUnread[];
} }
| { | {
type: "UNREADS_MENTION"; type: "UNREADS_MENTION";
......
export const REPO_URL = 'https://gitlab.insrt.uk/revolt/revite/-/commit'; /* eslint-disable */
export const GIT_REVISION = '__GIT_REVISION__'; // Strings needs to be explictly stated here as they can cause type issues elsewhere.
export const GIT_BRANCH: string = '__GIT_BRANCH__';
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 { .preact-context-menu .context-menu {
z-index: 100; z-index: 5000;
min-width: 180px; min-width: 190px;
padding: 6px 8px; padding: 6px 8px;
user-select: none; user-select: none;
border-radius: 4px; font-size: .875rem;
color: var(--secondary-foreground); color: var(--secondary-foreground);
border-radius: var(--border-radius);
background: var(--primary-background) !important; background: var(--primary-background) !important;
box-shadow: 0px 0px 8px 8px rgba(0, 0, 0, 0.05); box-shadow: 0px 0px 8px 8px rgba(0, 0, 0, 0.05);
> span { > span, .main > span {
gap: 6px; gap: 6px;
margin: 2px 0; margin: 2px 0;
display: flex; display: flex;
padding: 6px 8px; padding: 6px 8px;
border-radius: 3px; border-radius: 3px;
font-size: .875rem;
align-items: center; align-items: center;
white-space: nowrap; white-space: nowrap;
...@@ -33,6 +33,41 @@ ...@@ -33,6 +33,41 @@
color: var(--tertiary-foreground); 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 { .indicator {
width: 8px; width: 8px;
......