@marktiderman/genesis-chat 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +163 -2
- package/dist/chunk-27UTR5FW.js +322 -0
- package/dist/chunk-27UTR5FW.js.map +1 -0
- package/dist/chunk-JQXZUEEL.js +525 -0
- package/dist/chunk-JQXZUEEL.js.map +1 -0
- package/dist/chunk-SAY24E6V.js +309 -0
- package/dist/chunk-SAY24E6V.js.map +1 -0
- package/dist/client-BtdALFCx.d.ts +349 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +2 -0
- package/dist/native.js +3 -0
- package/dist/native.js.map +1 -0
- package/dist/react.d.ts +137 -0
- package/dist/react.js +5 -0
- package/dist/react.js.map +1 -0
- package/dist/server.d.ts +311 -0
- package/dist/server.js +562 -0
- package/dist/server.js.map +1 -0
- package/dist/web.d.ts +387 -0
- package/dist/web.js +2405 -0
- package/dist/web.js.map +1 -0
- package/migrations/0001_genesis_chat.sql +1836 -0
- package/migrations/tests/0001_genesis_chat.test.sql +254 -0
- package/package.json +144 -3
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
-- @feat:chat
|
|
2
|
+
-- pgTAP test for 0001_genesis_chat.sql.
|
|
3
|
+
-- Copy into your app's `supabase/tests/database/` and run `supabase test db`.
|
|
4
|
+
-- It writes only its own users, kinds and conversations, and rolls everything back.
|
|
5
|
+
--
|
|
6
|
+
-- People: a1 and b2 talk directly; c3 is in nobody's conversation; d4 is staff of the
|
|
7
|
+
-- test's "ticket" kind (an observer who may post); e5 leaves a group.
|
|
8
|
+
begin;
|
|
9
|
+
create extension if not exists pgtap with schema extensions;
|
|
10
|
+
select plan(79);
|
|
11
|
+
|
|
12
|
+
insert into auth.users (id, aud, role, email, instance_id)
|
|
13
|
+
select ('00000000-0000-4000-8000-00000000c0' || n)::uuid, 'authenticated', 'authenticated', 'genesis-chat-' || n || '@example.test', '00000000-0000-0000-0000-000000000000'
|
|
14
|
+
from unnest(array['a1', 'b2', 'c3', 'd4', 'e5']) n;
|
|
15
|
+
|
|
16
|
+
create function pg_temp.person(n text) returns uuid language sql immutable as $$ select ('00000000-0000-4000-8000-00000000c0' || n)::uuid $$;
|
|
17
|
+
create function pg_temp.sign_in(n text) returns void language plpgsql as $$
|
|
18
|
+
begin
|
|
19
|
+
perform set_config('request.jwt.claims', json_build_object('sub', pg_temp.person(n), 'role', 'authenticated')::text, true);
|
|
20
|
+
perform set_config('request.jwt.claim.sub', pg_temp.person(n)::text, true);
|
|
21
|
+
perform set_config('role', 'authenticated', true);
|
|
22
|
+
end $$;
|
|
23
|
+
create function pg_temp.as_anon() returns void language plpgsql as $$
|
|
24
|
+
begin
|
|
25
|
+
perform set_config('request.jwt.claims', '{"role":"anon"}', true);
|
|
26
|
+
perform set_config('request.jwt.claim.sub', '', true);
|
|
27
|
+
perform set_config('role', 'anon', true);
|
|
28
|
+
end $$;
|
|
29
|
+
create function pg_temp.as_server() returns void language plpgsql as $$
|
|
30
|
+
begin
|
|
31
|
+
perform set_config('request.jwt.claims', '{"role":"service_role"}', true);
|
|
32
|
+
perform set_config('request.jwt.claim.sub', '', true);
|
|
33
|
+
perform set_config('role', 'service_role', true);
|
|
34
|
+
end $$;
|
|
35
|
+
grant execute on function pg_temp.person(text), pg_temp.sign_in(text), pg_temp.as_anon(), pg_temp.as_server() to anon, authenticated, service_role;
|
|
36
|
+
|
|
37
|
+
create temp table t (k text primary key, v uuid);
|
|
38
|
+
grant select, insert, update on t to authenticated, service_role;
|
|
39
|
+
create function pg_temp.v(k_ text) returns uuid language sql stable as $$ select v from t where k = k_ $$;
|
|
40
|
+
grant execute on function pg_temp.v(text) to authenticated, service_role;
|
|
41
|
+
|
|
42
|
+
-- The app's rules: anyone may message a1, b2 or x5 (not c3); staff (d4) observe tickets.
|
|
43
|
+
create function public.genesis_chat_test_may_start(member_ids uuid[], subject_type text, subject_id text, user_id uuid)
|
|
44
|
+
returns boolean language sql stable set search_path = '' as $$
|
|
45
|
+
select not (('00000000-0000-4000-8000-00000000c0c3')::uuid = any (member_ids))
|
|
46
|
+
$$;
|
|
47
|
+
create function public.genesis_chat_test_staff(conversation_id uuid, user_id uuid)
|
|
48
|
+
returns boolean language sql stable set search_path = '' as $$
|
|
49
|
+
select user_id = '00000000-0000-4000-8000-00000000c0d4'::uuid
|
|
50
|
+
$$;
|
|
51
|
+
select genesis_chat.register_kind('direct', 'Direct message', p_max_members => 2,
|
|
52
|
+
p_start_check => 'public.genesis_chat_test_may_start(uuid[], text, text, uuid)');
|
|
53
|
+
select genesis_chat.register_kind('group', 'Group', p_membership => 'admins',
|
|
54
|
+
p_start_check => 'public.genesis_chat_test_may_start(uuid[], text, text, uuid)');
|
|
55
|
+
select genesis_chat.register_kind('ticket', 'Support ticket', p_threads => false,
|
|
56
|
+
p_observer_check => 'public.genesis_chat_test_staff(uuid, uuid)', p_observers_post => true);
|
|
57
|
+
|
|
58
|
+
select throws_ok($$ select genesis_chat.register_kind('bad', 'Bad', p_observer_check => 'public.genesis_chat_test_may_start(uuid[], text, text, uuid)') $$,
|
|
59
|
+
'22023', null, 'an observer check must take (uuid, uuid)');
|
|
60
|
+
|
|
61
|
+
-- Direct conversations -----------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
select pg_temp.sign_in('a1');
|
|
64
|
+
insert into t select 'dm', genesis_chat.start_conversation('direct', array[pg_temp.person('b2')]);
|
|
65
|
+
select is(genesis_chat.start_conversation('direct', array[pg_temp.person('b2')]), pg_temp.v('dm'), 'a direct conversation is found again, not made twice');
|
|
66
|
+
select pg_temp.sign_in('b2');
|
|
67
|
+
select is(genesis_chat.start_conversation('direct', array[pg_temp.person('a1')]), pg_temp.v('dm'), 'from either side');
|
|
68
|
+
select pg_temp.sign_in('a1');
|
|
69
|
+
select throws_ok($$ select genesis_chat.start_conversation('direct', array[pg_temp.person('c3')]) $$, '42501', null,
|
|
70
|
+
'the start check decides who you may message');
|
|
71
|
+
select throws_ok($$ select genesis_chat.start_conversation('direct', array[pg_temp.person('b2'), pg_temp.person('e5')]) $$, '22023', null,
|
|
72
|
+
'a direct conversation has two people');
|
|
73
|
+
select throws_ok($$ select genesis_chat.start_conversation('ticket', array[pg_temp.person('b2')]) $$, '42501', null,
|
|
74
|
+
'a kind without a start check is started by the server only');
|
|
75
|
+
|
|
76
|
+
-- Sending ---------------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
insert into t values ('c1', gen_random_uuid());
|
|
79
|
+
select is((genesis_chat.send_message(pg_temp.v('dm'), pg_temp.v('c1'), 'Hi **b2**')).seq, 1::bigint, 'the first message is number 1');
|
|
80
|
+
insert into t select 'm1', id from genesis_chat.messages where client_id = pg_temp.v('c1');
|
|
81
|
+
select is((genesis_chat.send_message(pg_temp.v('dm'), pg_temp.v('c1'), 'Hi **b2**')).id, pg_temp.v('m1'), 'sending the same client_id again returns the first message');
|
|
82
|
+
select is((select count(*)::integer from genesis_chat.messages where conversation_id = pg_temp.v('dm')), 1, 'and posts nothing new');
|
|
83
|
+
select throws_ok($$ select genesis_chat.send_message(pg_temp.v('dm'), gen_random_uuid(), ' ') $$, '22023', null,
|
|
84
|
+
'a message needs text or an attachment');
|
|
85
|
+
select is((genesis_chat.send_message(pg_temp.v('dm'), gen_random_uuid(), 'Second', p_reply_to_id => pg_temp.v('m1'),
|
|
86
|
+
p_mentions => array[pg_temp.person('b2'), pg_temp.person('c3')])).mentions,
|
|
87
|
+
array[pg_temp.person('b2')], 'mentions keep only members');
|
|
88
|
+
select is((select last_read_seq from genesis_chat.members where conversation_id = pg_temp.v('dm') and user_id = pg_temp.person('a1')),
|
|
89
|
+
2::bigint, 'the sender has read their own messages');
|
|
90
|
+
select ok(exists (select 1 from genesis_chat.inbox() i where i.conversation_id = pg_temp.v('dm') and i.unread = 0
|
|
91
|
+
and i.other_user_id = pg_temp.person('b2')), 'the sender''s inbox shows nothing unread and the other person');
|
|
92
|
+
|
|
93
|
+
select pg_temp.sign_in('b2');
|
|
94
|
+
select is((select unread from genesis_chat.inbox() where conversation_id = pg_temp.v('dm')), 2, 'the other person has two unread');
|
|
95
|
+
select ok((select mentioned from genesis_chat.inbox() where conversation_id = pg_temp.v('dm')), 'and was mentioned');
|
|
96
|
+
select is(genesis_chat.mark_read(pg_temp.v('dm'), 1), 1::bigint, 'reading up to the first');
|
|
97
|
+
select is(genesis_chat.mark_read(pg_temp.v('dm'), 0), 1::bigint, 'read state never moves back');
|
|
98
|
+
select is(genesis_chat.mark_read(pg_temp.v('dm'), 99), 2::bigint, 'nor past the latest');
|
|
99
|
+
select is(genesis_chat.unread_total(), 0, 'nothing waits for them');
|
|
100
|
+
|
|
101
|
+
-- Who reads what -------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
select pg_temp.sign_in('c3');
|
|
104
|
+
select is((select count(*)::integer from genesis_chat.messages where conversation_id = pg_temp.v('dm')), 0, 'someone outside reads no messages');
|
|
105
|
+
select is((select count(*)::integer from genesis_chat.conversations where id = pg_temp.v('dm')), 0, 'nor the conversation');
|
|
106
|
+
select is((select count(*)::integer from genesis_chat.members where conversation_id = pg_temp.v('dm')), 0, 'nor who is in it');
|
|
107
|
+
select throws_ok($$ select genesis_chat.send_message(pg_temp.v('dm'), gen_random_uuid(), 'Let me in') $$, '42501', null, 'and can''t post');
|
|
108
|
+
select throws_ok($$ select genesis_chat.mark_read(pg_temp.v('dm')) $$, '42501', null, 'or mark it read');
|
|
109
|
+
select throws_ok($$ select genesis_chat.react(pg_temp.v('m1'), '👍') $$, '42501', null, 'or react');
|
|
110
|
+
select ok(not genesis_chat.can_join_topic('genesis-chat:conv:' || pg_temp.v('dm')), 'or join its live topic');
|
|
111
|
+
select ok(not genesis_chat.can_join_topic('genesis-chat:user:' || pg_temp.person('a1')), 'or anyone else''s inbox topic');
|
|
112
|
+
select ok(genesis_chat.can_join_topic('genesis-chat:user:' || pg_temp.person('c3')), 'but joins their own');
|
|
113
|
+
select ok(not genesis_chat.can_join_topic('genesis-chat:user:' || pg_temp.person('c3'), true), 'without writing to it');
|
|
114
|
+
select pg_temp.sign_in('a1');
|
|
115
|
+
select ok(genesis_chat.can_join_topic('genesis-chat:conv:' || pg_temp.v('dm'), true), 'a member signals typing on the conversation''s topic');
|
|
116
|
+
|
|
117
|
+
select pg_temp.as_anon();
|
|
118
|
+
select throws_ok($$ select count(*) from genesis_chat.messages $$, '42501', null, 'anon reaches nothing');
|
|
119
|
+
select lives_ok($$ select count(*) from storage.objects where bucket_id <> 'genesis-chat' $$, 'anon still reads other buckets: chat''s storage policies don''t apply to it');
|
|
120
|
+
|
|
121
|
+
-- Someone outside never passes a role check (my_role is null for them).
|
|
122
|
+
reset role;
|
|
123
|
+
insert into t select 'm2', id from genesis_chat.messages where conversation_id = pg_temp.v('dm') and seq = 2;
|
|
124
|
+
select pg_temp.sign_in('c3');
|
|
125
|
+
select throws_ok($$ select genesis_chat.update_conversation(pg_temp.v('dm'), 'Mine now') $$, '42501', null, 'someone outside can''t rename a conversation');
|
|
126
|
+
select throws_ok($$ select genesis_chat.set_status(pg_temp.v('dm'), 'solved') $$, '42501', null, 'or set its status');
|
|
127
|
+
select throws_ok($$ select genesis_chat.delete_message(pg_temp.v('m2')) $$,
|
|
128
|
+
'42501', null, 'or delete its messages');
|
|
129
|
+
select throws_ok($$ select genesis_chat.pin_message(pg_temp.v('m2')) $$,
|
|
130
|
+
'42501', null, 'or pin them');
|
|
131
|
+
reset role;
|
|
132
|
+
select is((select title from genesis_chat.conversations where id = pg_temp.v('dm')), null, 'and nothing changed');
|
|
133
|
+
|
|
134
|
+
-- Edits, reactions, deletes -----------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
select pg_temp.sign_in('a1');
|
|
137
|
+
select is((genesis_chat.edit_message(pg_temp.v('m1'), 'Hi again')).body, 'Hi again', 'the sender edits their message');
|
|
138
|
+
select pg_temp.sign_in('b2');
|
|
139
|
+
select throws_ok($$ select genesis_chat.edit_message(pg_temp.v('m1'), 'Not mine') $$, '42501', null, 'nobody else does');
|
|
140
|
+
select lives_ok($$ select genesis_chat.react(pg_temp.v('m1'), '🔥') $$, 'a member reacts');
|
|
141
|
+
select is((select count(*)::integer from genesis_chat.reactions where message_id = pg_temp.v('m1')), 1, 'and both see it');
|
|
142
|
+
select throws_ok($$ select genesis_chat.delete_message(pg_temp.v('m1')) $$, '42501', null, 'in a direct conversation, only the sender deletes');
|
|
143
|
+
select pg_temp.sign_in('a1');
|
|
144
|
+
select lives_ok($$ select genesis_chat.delete_message(pg_temp.v('m1')) $$, 'the sender deletes it');
|
|
145
|
+
select ok((select deleted_at is not null and body = '' from genesis_chat.messages where id = pg_temp.v('m1')),
|
|
146
|
+
'a placeholder stays, without its text');
|
|
147
|
+
select is((select count(*)::integer from genesis_chat.reactions where message_id = pg_temp.v('m1')), 0, 'and its reactions go');
|
|
148
|
+
|
|
149
|
+
-- Files ------------------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
select ok(genesis_chat.can_upload(pg_temp.v('dm') || '/' || pg_temp.person('a1') || '/photo-1.jpg'), 'a member uploads into their folder');
|
|
152
|
+
select ok(not genesis_chat.can_upload(pg_temp.v('dm') || '/' || pg_temp.person('b2') || '/photo-1.jpg'), 'not someone else''s');
|
|
153
|
+
select ok(not genesis_chat.can_upload(pg_temp.v('dm') || '/' || pg_temp.person('a1') || '/page.svg'), 'and never an SVG');
|
|
154
|
+
select pg_temp.sign_in('c3');
|
|
155
|
+
select ok(not genesis_chat.can_upload(pg_temp.v('dm') || '/' || pg_temp.person('c3') || '/photo-1.jpg'), 'someone outside uploads nothing');
|
|
156
|
+
reset role;
|
|
157
|
+
insert into storage.objects (bucket_id, name, owner_id) values
|
|
158
|
+
('genesis-chat', pg_temp.v('dm') || '/' || pg_temp.person('a1') || '/voice-1.m4a', pg_temp.person('a1')::text),
|
|
159
|
+
('genesis-chat', pg_temp.v('dm') || '/' || pg_temp.person('b2') || '/theirs.jpg', pg_temp.person('b2')::text);
|
|
160
|
+
|
|
161
|
+
select pg_temp.sign_in('a1');
|
|
162
|
+
select throws_ok($$ select genesis_chat.send_message(pg_temp.v('dm'), gen_random_uuid(), '',
|
|
163
|
+
jsonb_build_array(jsonb_build_object('kind', 'image', 'path', pg_temp.v('dm') || '/' || pg_temp.person('b2') || '/theirs.jpg'))) $$,
|
|
164
|
+
'22023', null, 'a message carries only files its sender uploaded');
|
|
165
|
+
insert into t select 'voice', (genesis_chat.send_message(pg_temp.v('dm'), gen_random_uuid(), '',
|
|
166
|
+
jsonb_build_array(jsonb_build_object('kind', 'voice', 'path', pg_temp.v('dm') || '/' || pg_temp.person('a1') || '/voice-1.m4a',
|
|
167
|
+
'mime', 'audio/mp4', 'duration_ms', 4200, 'waveform', jsonb_build_array(10, 80, 200, -5))))).id;
|
|
168
|
+
select is((select waveform from genesis_chat.attachments where message_id = pg_temp.v('voice')), array[10, 80, 100, 0]::smallint[],
|
|
169
|
+
'a voice note keeps its waveform, clamped to 0–100');
|
|
170
|
+
select is((select transcript_status from genesis_chat.attachments where message_id = pg_temp.v('voice')), 'pending', 'and waits for a transcript');
|
|
171
|
+
select pg_temp.sign_in('b2');
|
|
172
|
+
select ok(genesis_chat.can_read_object(pg_temp.v('dm') || '/' || pg_temp.person('a1') || '/voice-1.m4a', pg_temp.person('a1')::text, now()),
|
|
173
|
+
'the other member reads the file once it is sent');
|
|
174
|
+
select pg_temp.sign_in('c3');
|
|
175
|
+
select ok(not genesis_chat.can_read_object(pg_temp.v('dm') || '/' || pg_temp.person('a1') || '/voice-1.m4a', pg_temp.person('a1')::text, now()),
|
|
176
|
+
'someone outside never does');
|
|
177
|
+
|
|
178
|
+
-- Groups -------------------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
select pg_temp.sign_in('a1');
|
|
181
|
+
insert into t select 'grp', genesis_chat.start_conversation('group', array[pg_temp.person('b2'), pg_temp.person('e5')], 'Cohort');
|
|
182
|
+
select is(genesis_chat.my_role(pg_temp.v('grp')), 'owner', 'whoever starts a group owns it');
|
|
183
|
+
select genesis_chat.update_conversation(pg_temp.v('grp'), p_settings => '{"who_posts": "admins"}');
|
|
184
|
+
select pg_temp.sign_in('b2');
|
|
185
|
+
select ok(not genesis_chat.can_post(pg_temp.v('grp')), 'a one-way group: members read but don''t post');
|
|
186
|
+
select pg_temp.sign_in('e5');
|
|
187
|
+
select lives_ok($$ select genesis_chat.remove_member(pg_temp.v('grp')) $$, 'a member leaves a group');
|
|
188
|
+
select ok(not genesis_chat.can_view(pg_temp.v('grp')), 'and no longer reads it');
|
|
189
|
+
select pg_temp.sign_in('a1');
|
|
190
|
+
select throws_ok($$ select genesis_chat.add_members(pg_temp.v('grp'), array[pg_temp.person('c3')]) $$, '42501', null,
|
|
191
|
+
'the start check also decides who may be added');
|
|
192
|
+
|
|
193
|
+
-- Tickets: observers and internal notes ----------------------------------------------------
|
|
194
|
+
|
|
195
|
+
select pg_temp.as_server();
|
|
196
|
+
insert into t select 'tkt', genesis_chat.ensure_conversation('ticket', 'report', 'r-1', 'Login trouble');
|
|
197
|
+
select is(genesis_chat.ensure_conversation('ticket', 'report', 'r-1'), pg_temp.v('tkt'), 'the server finds a record''s conversation again');
|
|
198
|
+
select is(genesis_chat.sync_members(pg_temp.v('tkt'), array[pg_temp.person('b2')]), 1, 'and sets its members');
|
|
199
|
+
select pg_temp.sign_in('b2');
|
|
200
|
+
select lives_ok($$ select genesis_chat.send_message(pg_temp.v('tkt'), gen_random_uuid(), 'I can''t sign in') $$, 'the person writes in');
|
|
201
|
+
select throws_ok($$ select genesis_chat.send_message(pg_temp.v('tkt'), gen_random_uuid(), 'note', p_visibility => 'observers') $$, '42501', null,
|
|
202
|
+
'only observers write internal notes');
|
|
203
|
+
select pg_temp.sign_in('d4');
|
|
204
|
+
select is(genesis_chat.my_role(pg_temp.v('tkt')), 'observer', 'staff observe the ticket without being in it');
|
|
205
|
+
select lives_ok($$ select genesis_chat.send_message(pg_temp.v('tkt'), gen_random_uuid(), 'Looking now') $$, 'and reply');
|
|
206
|
+
select lives_ok($$ select genesis_chat.send_message(pg_temp.v('tkt'), gen_random_uuid(), 'Their session expired', p_visibility => 'observers') $$,
|
|
207
|
+
'and leave an internal note');
|
|
208
|
+
select is((select count(*)::integer from genesis_chat.observed('ticket')), 1, 'the ticket is in staff''s queue');
|
|
209
|
+
select pg_temp.sign_in('b2');
|
|
210
|
+
select is((select count(*)::integer from genesis_chat.messages where conversation_id = pg_temp.v('tkt')), 2, 'the person never sees the note');
|
|
211
|
+
select is((select unread from genesis_chat.inbox() where conversation_id = pg_temp.v('tkt')), 1, 'nor counts it as unread');
|
|
212
|
+
select pg_temp.sign_in('d4');
|
|
213
|
+
select ok(not genesis_chat.can_view(pg_temp.v('dm')), 'observing one kind opens nothing else');
|
|
214
|
+
|
|
215
|
+
-- The runner ------------------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
select pg_temp.as_server();
|
|
218
|
+
select ok(exists (select 1 from genesis_chat.push_targets(
|
|
219
|
+
(select id from genesis_chat.messages where conversation_id = pg_temp.v('dm') and seq = 2)) p
|
|
220
|
+
where p.user_id = pg_temp.person('b2') and p.mentioned), 'push goes to the other member, marked as mentioned');
|
|
221
|
+
select ok(exists (select 1 from genesis_chat.effects where key = 'transcribe:' ||
|
|
222
|
+
(select id from genesis_chat.attachments where message_id = pg_temp.v('voice'))), 'a voice note queues its transcript');
|
|
223
|
+
select pg_temp.sign_in('a1');
|
|
224
|
+
select genesis_chat.delete_message(pg_temp.v('voice'));
|
|
225
|
+
select pg_temp.as_server();
|
|
226
|
+
select ok(exists (select 1 from genesis_chat.effects where kind = 'storage.remove'
|
|
227
|
+
and payload ->> 'path' = pg_temp.v('dm') || '/' || pg_temp.person('a1') || '/voice-1.m4a'), 'a deleted file is queued for removal');
|
|
228
|
+
select pg_temp.sign_in('a1');
|
|
229
|
+
select throws_ok($$ select * from genesis_chat.claim_effects() $$, '42501', null, 'only the server works the outbox');
|
|
230
|
+
|
|
231
|
+
-- Hosted video and the badge ------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
select pg_temp.sign_in('a1');
|
|
234
|
+
insert into t select 'vid', (genesis_chat.send_message(pg_temp.v('dm'), gen_random_uuid(), '',
|
|
235
|
+
jsonb_build_array(jsonb_build_object('kind', 'video', 'provider', 'mux', 'provider_id', 'upload-abc', 'status', 'ready')))).id;
|
|
236
|
+
select is((select status from genesis_chat.attachments where message_id = pg_temp.v('vid')), 'pending',
|
|
237
|
+
'a hosted video starts pending whatever the sender says; only its webhook makes it ready');
|
|
238
|
+
select pg_temp.sign_in('b2');
|
|
239
|
+
select throws_ok($$ select genesis_chat.send_message(pg_temp.v('dm'), gen_random_uuid(), '',
|
|
240
|
+
jsonb_build_array(jsonb_build_object('kind', 'video', 'provider', 'mux', 'provider_id', 'upload-abc'))) $$,
|
|
241
|
+
'23505', null, 'nobody can attach someone else''s hosted video by copying its id');
|
|
242
|
+
select genesis_chat.mark_read(pg_temp.v('dm'));
|
|
243
|
+
select pg_temp.sign_in('a1');
|
|
244
|
+
select genesis_chat.send_message(pg_temp.v('dm'), gen_random_uuid(), 'In a thread', p_thread_root_id => pg_temp.v('vid'));
|
|
245
|
+
select pg_temp.sign_in('b2');
|
|
246
|
+
select is(genesis_chat.unread_total(), (select sum(unread)::integer from genesis_chat.inbox()),
|
|
247
|
+
'the badge counts exactly what the inbox does (a thread reply counts on its thread, not here)');
|
|
248
|
+
select is((select unread from genesis_chat.inbox() where conversation_id = pg_temp.v('tkt')), 1,
|
|
249
|
+
'a member''s count has the staff reply, never the internal note');
|
|
250
|
+
select is(genesis_chat.unread_total() - (select coalesce(sum(unread), 0)::integer from genesis_chat.inbox()), 0,
|
|
251
|
+
'nor in their badge');
|
|
252
|
+
|
|
253
|
+
select * from finish();
|
|
254
|
+
rollback;
|
package/package.json
CHANGED
|
@@ -1,6 +1,147 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marktiderman/genesis-chat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"description": "Chat for Genesis apps, in the app's own Supabase: conversations of kinds the app defines (direct, group, cohort, support ticket) with members, observers and internal notes; messages with replies, threads, topics, mentions, reactions, edits and deletes; photos, files, voice notes and videos; read state, live updates and typing over private Realtime topics; an outbox for push, transcripts and storage cleanup. A headless core, React hooks, web and native renderers on Genesis UI, and a server runner. Part of the Genesis framework.",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/marktiderman/genesis.git",
|
|
11
|
+
"directory": "packages/chat"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public",
|
|
16
|
+
"provenance": false
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
"./package.json": "./package.json",
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./react": {
|
|
28
|
+
"types": "./dist/react.d.ts",
|
|
29
|
+
"import": "./dist/react.js"
|
|
30
|
+
},
|
|
31
|
+
"./web": {
|
|
32
|
+
"types": "./dist/web.d.ts",
|
|
33
|
+
"import": "./dist/web.js"
|
|
34
|
+
},
|
|
35
|
+
"./native": {
|
|
36
|
+
"types": "./dist/native.d.ts",
|
|
37
|
+
"import": "./dist/native.js"
|
|
38
|
+
},
|
|
39
|
+
"./server": {
|
|
40
|
+
"types": "./dist/server.d.ts",
|
|
41
|
+
"import": "./dist/server.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"migrations",
|
|
47
|
+
"README.md"
|
|
48
|
+
],
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@marktiderman/genesis-features": "^0.3.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@marktiderman/genesis-core": "^1.7.0",
|
|
54
|
+
"@marktiderman/genesis-ui": "^2.13.0",
|
|
55
|
+
"@supabase/supabase-js": "^2.118.0",
|
|
56
|
+
"@tanstack/react-query": "^5.0.0",
|
|
57
|
+
"lucide-react": "^1.0.0",
|
|
58
|
+
"react": "^19.0.0",
|
|
59
|
+
"react-dom": "^19.0.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"@marktiderman/genesis-core": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"@marktiderman/genesis-ui": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"@tanstack/react-query": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"lucide-react": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"react": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"react-dom": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@marktiderman/genesis-core": "^1.7.0",
|
|
83
|
+
"@marktiderman/genesis-ui": "^2.13.0",
|
|
84
|
+
"@supabase/supabase-js": "^2.118.0",
|
|
85
|
+
"@tanstack/react-query": "^5.75.0",
|
|
86
|
+
"@testing-library/react": "^16.3.2",
|
|
87
|
+
"@types/node": "^22",
|
|
88
|
+
"@types/react": "^19.2.14",
|
|
89
|
+
"@types/react-dom": "^19.2.3",
|
|
90
|
+
"happy-dom": "^20.9.0",
|
|
91
|
+
"lucide-react": "^1.28.0",
|
|
92
|
+
"react": "^19.2.5",
|
|
93
|
+
"react-dom": "^19.2.5",
|
|
94
|
+
"tsup": "^8.3.0",
|
|
95
|
+
"typescript": "~6.0.3",
|
|
96
|
+
"vitest": "^4.1.12"
|
|
97
|
+
},
|
|
98
|
+
"genesis": {
|
|
99
|
+
"blocks": {
|
|
100
|
+
"chat": {
|
|
101
|
+
"feature": {
|
|
102
|
+
"name": "Chat",
|
|
103
|
+
"summary": "Direct messages, groups and the app's own kinds of conversation (cohorts, support tickets), with photos, files, voice notes and videos, reactions, replies and threads, read state, live updates and push, in the app's own Supabase.",
|
|
104
|
+
"keywords": [
|
|
105
|
+
"chat",
|
|
106
|
+
"messages",
|
|
107
|
+
"dm",
|
|
108
|
+
"group chat",
|
|
109
|
+
"voice notes",
|
|
110
|
+
"video messages",
|
|
111
|
+
"inbox",
|
|
112
|
+
"support tickets"
|
|
113
|
+
],
|
|
114
|
+
"audience": [
|
|
115
|
+
"person",
|
|
116
|
+
"admin"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"platforms": [
|
|
120
|
+
"web",
|
|
121
|
+
"native",
|
|
122
|
+
"server"
|
|
123
|
+
],
|
|
124
|
+
"migrations": [
|
|
125
|
+
{
|
|
126
|
+
"file": "migrations/0001_genesis_chat.sql",
|
|
127
|
+
"test": "migrations/tests/0001_genesis_chat.test.sql"
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
"install": [
|
|
131
|
+
"@supabase/supabase-js@^2.118.0"
|
|
132
|
+
],
|
|
133
|
+
"steps": [
|
|
134
|
+
"Expose the genesis_chat schema to the API: add it to `schemas` under [api] in supabase/config.toml, and to Exposed schemas in the dashboard.",
|
|
135
|
+
"Register your kinds' rules in a migration of your own, after this one: a start check for direct (who may message whom) and any kinds of your own, with an observer check where staff read without being members (genesis-chat README, Kinds and rules).",
|
|
136
|
+
"Run the outbox: call createChatRunner from @marktiderman/genesis-chat/server on a schedule (every minute) or from a database webhook on genesis_chat.effects, with your push sender."
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"scripts": {
|
|
142
|
+
"build": "tsup && node ../../scripts/check-exports-resolve.mjs",
|
|
143
|
+
"clean": "rm -rf dist",
|
|
144
|
+
"test": "vitest run",
|
|
145
|
+
"typecheck": "tsc --noEmit"
|
|
146
|
+
}
|
|
6
147
|
}
|