@gryt/core 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/permissions/folderRules.d.ts +10 -0
- package/dist/permissions/folderRules.js +29 -0
- package/dist/terms/termsAgreement.d.ts +28 -0
- package/dist/terms/termsAgreement.js +125 -0
- package/dist/webrtc/senderStreamId.d.ts +5 -0
- package/dist/webrtc/senderStreamId.js +16 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,6 @@ export { inviteLink, isPublicHost, normalizeCode, normalizeHost, parseServerInpu
|
|
|
7
7
|
export { NAME_POOL, pickRandomName } from "./profile/randomName.js";
|
|
8
8
|
export { cellState, CUSTOM_VALUE, describeRules, EVERYONE_VALUE, indexRules, nextCellState, scopeChoiceFromValue, scopeChoiceValue, scopeOptions, scopeSetPayload, withCell, type CellState, type ChannelRule, type RuleEffect, type ScopeChoice, } from "./permissions/channelRules.js";
|
|
9
9
|
export { groupMembersByRole, OFFLINE_GROUP_KEY, UNGROUPED_GROUP_KEY, type GroupableMember, type GroupableRole, type MemberGroup, } from "./members/groups.js";
|
|
10
|
+
export { senderStreamId } from "./webrtc/senderStreamId.js";
|
|
11
|
+
export { describeFolderRules, folderFollowNote, folderPhrase, } from "./permissions/folderRules.js";
|
|
12
|
+
export { agreementAt, createTermsGate, GUIDELINES_URL, TERMS_STORAGE_KEY, TERMS_URL, TERMS_VERSION, } from "./terms/termsAgreement.js";
|
package/dist/index.js
CHANGED
|
@@ -9,3 +9,6 @@ export { inviteLink, isPublicHost, normalizeCode, normalizeHost, parseServerInpu
|
|
|
9
9
|
export { NAME_POOL, pickRandomName } from "./profile/randomName.js";
|
|
10
10
|
export { cellState, CUSTOM_VALUE, describeRules, EVERYONE_VALUE, indexRules, nextCellState, scopeChoiceFromValue, scopeChoiceValue, scopeOptions, scopeSetPayload, withCell, } from "./permissions/channelRules.js";
|
|
11
11
|
export { groupMembersByRole, OFFLINE_GROUP_KEY, UNGROUPED_GROUP_KEY, } from "./members/groups.js";
|
|
12
|
+
export { senderStreamId } from "./webrtc/senderStreamId.js";
|
|
13
|
+
export { describeFolderRules, folderFollowNote, folderPhrase, } from "./permissions/folderRules.js";
|
|
14
|
+
export { agreementAt, createTermsGate, GUIDELINES_URL, TERMS_STORAGE_KEY, TERMS_URL, TERMS_VERSION, } from "./terms/termsAgreement.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChannelRule } from "./channelRules.js";
|
|
2
|
+
/** "the X folder", or "its folder" when nothing is named. */
|
|
3
|
+
export declare function folderPhrase(folderName: string | null): string;
|
|
4
|
+
/** Shown once a channel has permissions of its own instead of following its folder. */
|
|
5
|
+
export declare function folderFollowNote(folderName: string | null): string;
|
|
6
|
+
/**
|
|
7
|
+
* The folder's half of `describeRules`: reading denied here hides the folder
|
|
8
|
+
* along with every channel in it that follows it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function describeFolderRules(rules: ChannelRule[], roleNames: Map<string, string>): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** "the X folder", or "its folder" when nothing is named. */
|
|
2
|
+
export function folderPhrase(folderName) {
|
|
3
|
+
return folderName ? `the ${folderName} folder` : "its folder";
|
|
4
|
+
}
|
|
5
|
+
/** Shown once a channel has permissions of its own instead of following its folder. */
|
|
6
|
+
export function folderFollowNote(folderName) {
|
|
7
|
+
return `Has its own permissions instead of ${folderPhrase(folderName)}'s.`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The folder's half of `describeRules`: reading denied here hides the folder
|
|
11
|
+
* along with every channel in it that follows it.
|
|
12
|
+
*/
|
|
13
|
+
export function describeFolderRules(rules, roleNames) {
|
|
14
|
+
if (rules.length === 0) {
|
|
15
|
+
return "Everyone on the server can see and use the channels in this folder.";
|
|
16
|
+
}
|
|
17
|
+
const hidden = rules
|
|
18
|
+
.filter((r) => r.permission === "read_messages" && r.effect === "deny")
|
|
19
|
+
.map((r) => roleNames.get(r.roleId) ?? r.roleId);
|
|
20
|
+
const others = rules.filter((r) => r.permission !== "read_messages").length;
|
|
21
|
+
if (hidden.length === 0) {
|
|
22
|
+
return `${others} change${others === 1 ? "" : "s"} to what roles can do in this folder's channels.`;
|
|
23
|
+
}
|
|
24
|
+
const list = hidden.length === 1
|
|
25
|
+
? hidden[0]
|
|
26
|
+
: `${hidden.slice(0, -1).join(", ")} and ${hidden[hidden.length - 1]}`;
|
|
27
|
+
const rest = others > 0 ? `, and ${others} other change${others === 1 ? "" : "s"}` : "";
|
|
28
|
+
return `${list} won't see this folder or anything in it${rest}.`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const TERMS_URL = "https://gryt.chat/terms";
|
|
2
|
+
export declare const GUIDELINES_URL = "https://gryt.chat/community-guidelines";
|
|
3
|
+
export declare const TERMS_VERSION = "2026-09-03";
|
|
4
|
+
export declare const TERMS_STORAGE_KEY = "gryt.termsAgreement";
|
|
5
|
+
export interface TermsAgreement {
|
|
6
|
+
version: string;
|
|
7
|
+
agreedAt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TermsStorage {
|
|
10
|
+
read: () => string | null | Promise<string | null>;
|
|
11
|
+
write: (value: string) => void | Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/** What storage held, or null when nothing in it is an agreement. */
|
|
14
|
+
export declare function parseAgreement(raw: string | null | undefined): TermsAgreement | null;
|
|
15
|
+
/** Agreeing to newer terms covers older ones, so a downgrade doesn't ask twice. */
|
|
16
|
+
export declare function coversTerms(agreement: TermsAgreement | null, version?: string): boolean;
|
|
17
|
+
export declare function agreementAt(now: Date, version?: string): TermsAgreement;
|
|
18
|
+
export interface TermsGate {
|
|
19
|
+
/** Runs `post` once this device has agreed: now, or after the person presses Agree. */
|
|
20
|
+
postAfterAgreeing: (post: () => void) => void;
|
|
21
|
+
agree: () => void;
|
|
22
|
+
decline: () => void;
|
|
23
|
+
/** Reads storage ahead of the first post, so that one doesn't wait for it. */
|
|
24
|
+
load: () => void;
|
|
25
|
+
asking: () => boolean;
|
|
26
|
+
subscribe: (listener: () => void) => () => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createTermsGate(storage: TermsStorage, version?: string, now?: () => Date): TermsGate;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export const TERMS_URL = "https://gryt.chat/terms";
|
|
2
|
+
export const GUIDELINES_URL = "https://gryt.chat/community-guidelines";
|
|
3
|
+
/* The newer of the two pages' "Last updated" dates. Moving it asks everybody again,
|
|
4
|
+
so move it when a change to either page should. */
|
|
5
|
+
export const TERMS_VERSION = "2026-09-03";
|
|
6
|
+
export const TERMS_STORAGE_KEY = "gryt.termsAgreement";
|
|
7
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
8
|
+
/** What storage held, or null when nothing in it is an agreement. */
|
|
9
|
+
export function parseAgreement(raw) {
|
|
10
|
+
if (!raw)
|
|
11
|
+
return null;
|
|
12
|
+
let value;
|
|
13
|
+
try {
|
|
14
|
+
value = JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (!value || typeof value !== "object")
|
|
20
|
+
return null;
|
|
21
|
+
const { version, agreedAt } = value;
|
|
22
|
+
if (typeof version !== "string" || !DAY.test(version))
|
|
23
|
+
return null;
|
|
24
|
+
if (typeof agreedAt !== "string")
|
|
25
|
+
return null;
|
|
26
|
+
return { version, agreedAt };
|
|
27
|
+
}
|
|
28
|
+
/** Agreeing to newer terms covers older ones, so a downgrade doesn't ask twice. */
|
|
29
|
+
export function coversTerms(agreement, version = TERMS_VERSION) {
|
|
30
|
+
return agreement !== null && agreement.version >= version;
|
|
31
|
+
}
|
|
32
|
+
export function agreementAt(now, version = TERMS_VERSION) {
|
|
33
|
+
return { version, agreedAt: now.toISOString() };
|
|
34
|
+
}
|
|
35
|
+
export function createTermsGate(storage, version = TERMS_VERSION, now = () => new Date()) {
|
|
36
|
+
let agreed = false;
|
|
37
|
+
let queued = null;
|
|
38
|
+
let asked = null;
|
|
39
|
+
let reading = false;
|
|
40
|
+
const listeners = new Set();
|
|
41
|
+
const emit = () => {
|
|
42
|
+
for (const listener of listeners)
|
|
43
|
+
listener();
|
|
44
|
+
};
|
|
45
|
+
const settle = (raw) => {
|
|
46
|
+
if (coversTerms(parseAgreement(raw), version))
|
|
47
|
+
agreed = true;
|
|
48
|
+
};
|
|
49
|
+
const flush = () => {
|
|
50
|
+
const post = queued;
|
|
51
|
+
queued = null;
|
|
52
|
+
if (!post)
|
|
53
|
+
return;
|
|
54
|
+
if (agreed) {
|
|
55
|
+
post();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
asked = post;
|
|
59
|
+
emit();
|
|
60
|
+
};
|
|
61
|
+
// One read at a time, and only the newest post waits on it: two taps must not send twice.
|
|
62
|
+
const refresh = () => {
|
|
63
|
+
if (reading)
|
|
64
|
+
return;
|
|
65
|
+
let raw;
|
|
66
|
+
try {
|
|
67
|
+
raw = storage.read();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
raw = null;
|
|
71
|
+
}
|
|
72
|
+
if (raw === null || typeof raw === "string") {
|
|
73
|
+
settle(raw);
|
|
74
|
+
flush();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
reading = true;
|
|
78
|
+
raw
|
|
79
|
+
.then(settle, () => undefined)
|
|
80
|
+
.then(() => {
|
|
81
|
+
reading = false;
|
|
82
|
+
flush();
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
postAfterAgreeing(post) {
|
|
87
|
+
if (agreed) {
|
|
88
|
+
post();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
queued = post;
|
|
92
|
+
refresh();
|
|
93
|
+
},
|
|
94
|
+
agree() {
|
|
95
|
+
agreed = true;
|
|
96
|
+
const post = asked;
|
|
97
|
+
asked = null;
|
|
98
|
+
emit();
|
|
99
|
+
try {
|
|
100
|
+
void Promise.resolve(storage.write(JSON.stringify(agreementAt(now(), version)))).catch(() => undefined);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Holds until the app restarts, and then it asks again.
|
|
104
|
+
}
|
|
105
|
+
post?.();
|
|
106
|
+
},
|
|
107
|
+
decline() {
|
|
108
|
+
if (!asked)
|
|
109
|
+
return;
|
|
110
|
+
asked = null;
|
|
111
|
+
emit();
|
|
112
|
+
},
|
|
113
|
+
load() {
|
|
114
|
+
if (!agreed)
|
|
115
|
+
refresh();
|
|
116
|
+
},
|
|
117
|
+
asking: () => asked !== null,
|
|
118
|
+
subscribe(listener) {
|
|
119
|
+
listeners.add(listener);
|
|
120
|
+
return () => {
|
|
121
|
+
listeners.delete(listener);
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stream id for a role: the first one given on this connection, kept in place
|
|
3
|
+
* across a `replaceTrack` swap. With no connection, `streamId` is returned unchanged.
|
|
4
|
+
*/
|
|
5
|
+
export declare function senderStreamId<Role extends string>(pc: object | null | undefined, role: Role, streamId: string): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Keyed by connection, so a reconnect — a new connection object — starts over. */
|
|
2
|
+
const byConnection = new WeakMap();
|
|
3
|
+
/**
|
|
4
|
+
* The stream id for a role: the first one given on this connection, kept in place
|
|
5
|
+
* across a `replaceTrack` swap. With no connection, `streamId` is returned unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export function senderStreamId(pc, role, streamId) {
|
|
8
|
+
if (!pc)
|
|
9
|
+
return streamId;
|
|
10
|
+
let ids = byConnection.get(pc);
|
|
11
|
+
if (!ids) {
|
|
12
|
+
ids = {};
|
|
13
|
+
byConnection.set(pc, ids);
|
|
14
|
+
}
|
|
15
|
+
return (ids[role] ??= streamId);
|
|
16
|
+
}
|
package/package.json
CHANGED