@gryt/core 0.8.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 CHANGED
@@ -9,3 +9,4 @@ export { cellState, CUSTOM_VALUE, describeRules, EVERYONE_VALUE, indexRules, nex
9
9
  export { groupMembersByRole, OFFLINE_GROUP_KEY, UNGROUPED_GROUP_KEY, type GroupableMember, type GroupableRole, type MemberGroup, } from "./members/groups.js";
10
10
  export { senderStreamId } from "./webrtc/senderStreamId.js";
11
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
@@ -11,3 +11,4 @@ export { cellState, CUSTOM_VALUE, describeRules, EVERYONE_VALUE, indexRules, nex
11
11
  export { groupMembersByRole, OFFLINE_GROUP_KEY, UNGROUPED_GROUP_KEY, } from "./members/groups.js";
12
12
  export { senderStreamId } from "./webrtc/senderStreamId.js";
13
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,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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gryt/core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Gryt's shared application logic: one implementation of the things the desktop and mobile apps both do.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {