feat: esm + unreads util

This commit is contained in:
Paul
2021-12-23 19:34:17 +00:00
parent e324007567
commit ed42409fd4
10 changed files with 284 additions and 67 deletions
+3 -1
View File
@@ -2,11 +2,13 @@
![revolt.js](https://img.shields.io/npm/v/revolt.js) ![revolt-api](https://img.shields.io/npm/v/revolt-api?label=Revolt%20API)
**revolt.js** is a direct implementation of the entire Revolt API and provides a way to authenticate and start communicating with Revolt servers.
**revolt.js** is a direct implementation of the entire Revolt API and provides a way to authenticate and start communicating with Revolt servers. **This is an ESM library!**
## Example Usage
```typescript
import { Client } from "revolt.js";
let client = new Client();
client.on("ready", async () =>
+6 -2
View File
@@ -1,6 +1,7 @@
{
"type": "module",
"name": "revolt.js",
"version": "5.1.0-alpha.15",
"version": "5.2.0",
"main": "dist/index.js",
"repository": "https://github.com/revoltchat/revolt.js",
"author": "Paul Makles <insrt.uk>",
@@ -11,6 +12,7 @@
"exponential-backoff": "^3.1.0",
"isomorphic-ws": "^4.0.1",
"lodash.defaultsdeep": "^4.6.1",
"lodash.flatten": "^4.4.0",
"lodash.isequal": "^4.5.0",
"mobx": "^6.3.2",
"revolt-api": "^0.5.3-alpha.9",
@@ -21,6 +23,7 @@
"@types/events": "^3.0.0",
"@types/lodash": "^4.14.168",
"@types/lodash.defaultsdeep": "^4.6.6",
"@types/lodash.flatten": "^4.4.6",
"@types/lodash.isequal": "^4.5.5",
"@types/node": "^14.14.31",
"@types/ws": "^7.2.1",
@@ -28,6 +31,7 @@
"@typescript-eslint/parser": "^5.3.1",
"dotenv": "^8.2.0",
"eslint": "^8.2.0",
"esm": "^3.2.25",
"gulp": "^4.0.2",
"gulp-typedoc": "^3.0.1",
"in-publish": "^2.0.1",
@@ -68,7 +72,7 @@
}
},
"scripts": {
"dev": "tsc-watch --onSuccess \"node dist/tester\"",
"dev": "tsc-watch --onSuccess \"node --experimental-specifier-resolution=node dist/tester\"",
"prepublish": "in-publish && tsc || echo Skipping build.",
"build": "rimraf dist && tsc",
"build:watch": "tsc-watch",
+24 -6
View File
@@ -1,7 +1,7 @@
import Axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { EventEmitter } from "eventemitter3";
import EventEmitter from "eventemitter3";
import defaultsDeep from "lodash.defaultsdeep";
import { action, makeObservable, observable } from "mobx";
import { action, makeObservable, observable, runInAction } from "mobx";
import type { Session } from "revolt-api/types/Auth";
import type { SizeOptions } from "revolt-api/types/Autumn";
import type { RevoltConfiguration } from "revolt-api/types/Core";
@@ -21,6 +21,7 @@ import { ClientboundNotification } from "./websocket/notifications";
import { defaultConfig } from "./config";
import { Nullable } from "./util/null";
import Unreads from "./util/Unreads";
/**
* Client options object
@@ -28,7 +29,9 @@ import { Nullable } from "./util/null";
export interface ClientOptions {
apiURL: string;
debug: boolean;
cache: boolean;
unreads: boolean;
heartbeat: number;
autoReconnect: boolean;
@@ -108,6 +111,8 @@ export class Client extends EventEmitter {
messages: Messages;
bots: Bots;
unreads?: Unreads;
constructor(options: Partial<ClientOptions> = {}) {
super();
@@ -137,6 +142,10 @@ export class Client extends EventEmitter {
this.options = defaultsDeep(options, defaultConfig);
if (this.options.cache) throw "Cache is not supported yet.";
if (this.options.unreads) {
this.unreads = new Unreads(this);
}
this.Axios = Axios.create({ baseURL: this.apiURL });
this.websocket = new WebSocketClient(this);
this.heartbeat = this.options.heartbeat;
@@ -161,14 +170,23 @@ export class Client extends EventEmitter {
});
}
this.on("ready", () => {
if (this.options.unreads) {
this.unreads!.sync();
}
});
this.on("message", async (message) => {
const channel = message.channel;
if (!channel) return;
if (channel.channel_type === "DirectMessage") {
channel.active = true;
}
channel.last_message_id = message._id;
runInAction(() => {
if (channel.channel_type === "DirectMessage") {
channel.active = true;
}
channel.last_message_id = message._id;
});
});
}
+2
View File
@@ -5,7 +5,9 @@ export const defaultConfig = {
autoReconnect: true,
heartbeat: 30,
debug: false,
cache: false,
unreads: false,
ackRateLimiter: true,
};
+42
View File
@@ -20,6 +20,7 @@ import {
U32_MAX,
UserPermission,
} from "../api/permissions";
import { INotificationChecker } from "../util/Unreads";
export class Channel {
client: Client;
@@ -146,6 +147,13 @@ export class Channel {
return this.client.messages.get(id);
}
/**
* Get the last message ID if it is present or the origin timestamp.
*/
get last_message_id_or_past() {
return this.last_message_id ?? "0";
}
/**
* Group recipients.
* @requires `Group`
@@ -170,6 +178,40 @@ export class Channel {
return decodeTime(this._id);
}
@computed isUnread(permit: INotificationChecker) {
if (permit.isMuted(this)) return false;
return this.unread;
}
@computed getMentions(permit: INotificationChecker) {
if (permit.isMuted(this)) return [];
return this.mentions;
}
/**
* Get whether this channel is unread.
*/
get unread() {
if (!this.last_message_id
|| this.channel_type === 'SavedMessages'
|| this.channel_type === 'VoiceChannel') return false;
return (
(
this.client.unreads?.getUnread(this._id)?.last_id ?? "0"
).localeCompare(this.last_message_id) === -1
);
}
/**
* Get mentions in this channel for user.
*/
get mentions() {
if (this.channel_type === 'SavedMessages'
|| this.channel_type === 'VoiceChannel') return [];
return this.client.unreads?.getUnread(this._id)?.mentions ?? [];
}
constructor(client: Client, data: ChannelI) {
this.client = client;
+16
View File
@@ -17,6 +17,7 @@ import Collection from "./Collection";
import { User } from "./Users";
import { Client, FileArgs } from "..";
import { decodeTime } from "ulid";
import { INotificationChecker } from "../util/Unreads";
export class Server {
client: Client;
@@ -50,6 +51,21 @@ export class Server {
return decodeTime(this._id);
}
@computed isUnread(permit?: INotificationChecker) {
if (permit?.isMuted(this)) return false;
return this.channels.find((channel) =>
!permit?.isMuted(channel) && channel?.unread);
}
@computed getMentions(permit?: INotificationChecker) {
if (permit?.isMuted(this)) return [];
const arr = this.channels
.filter(channel => !permit?.isMuted(channel))
.map((channel) => channel?.mentions) as string[][];
return arr[0].concat(...arr.slice(1));
}
constructor(client: Client, data: ServerI) {
this.client = client;
+113
View File
@@ -0,0 +1,113 @@
import { Client } from "../Client";
import {
action,
computed,
makeAutoObservable,
ObservableMap,
runInAction,
} from "mobx";
import type { ChannelUnread } from "revolt-api/types/Sync";
import { ulid } from "ulid";
import { Channel } from "../maps/Channels";
import { Server } from "../maps/Servers";
export interface INotificationChecker {
isMuted(target?: Channel | Server): boolean;
}
/**
* Handles channel unreads.
*/
export default class Unreads {
private client: Client;
private channels: ObservableMap<string, Omit<ChannelUnread, "_id">>;
/**
* Construct new Unreads store.
*/
constructor(client: Client) {
this.channels = new ObservableMap();
makeAutoObservable(this);
this.client = client;
}
/**
* Sync unread data from the server.
*/
async sync() {
const unreads = await this.client.syncFetchUnreads();
runInAction(() => {
for (const unread of unreads) {
const { _id, ...data } = unread;
this.channels.set(_id.channel, data);
}
});
}
/**
* Get channel unread object for a given channel.
* @param channel_id Target channel ID
* @returns Partial channel unread object
*/
@computed getUnread(channel_id: string) {
return this.channels.get(channel_id);
}
/**
* Mark a channel as unread by setting a custom last_id.
* @param channel_id Target channel ID
* @param last_id New last ID
*/
@action markUnread(channel_id: string, last_id: string) {
this.channels.set(channel_id, {
...this.getUnread(channel_id),
last_id,
});
}
/**
* Add a mention to a channel unread.
* @param channel_id Target channel ID
* @param message_id Target message ID
*/
@action markMention(channel_id: string, message_id: string) {
const unread = this.getUnread(channel_id);
this.channels.set(channel_id, {
last_id: "0",
...unread,
mentions: [...(unread?.mentions ?? []), message_id],
});
}
/**
* Mark a channel as read.
* @param channel_id Target channel ID
* @param message_id Target message ID (last sent in channel)
* @param emit Whether to emit to server
* @param skipRateLimiter Whether to skip the rate limiter
*/
@action markRead(
channel_id: string,
message_id?: string,
emit = false,
skipRateLimiter = false,
) {
const last_id = message_id ?? ulid();
this.channels.set(channel_id, { last_id });
if (emit) {
this.client.channels.get(channel_id)?.ack(last_id, skipRateLimiter);
}
}
/**
* Mark multiple channels as read.
* @param channel_ids Target channel IDs
*/
@action markMultipleRead(channel_ids: string[]) {
const last_id = ulid();
for (const channel_id of channel_ids) {
this.channels.set(channel_id, { last_id });
}
}
}
+6 -1
View File
@@ -445,8 +445,13 @@ export class WebSocketClient {
break;
}
case "ChannelAck":
case "ChannelAck": {
this.client.unreads?.markRead(
packet.id,
packet.message_id,
);
break;
}
case "Pong": {
this.ping = +new Date() - packet.data;
+55 -57
View File
@@ -1,63 +1,61 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
"compilerOptions": {
/* Basic Options */
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "ES6" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true /* Generates corresponding '.d.ts' file. */,
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */,
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"include": [
"src/*"
]
}
/* Experimental Options */
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"include": ["src/*"]
}
+17
View File
@@ -69,6 +69,13 @@
dependencies:
"@types/lodash" "*"
"@types/lodash.flatten@^4.4.6":
version "4.4.6"
resolved "https://registry.yarnpkg.com/@types/lodash.flatten/-/lodash.flatten-4.4.6.tgz#b74c3267c87e44e603137d4621e8a9396b6551f5"
integrity sha512-omCBl4M8EJSmf2DZqh4/zwjgXQpzC7YO/PXTcG8rI9r7xun8CohrHeNx8HZRkqWc61uJfIaZop9MwJEXPVssHw==
dependencies:
"@types/lodash" "*"
"@types/lodash.isequal@^4.5.5":
version "4.5.5"
resolved "https://registry.yarnpkg.com/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz#4fed1b1b00bef79e305de0352d797e9bb816c8ff"
@@ -912,6 +919,11 @@ eslint@^8.2.0:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
esm@^3.2.25:
version "3.2.25"
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
espree@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.0.0.tgz#e90a2965698228502e771c7a58489b1a9d107090"
@@ -1920,6 +1932,11 @@ lodash.defaultsdeep@^4.6.1:
resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6"
integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==
lodash.flatten@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"