@marktiderman/genesis-chat 0.1.0 → 0.2.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/README.md +304 -13
- package/dist/{chunk-SAY24E6V.js → chunk-5D5A7I4Q.js} +166 -45
- package/dist/chunk-5D5A7I4Q.js.map +1 -0
- package/dist/{chunk-JQXZUEEL.js → chunk-7S6HYNWW.js} +382 -8
- package/dist/chunk-7S6HYNWW.js.map +1 -0
- package/dist/chunk-BJCE5O7V.js +70 -0
- package/dist/chunk-BJCE5O7V.js.map +1 -0
- package/dist/chunk-PZTOY2LY.js +576 -0
- package/dist/chunk-PZTOY2LY.js.map +1 -0
- package/dist/chunk-VEPZROQB.js +187 -0
- package/dist/chunk-VEPZROQB.js.map +1 -0
- package/dist/{client-BtdALFCx.d.ts → client-DMa2j7kz.d.ts} +152 -11
- package/dist/index.d.ts +78 -163
- package/dist/index.js +3 -2
- package/dist/native.d.ts +1910 -1
- package/dist/native.js +4900 -0
- package/dist/native.js.map +1 -1
- package/dist/outbox-B0y3phE0.d.ts +151 -0
- package/dist/push.d.ts +206 -0
- package/dist/push.js +3 -0
- package/dist/push.js.map +1 -0
- package/dist/react.d.ts +149 -7
- package/dist/react.js +3 -3
- package/dist/server.d.ts +510 -21
- package/dist/server.js +863 -21
- package/dist/server.js.map +1 -1
- package/dist/thread-DWXHKPdw.d.ts +57 -0
- package/dist/web.d.ts +313 -10
- package/dist/web.js +1731 -60
- package/dist/web.js.map +1 -1
- package/migrations/0002_genesis_chat_coaching.sql +934 -0
- package/migrations/tests/0002_genesis_chat_coaching.test.sql +265 -0
- package/package.json +74 -6
- package/src/__tests__/coaching-core.test.ts +332 -0
- package/src/__tests__/coaching-react.test.tsx +248 -0
- package/src/__tests__/coaching-web.test.tsx +449 -0
- package/src/__tests__/core.test.ts +91 -0
- package/src/__tests__/outbox.test.ts +122 -0
- package/src/__tests__/realtime.test.ts +70 -0
- package/src/__tests__/review-fixes-core.test.ts +193 -0
- package/src/__tests__/review-fixes-web.test.tsx +402 -0
- package/src/__tests__/server-effects.test.ts +158 -0
- package/src/__tests__/server-fakes.ts +90 -0
- package/src/__tests__/server-integrations.test.ts +247 -0
- package/src/__tests__/server-providers.test.ts +246 -0
- package/src/__tests__/server-runner.test.ts +277 -0
- package/src/__tests__/server-safety.test.ts +290 -0
- package/src/__tests__/web-markdown.test.tsx +88 -0
- package/src/__tests__/web.test.tsx +333 -0
- package/src/core/availability.ts +223 -0
- package/src/core/client.ts +757 -0
- package/src/core/extras.ts +106 -0
- package/src/core/media.ts +144 -0
- package/src/core/outbox.ts +429 -0
- package/src/core/thread.ts +171 -0
- package/src/core/types.ts +311 -0
- package/src/index.ts +13 -0
- package/src/native/__tests__/auto-retry.test.tsx +128 -0
- package/src/native/__tests__/composer-mic-lock.test.tsx +115 -0
- package/src/native/__tests__/composer-state.test.ts +188 -0
- package/src/native/__tests__/composer.test.tsx +133 -0
- package/src/native/__tests__/emoji.test.tsx +238 -0
- package/src/native/__tests__/inbox-screen.test.tsx +48 -0
- package/src/native/__tests__/message-bubble.test.tsx +127 -0
- package/src/native/__tests__/organise-logic.test.ts +164 -0
- package/src/native/__tests__/organise-screens.test.tsx +344 -0
- package/src/native/__tests__/photo-viewer.test.tsx +226 -0
- package/src/native/__tests__/push-payload.test.ts +66 -0
- package/src/native/__tests__/push.test.ts +209 -0
- package/src/native/__tests__/recorder-machine.test.ts +96 -0
- package/src/native/__tests__/rows-actions.test.ts +76 -0
- package/src/native/__tests__/thread-layout.test.ts +41 -0
- package/src/native/__tests__/thread-screen.test.tsx +116 -0
- package/src/native/__tests__/tus.test.ts +154 -0
- package/src/native/__tests__/use-voice-recorder.test.tsx +316 -0
- package/src/native/__tests__/video-media.test.tsx +421 -0
- package/src/native/__tests__/video-recorder-state.test.ts +87 -0
- package/src/native/__tests__/video-upload.test.ts +227 -0
- package/src/native/access-error.tsx +31 -0
- package/src/native/actions.ts +42 -0
- package/src/native/attachment-cards.tsx +116 -0
- package/src/native/auto-retry.ts +69 -0
- package/src/native/availability.tsx +376 -0
- package/src/native/broadcast-sheet.tsx +193 -0
- package/src/native/composer-state.ts +302 -0
- package/src/native/composer.tsx +453 -0
- package/src/native/context.tsx +70 -0
- package/src/native/copy.ts +296 -0
- package/src/native/emoji-data.ts +21 -0
- package/src/native/emoji-picker.tsx +229 -0
- package/src/native/emoji.ts +118 -0
- package/src/native/icon-button.tsx +40 -0
- package/src/native/inbox-screen.tsx +116 -0
- package/src/native/index.ts +184 -0
- package/src/native/link-preview.tsx +104 -0
- package/src/native/media-colors.ts +15 -0
- package/src/native/media.tsx +187 -0
- package/src/native/message-actions-sheet.tsx +121 -0
- package/src/native/message-bubble.tsx +374 -0
- package/src/native/moderation.tsx +42 -0
- package/src/native/modules.ts +275 -0
- package/src/native/organise-copy.ts +195 -0
- package/src/native/photo-viewer.tsx +531 -0
- package/src/native/push.ts +375 -0
- package/src/native/recorder-machine.ts +176 -0
- package/src/native/rows.ts +81 -0
- package/src/native/schedule-sheet.tsx +314 -0
- package/src/native/search-screen.tsx +168 -0
- package/src/native/stack-options.ts +36 -0
- package/src/native/start-sheet.tsx +87 -0
- package/src/native/thread-screen.tsx +620 -0
- package/src/native/topics.tsx +179 -0
- package/src/native/tus.ts +282 -0
- package/src/native/use-composer.ts +71 -0
- package/src/native/use-jump.ts +91 -0
- package/src/native/use-voice-recorder.ts +234 -0
- package/src/native/video-player.tsx +370 -0
- package/src/native/video-recorder-state.ts +140 -0
- package/src/native/video-recorder.tsx +271 -0
- package/src/native/video-upload-tray.tsx +77 -0
- package/src/native/video-upload.ts +432 -0
- package/src/native/voice-player.tsx +283 -0
- package/src/react/index.tsx +810 -0
- package/src/server/assistant.ts +235 -0
- package/src/server/crypto.ts +86 -0
- package/src/server/expo.ts +119 -0
- package/src/server/handler.ts +32 -0
- package/src/server/index.ts +57 -0
- package/src/server/moderation.ts +155 -0
- package/src/server/mux.ts +290 -0
- package/src/server/notifications.ts +130 -0
- package/src/server/runner.ts +468 -0
- package/src/server/transcribers.ts +111 -0
- package/src/server/types.ts +171 -0
- package/src/server/unfurl.ts +480 -0
- package/src/server/upload.ts +158 -0
- package/src/web/attachments.tsx +379 -0
- package/src/web/availability.tsx +299 -0
- package/src/web/broadcast.tsx +166 -0
- package/src/web/bubble.tsx +563 -0
- package/src/web/cards.tsx +136 -0
- package/src/web/composer.tsx +571 -0
- package/src/web/emoji-picker.tsx +84 -0
- package/src/web/error-boundary.tsx +33 -0
- package/src/web/format.ts +55 -0
- package/src/web/inbox.tsx +195 -0
- package/src/web/index.ts +75 -0
- package/src/web/markdown.tsx +245 -0
- package/src/web/memory-client.ts +706 -0
- package/src/web/panel.tsx +135 -0
- package/src/web/schedule.tsx +192 -0
- package/src/web/search.tsx +129 -0
- package/src/web/thread-context.ts +64 -0
- package/src/web/thread.tsx +802 -0
- package/src/web/topics.tsx +160 -0
- package/src/web/use-voice-recorder.ts +264 -0
- package/src/web/video.tsx +522 -0
- package/src/web/voice-note.tsx +197 -0
- package/dist/chunk-27UTR5FW.js +0 -322
- package/dist/chunk-27UTR5FW.js.map +0 -1
- package/dist/chunk-JQXZUEEL.js.map +0 -1
- package/dist/chunk-SAY24E6V.js.map +0 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
-- @feat:chat
|
|
2
|
+
-- pgTAP test for 0002_genesis_chat_coaching.sql (with 0001 applied).
|
|
3
|
+
-- Copy into your app's `supabase/tests/database/` and run `supabase test db`.
|
|
4
|
+
-- Writes only its own users, kinds and conversations, and rolls everything back.
|
|
5
|
+
--
|
|
6
|
+
-- People: a1 owns a group with b2 and e5 in it; c3 is outside; d4 is staff (an observer).
|
|
7
|
+
begin;
|
|
8
|
+
create extension if not exists pgtap with schema extensions;
|
|
9
|
+
select plan(65);
|
|
10
|
+
|
|
11
|
+
insert into auth.users (id, aud, role, email, instance_id)
|
|
12
|
+
select ('00000000-0000-4000-8000-00000000c2' || n)::uuid, 'authenticated', 'authenticated', 'genesis-chat2-' || n || '@example.test', '00000000-0000-0000-0000-000000000000'
|
|
13
|
+
from unnest(array['a1', 'b2', 'c3', 'd4', 'e5']) n;
|
|
14
|
+
|
|
15
|
+
create function pg_temp.person(n text) returns uuid language sql immutable as $$ select ('00000000-0000-4000-8000-00000000c2' || n)::uuid $$;
|
|
16
|
+
create function pg_temp.sign_in(n text) returns void language plpgsql as $$
|
|
17
|
+
begin
|
|
18
|
+
perform set_config('request.jwt.claims', json_build_object('sub', pg_temp.person(n), 'role', 'authenticated')::text, true);
|
|
19
|
+
perform set_config('request.jwt.claim.sub', pg_temp.person(n)::text, true);
|
|
20
|
+
perform set_config('role', 'authenticated', true);
|
|
21
|
+
end $$;
|
|
22
|
+
create function pg_temp.as_server() returns void language plpgsql as $$
|
|
23
|
+
begin
|
|
24
|
+
perform set_config('request.jwt.claims', '{"role":"service_role"}', true);
|
|
25
|
+
perform set_config('request.jwt.claim.sub', '', true);
|
|
26
|
+
perform set_config('role', 'service_role', true);
|
|
27
|
+
end $$;
|
|
28
|
+
grant execute on function pg_temp.person(text), pg_temp.sign_in(text), pg_temp.as_server() to anon, authenticated, service_role;
|
|
29
|
+
|
|
30
|
+
create temp table t (k text primary key, v uuid);
|
|
31
|
+
grant select, insert, update on t to authenticated, service_role;
|
|
32
|
+
create function pg_temp.v(k_ text) returns uuid language sql stable as $$ select v from t where k = k_ $$;
|
|
33
|
+
grant execute on function pg_temp.v(text) to authenticated, service_role;
|
|
34
|
+
|
|
35
|
+
create function public.genesis_chat2_test_may_start(member_ids uuid[], subject_type text, subject_id text, user_id uuid)
|
|
36
|
+
returns boolean language sql stable set search_path = '' as $$ select true $$;
|
|
37
|
+
create function public.genesis_chat2_test_staff(conversation_id uuid, user_id uuid)
|
|
38
|
+
returns boolean language sql stable set search_path = '' as $$ select user_id = '00000000-0000-4000-8000-00000000c2d4'::uuid $$;
|
|
39
|
+
select genesis_chat.register_kind('direct', 'Direct message', p_max_members => 2,
|
|
40
|
+
p_start_check => 'public.genesis_chat2_test_may_start(uuid[], text, text, uuid)');
|
|
41
|
+
select genesis_chat.register_kind('cohort', 'Cohort', p_membership => 'admins',
|
|
42
|
+
p_start_check => 'public.genesis_chat2_test_may_start(uuid[], text, text, uuid)',
|
|
43
|
+
p_observer_check => 'public.genesis_chat2_test_staff(uuid, uuid)', p_observers_post => true);
|
|
44
|
+
|
|
45
|
+
select pg_temp.sign_in('a1');
|
|
46
|
+
insert into t select 'grp', genesis_chat.start_conversation('cohort', array[pg_temp.person('b2'), pg_temp.person('e5')], 'Cohort 3');
|
|
47
|
+
|
|
48
|
+
-- Topics ------------------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
select lives_ok($$ select genesis_chat.set_topic(pg_temp.v('grp'), 'lesson-1', 'Lesson 1: Foundations') $$, 'an owner adds a topic');
|
|
51
|
+
select is((select title from genesis_chat.topics where conversation_id = pg_temp.v('grp') and topic = 'lesson-1'),
|
|
52
|
+
'Lesson 1: Foundations', 'with its title');
|
|
53
|
+
select pg_temp.sign_in('b2');
|
|
54
|
+
select ok(exists (select 1 from genesis_chat.topics where conversation_id = pg_temp.v('grp')), 'members read the topics');
|
|
55
|
+
select throws_ok($$ select genesis_chat.set_topic(pg_temp.v('grp'), 'lesson-2', 'Mine') $$, '42501', null, 'a member doesn''t add topics');
|
|
56
|
+
select lives_ok($$ select genesis_chat.send_message(pg_temp.v('grp'), gen_random_uuid(), 'Question on lesson 1', p_topic => 'lesson-1') $$,
|
|
57
|
+
'a member posts in an open topic');
|
|
58
|
+
select pg_temp.sign_in('a1');
|
|
59
|
+
select genesis_chat.set_topic(pg_temp.v('grp'), 'lesson-1', p_closed => true);
|
|
60
|
+
select pg_temp.sign_in('b2');
|
|
61
|
+
select throws_ok($$ select genesis_chat.send_message(pg_temp.v('grp'), gen_random_uuid(), 'Late', p_topic => 'lesson-1') $$, '42501', null,
|
|
62
|
+
'not in a closed one');
|
|
63
|
+
select pg_temp.sign_in('a1');
|
|
64
|
+
select lives_ok($$ select genesis_chat.send_message(pg_temp.v('grp'), gen_random_uuid(), 'Wrapping up', p_topic => 'lesson-1') $$,
|
|
65
|
+
'its owner still can');
|
|
66
|
+
select pg_temp.sign_in('c3');
|
|
67
|
+
select is((select count(*)::integer from genesis_chat.topics where conversation_id = pg_temp.v('grp')), 0, 'someone outside sees no topics');
|
|
68
|
+
|
|
69
|
+
-- Scheduled send --------------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
select pg_temp.sign_in('b2');
|
|
72
|
+
insert into t select 'sch', genesis_chat.schedule_message(pg_temp.v('grp'), gen_random_uuid(), now() + interval '1 hour', 'Reminder: check-in due');
|
|
73
|
+
select throws_ok($$ select genesis_chat.schedule_message(pg_temp.v('grp'), gen_random_uuid(), now(), 'Now') $$, '22023', null,
|
|
74
|
+
'a schedule is at least a minute ahead');
|
|
75
|
+
select pg_temp.sign_in('c3');
|
|
76
|
+
select throws_ok($$ select genesis_chat.schedule_message(pg_temp.v('grp'), gen_random_uuid(), now() + interval '1 hour', 'Hi') $$, '42501', null,
|
|
77
|
+
'only someone who may post schedules');
|
|
78
|
+
select is((select count(*)::integer from genesis_chat.scheduled), 0, 'and nobody else sees a scheduled message');
|
|
79
|
+
reset role;
|
|
80
|
+
update genesis_chat.scheduled set send_at = now() - interval '1 minute' where id = pg_temp.v('sch');
|
|
81
|
+
select pg_temp.as_server();
|
|
82
|
+
select is((select array_agg(x) from genesis_chat.due_scheduled() x), array[pg_temp.v('sch')], 'it comes due');
|
|
83
|
+
insert into t select 'posted', genesis_chat.post_scheduled(pg_temp.v('sch'));
|
|
84
|
+
select is((select sender_id from genesis_chat.messages where id = pg_temp.v('posted')), pg_temp.person('b2'), 'and is posted as its sender');
|
|
85
|
+
select is(genesis_chat.post_scheduled(pg_temp.v('sch')), pg_temp.v('posted'), 'once');
|
|
86
|
+
select pg_temp.sign_in('e5');
|
|
87
|
+
insert into t select 'sch2', genesis_chat.schedule_message(pg_temp.v('grp'), gen_random_uuid(), now() + interval '1 hour', 'Bye later');
|
|
88
|
+
select genesis_chat.remove_member(pg_temp.v('grp'));
|
|
89
|
+
reset role;
|
|
90
|
+
update genesis_chat.scheduled set send_at = now() - interval '1 minute' where id = pg_temp.v('sch2');
|
|
91
|
+
select pg_temp.as_server();
|
|
92
|
+
select is(genesis_chat.post_scheduled(pg_temp.v('sch2')), null, 'a scheduled message from someone who has since left isn''t posted');
|
|
93
|
+
select ok((select error is not null from genesis_chat.scheduled where id = pg_temp.v('sch2')), 'and says why');
|
|
94
|
+
select pg_temp.sign_in('a1');
|
|
95
|
+
select throws_ok($$ select genesis_chat.post_scheduled(pg_temp.v('sch2')) $$, '42501', null, 'only the server posts scheduled messages');
|
|
96
|
+
|
|
97
|
+
-- A failure that may pass (here a deadlock) is tried again on the next run, not recorded for good.
|
|
98
|
+
select pg_temp.sign_in('b2');
|
|
99
|
+
insert into t select 'sch3', genesis_chat.schedule_message(pg_temp.v('grp'), gen_random_uuid(), now() + interval '1 hour', 'Flaky one');
|
|
100
|
+
reset role;
|
|
101
|
+
update genesis_chat.scheduled set send_at = now() - interval '1 minute' where id = pg_temp.v('sch3');
|
|
102
|
+
create function public.genesis_chat2_test_flaky() returns trigger language plpgsql set search_path = '' as $$
|
|
103
|
+
begin
|
|
104
|
+
if new.body = 'Flaky one' then raise exception 'deadlock detected' using errcode = '40P01'; end if;
|
|
105
|
+
return new;
|
|
106
|
+
end $$;
|
|
107
|
+
create trigger zz_genesis_chat2_test_flaky before insert on genesis_chat.messages
|
|
108
|
+
for each row execute function public.genesis_chat2_test_flaky();
|
|
109
|
+
select pg_temp.as_server();
|
|
110
|
+
select is(genesis_chat.post_scheduled(pg_temp.v('sch3')), null, 'a deadlock posts nothing yet');
|
|
111
|
+
select ok((select error is null and attempts = 1 from genesis_chat.scheduled where id = pg_temp.v('sch3')),
|
|
112
|
+
'but keeps the message due, counting the try');
|
|
113
|
+
reset role;
|
|
114
|
+
drop trigger zz_genesis_chat2_test_flaky on genesis_chat.messages;
|
|
115
|
+
select pg_temp.as_server();
|
|
116
|
+
select ok(genesis_chat.post_scheduled(pg_temp.v('sch3')) is not null, 'and the next run posts it');
|
|
117
|
+
select pg_temp.sign_in('a1');
|
|
118
|
+
|
|
119
|
+
-- Availability ------------------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
select genesis_chat.set_availability('Back Monday', null, 'America/New_York', '{"mon": [["09:00","17:00"]]}');
|
|
122
|
+
select pg_temp.sign_in('b2');
|
|
123
|
+
select is((select away_message from genesis_chat.availability_for(pg_temp.v('grp')) where user_id = pg_temp.person('a1')),
|
|
124
|
+
'Back Monday', 'people in a conversation see each other''s away message');
|
|
125
|
+
select pg_temp.sign_in('c3');
|
|
126
|
+
select is((select count(*)::integer from genesis_chat.availability_for(pg_temp.v('grp'))), 0, 'someone outside sees nobody''s');
|
|
127
|
+
select throws_ok($$ select genesis_chat.set_availability(null, null, 'Mars/Olympus', null) $$, '22023', null, 'a time zone must be real');
|
|
128
|
+
select pg_temp.sign_in('a1');
|
|
129
|
+
select throws_ok($$ select genesis_chat.set_availability(null, null, null, '{"mon": [[9, 17]]}') $$, '22023', null, 'hours are strings');
|
|
130
|
+
select throws_ok($$ select genesis_chat.set_availability(null, null, null, '{"tue": 5}') $$, '22023', null, 'a day is a list of ranges');
|
|
131
|
+
select throws_ok($$ select genesis_chat.set_availability(null, null, null, '{"funday": [["09:00","17:00"]]}') $$, '22023', null, 'days are mon..sun');
|
|
132
|
+
select throws_ok($$ select genesis_chat.set_availability(null, null, null, '{"mon": [["9am","5pm"]]}') $$, '22023', null, 'times are HH:MM');
|
|
133
|
+
select throws_ok($$ select genesis_chat.set_availability(null, null, null, '{"mon": [["09:00"]]}') $$, '22023', null, 'a range is a pair');
|
|
134
|
+
select throws_ok($$ select genesis_chat.set_availability(null, null, null, '{"mon": [["01:00","02:00"],["03:00","04:00"],["05:00","06:00"],["07:00","08:00"],["09:00","10:00"]]}') $$,
|
|
135
|
+
'22023', null, 'at most four ranges a day');
|
|
136
|
+
select lives_ok($$ select genesis_chat.set_availability('Back Monday', null, 'America/New_York', '{"mon": [["9:00","17:00"]], "fri": [["22:00","24:00"],["00:00","02:00"]]}') $$,
|
|
137
|
+
'well-formed hours are kept');
|
|
138
|
+
select pg_temp.as_server();
|
|
139
|
+
select throws_ok($$ update genesis_chat.availability set hours = '{"mon": [[9, 17]]}' where user_id = pg_temp.person('a1') $$, '23514', null,
|
|
140
|
+
'the table refuses malformed hours from the server too');
|
|
141
|
+
|
|
142
|
+
-- Broadcast ---------------------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
select pg_temp.sign_in('a1');
|
|
145
|
+
insert into t select 'dm1', genesis_chat.start_conversation('direct', array[pg_temp.person('b2')]);
|
|
146
|
+
insert into t select 'dm2', genesis_chat.start_conversation('direct', array[pg_temp.person('e5')]);
|
|
147
|
+
insert into t values ('bc', gen_random_uuid());
|
|
148
|
+
select is(cardinality(genesis_chat.broadcast_message(array[pg_temp.v('dm1'), pg_temp.v('dm2')], pg_temp.v('bc'), 'Week 2 is live')), 2,
|
|
149
|
+
'a broadcast goes into each conversation');
|
|
150
|
+
select is(cardinality(genesis_chat.broadcast_message(array[pg_temp.v('dm1'), pg_temp.v('dm2')], pg_temp.v('bc'), 'Week 2 is live')), 2,
|
|
151
|
+
'and sending it again posts nothing new');
|
|
152
|
+
select is((select count(*)::integer from genesis_chat.messages where body = 'Week 2 is live'), 2, 'once per conversation');
|
|
153
|
+
select pg_temp.sign_in('b2');
|
|
154
|
+
select is((select count(*)::integer from genesis_chat.messages where body = 'Week 2 is live'), 1, 'each person sees only their own copy');
|
|
155
|
+
|
|
156
|
+
-- Search ----------------------------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
select pg_temp.sign_in('d4');
|
|
159
|
+
select genesis_chat.send_message(pg_temp.v('grp'), gen_random_uuid(), 'Internal: lesson pacing is off', p_visibility => 'observers');
|
|
160
|
+
select pg_temp.sign_in('b2');
|
|
161
|
+
select ok(exists (select 1 from genesis_chat.search_messages('lesson')), 'a member finds messages they can read');
|
|
162
|
+
select ok(not exists (select 1 from genesis_chat.search_messages('pacing')), 'never an internal note');
|
|
163
|
+
select genesis_chat.send_message(pg_temp.v('grp'), gen_random_uuid(), 'the word quilt beside a «fake» highlight');
|
|
164
|
+
select ok((select snippet like '%«quilt»%' and snippet not like '%«fake»%' from genesis_chat.search_messages('quilt')),
|
|
165
|
+
'a message''s own « and » can''t fake a highlight');
|
|
166
|
+
select pg_temp.sign_in('c3');
|
|
167
|
+
select ok(not exists (select 1 from genesis_chat.search_messages('lesson')), 'someone outside finds nothing');
|
|
168
|
+
|
|
169
|
+
-- Moderation, link previews, retention ----------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
select pg_temp.as_server();
|
|
172
|
+
select genesis_chat.set_kind_options('cohort', true, interval '30 days');
|
|
173
|
+
select pg_temp.sign_in('b2');
|
|
174
|
+
insert into t select 'bad', (genesis_chat.send_message(pg_temp.v('grp'), gen_random_uuid(), 'see https://example.com/page')).id;
|
|
175
|
+
select pg_temp.as_server();
|
|
176
|
+
select ok(exists (select 1 from genesis_chat.effects where key = 'moderate:' || pg_temp.v('bad')), 'a kind that moderates queues moderation');
|
|
177
|
+
select ok(exists (select 1 from genesis_chat.effects where kind = 'unfurl' and payload ->> 'url' = 'https://example.com/page'),
|
|
178
|
+
'a link queues its preview');
|
|
179
|
+
select genesis_chat.moderate_message(pg_temp.v('bad'), 'hidden', 'test');
|
|
180
|
+
select pg_temp.sign_in('a1');
|
|
181
|
+
select is((select count(*)::integer from genesis_chat.messages where id = pg_temp.v('bad')), 0, 'a hidden message leaves members'' view');
|
|
182
|
+
select pg_temp.sign_in('d4');
|
|
183
|
+
select is((select count(*)::integer from genesis_chat.messages where id = pg_temp.v('bad')), 1, 'observers still see it, with a report');
|
|
184
|
+
reset role;
|
|
185
|
+
insert into genesis_chat.attachments (message_id, conversation_id, kind, provider, transcript)
|
|
186
|
+
values (pg_temp.v('bad'), pg_temp.v('grp'), 'voice', 'none', 'the walrus transcript of a hidden note');
|
|
187
|
+
select pg_temp.sign_in('a1');
|
|
188
|
+
select ok(not exists (select 1 from genesis_chat.search_messages('walrus')), 'search never shows members a hidden message''s transcript');
|
|
189
|
+
select pg_temp.sign_in('d4');
|
|
190
|
+
select ok(exists (select 1 from genesis_chat.search_messages('walrus', pg_temp.v('grp'))), 'observers still find it');
|
|
191
|
+
|
|
192
|
+
-- Pushes wait for moderation; a hidden message is never pushed, counted or previewed.
|
|
193
|
+
select pg_temp.as_server();
|
|
194
|
+
select ok((select done_at is not null from genesis_chat.effects where key = 'push:' || pg_temp.v('bad')),
|
|
195
|
+
'a hidden message''s push is settled unsent');
|
|
196
|
+
select is((select count(*)::integer from genesis_chat.push_targets(pg_temp.v('bad'))), 0, 'and push_targets has nobody for it');
|
|
197
|
+
select is(genesis_chat.unread_count(pg_temp.v('grp'), pg_temp.person('a1'),
|
|
198
|
+
(select seq - 1 from genesis_chat.messages where id = pg_temp.v('bad'))), 0, 'it counts toward nobody''s badge');
|
|
199
|
+
select pg_temp.sign_in('a1');
|
|
200
|
+
select ok((select last_message ->> 'id' from genesis_chat.inbox() where conversation_id = pg_temp.v('grp')) is distinct from pg_temp.v('bad')::text,
|
|
201
|
+
'nor shows in members'' inbox');
|
|
202
|
+
select pg_temp.sign_in('b2');
|
|
203
|
+
insert into t select 'ok1', (genesis_chat.send_message(pg_temp.v('grp'), gen_random_uuid(), 'All good here')).id;
|
|
204
|
+
select pg_temp.as_server();
|
|
205
|
+
select is((select run_after from genesis_chat.effects where key = 'push:' || pg_temp.v('ok1')), 'infinity'::timestamptz,
|
|
206
|
+
'a push in a kind that moderates waits for the verdict');
|
|
207
|
+
select genesis_chat.moderate_message(pg_temp.v('ok1'), 'clean');
|
|
208
|
+
select ok((select run_after <= now() and done_at is null from genesis_chat.effects where key = 'push:' || pg_temp.v('ok1')),
|
|
209
|
+
'and goes once the message is clean');
|
|
210
|
+
|
|
211
|
+
-- A retried or repeated verdict never un-hides a message, nor files the same report twice.
|
|
212
|
+
select genesis_chat.moderate_message(pg_temp.v('bad'), 'hidden', 'again');
|
|
213
|
+
select genesis_chat.moderate_message(pg_temp.v('bad'), 'flagged', 'moderation unavailable');
|
|
214
|
+
select is((select moderation from genesis_chat.messages where id = pg_temp.v('bad')), 'hidden', 'a later verdict never un-hides a message');
|
|
215
|
+
select is((select count(*)::integer from genesis_chat.reports where message_id = pg_temp.v('bad') and snapshot ->> 'automatic' = 'true'), 2,
|
|
216
|
+
'one report per verdict');
|
|
217
|
+
|
|
218
|
+
-- An edit is moderated again.
|
|
219
|
+
select pg_temp.sign_in('b2');
|
|
220
|
+
select genesis_chat.edit_message(pg_temp.v('ok1'), 'Edited into something else');
|
|
221
|
+
select pg_temp.as_server();
|
|
222
|
+
select ok(exists (select 1 from genesis_chat.effects where kind = 'moderate' and key like 'moderate:' || pg_temp.v('ok1') || ':%'),
|
|
223
|
+
'an edited message is judged again');
|
|
224
|
+
|
|
225
|
+
-- Link previews are read through a message you can read, never as a table.
|
|
226
|
+
select genesis_chat.save_link_preview('https://example.com/page', 'ok', 'Example page');
|
|
227
|
+
select pg_temp.sign_in('b2');
|
|
228
|
+
select throws_ok($$ select count(*) from genesis_chat.link_previews $$, '42501', null, 'people don''t read the preview cache directly');
|
|
229
|
+
insert into t select 'linked', (genesis_chat.send_message(pg_temp.v('grp'), gen_random_uuid(), 'look at https://example.com/page')).id;
|
|
230
|
+
select is((select title from genesis_chat.link_previews_for(pg_temp.v('linked'))), 'Example page',
|
|
231
|
+
'a member reads the previews of a message they can read');
|
|
232
|
+
select pg_temp.sign_in('a1');
|
|
233
|
+
select is((select count(*)::integer from genesis_chat.link_previews_for(pg_temp.v('bad'))), 0, 'not those of a hidden message');
|
|
234
|
+
select pg_temp.sign_in('c3');
|
|
235
|
+
select is((select count(*)::integer from genesis_chat.link_previews_for(pg_temp.v('linked'))), 0, 'and someone outside reads none');
|
|
236
|
+
select pg_temp.sign_in('b2');
|
|
237
|
+
select genesis_chat.send_message(pg_temp.v('grp'), gen_random_uuid(), 'reset it at https://example.com/reset?token=abc123');
|
|
238
|
+
select pg_temp.as_server();
|
|
239
|
+
select ok(not exists (select 1 from genesis_chat.effects where kind = 'unfurl' and payload ->> 'url' like '%token=%'),
|
|
240
|
+
'a link carrying a credential is never fetched');
|
|
241
|
+
|
|
242
|
+
reset role;
|
|
243
|
+
update genesis_chat.messages set created_at = now() - interval '60 days' where conversation_id = pg_temp.v('grp');
|
|
244
|
+
update genesis_chat.conversations set legal_hold = true where id = pg_temp.v('grp');
|
|
245
|
+
select pg_temp.as_server();
|
|
246
|
+
select is(genesis_chat.purge_expired(), 0, 'legal hold keeps everything');
|
|
247
|
+
select genesis_chat.set_legal_hold(pg_temp.v('grp'), false);
|
|
248
|
+
select ok(genesis_chat.purge_expired() > 0, 'without it, messages past the kind''s retention go');
|
|
249
|
+
|
|
250
|
+
-- Inbox kinds ------------------------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
select pg_temp.sign_in('b2');
|
|
253
|
+
select ok(exists (select 1 from genesis_chat.inbox(p_kinds => array['direct'])) and
|
|
254
|
+
not exists (select 1 from genesis_chat.inbox(p_kinds => array['direct']) where kind <> 'direct'),
|
|
255
|
+
'the inbox narrows to the kinds asked for');
|
|
256
|
+
select ok(not exists (select 1 from genesis_chat.inbox(p_exclude_kinds => array['cohort']) where kind = 'cohort'),
|
|
257
|
+
'and leaves out the kinds asked to');
|
|
258
|
+
select ok(genesis_chat.unread_total(p_exclude_kinds => array['cohort', 'direct']) = 0, 'so does the badge');
|
|
259
|
+
|
|
260
|
+
select pg_temp.as_server();
|
|
261
|
+
select ok((select coalesce(bool_and(unread_total = 0), true) from genesis_chat.push_targets(pg_temp.v('bad'), array['cohort', 'direct'])),
|
|
262
|
+
'the push badge leaves out the kinds the app leaves out of its own');
|
|
263
|
+
|
|
264
|
+
select * from finish();
|
|
265
|
+
rollback;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marktiderman/genesis-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -16,17 +16,22 @@
|
|
|
16
16
|
"provenance": false
|
|
17
17
|
},
|
|
18
18
|
"main": "./dist/index.js",
|
|
19
|
+
"react-native": "./src/index.ts",
|
|
19
20
|
"module": "./dist/index.js",
|
|
20
21
|
"types": "./dist/index.d.ts",
|
|
21
22
|
"exports": {
|
|
22
23
|
"./package.json": "./package.json",
|
|
23
24
|
".": {
|
|
24
25
|
"types": "./dist/index.d.ts",
|
|
25
|
-
"
|
|
26
|
+
"react-native": "./src/index.ts",
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"default": "./dist/index.js"
|
|
26
29
|
},
|
|
27
30
|
"./react": {
|
|
28
31
|
"types": "./dist/react.d.ts",
|
|
29
|
-
"
|
|
32
|
+
"react-native": "./src/react/index.tsx",
|
|
33
|
+
"import": "./dist/react.js",
|
|
34
|
+
"default": "./dist/react.js"
|
|
30
35
|
},
|
|
31
36
|
"./web": {
|
|
32
37
|
"types": "./dist/web.d.ts",
|
|
@@ -34,7 +39,15 @@
|
|
|
34
39
|
},
|
|
35
40
|
"./native": {
|
|
36
41
|
"types": "./dist/native.d.ts",
|
|
37
|
-
"
|
|
42
|
+
"react-native": "./src/native/index.ts",
|
|
43
|
+
"import": "./dist/native.js",
|
|
44
|
+
"default": "./dist/native.js"
|
|
45
|
+
},
|
|
46
|
+
"./push": {
|
|
47
|
+
"types": "./dist/push.d.ts",
|
|
48
|
+
"react-native": "./src/native/push.ts",
|
|
49
|
+
"import": "./dist/push.js",
|
|
50
|
+
"default": "./dist/push.js"
|
|
38
51
|
},
|
|
39
52
|
"./server": {
|
|
40
53
|
"types": "./dist/server.d.ts",
|
|
@@ -43,20 +56,26 @@
|
|
|
43
56
|
},
|
|
44
57
|
"files": [
|
|
45
58
|
"dist",
|
|
59
|
+
"src",
|
|
46
60
|
"migrations",
|
|
47
61
|
"README.md"
|
|
48
62
|
],
|
|
49
63
|
"dependencies": {
|
|
50
|
-
"@marktiderman/genesis-features": "^0.3.0"
|
|
64
|
+
"@marktiderman/genesis-features": "^0.3.0",
|
|
65
|
+
"frimousse": "0.4.0"
|
|
51
66
|
},
|
|
52
67
|
"peerDependencies": {
|
|
53
68
|
"@marktiderman/genesis-core": "^1.7.0",
|
|
54
69
|
"@marktiderman/genesis-ui": "^2.13.0",
|
|
70
|
+
"@marktiderman/genesis-ui-native": "^2.4.0",
|
|
55
71
|
"@supabase/supabase-js": "^2.118.0",
|
|
56
72
|
"@tanstack/react-query": "^5.0.0",
|
|
73
|
+
"hls.js": "^1.5.0",
|
|
57
74
|
"lucide-react": "^1.0.0",
|
|
58
75
|
"react": "^19.0.0",
|
|
59
|
-
"react-dom": "^19.0.0"
|
|
76
|
+
"react-dom": "^19.0.0",
|
|
77
|
+
"react-native": ">=0.76.0",
|
|
78
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
60
79
|
},
|
|
61
80
|
"peerDependenciesMeta": {
|
|
62
81
|
"@marktiderman/genesis-core": {
|
|
@@ -65,9 +84,42 @@
|
|
|
65
84
|
"@marktiderman/genesis-ui": {
|
|
66
85
|
"optional": true
|
|
67
86
|
},
|
|
87
|
+
"@marktiderman/genesis-ui-native": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
90
|
+
"@shopify/flash-list": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
68
93
|
"@tanstack/react-query": {
|
|
69
94
|
"optional": true
|
|
70
95
|
},
|
|
96
|
+
"expo-audio": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
99
|
+
"expo-camera": {
|
|
100
|
+
"optional": true
|
|
101
|
+
},
|
|
102
|
+
"expo-clipboard": {
|
|
103
|
+
"optional": true
|
|
104
|
+
},
|
|
105
|
+
"expo-haptics": {
|
|
106
|
+
"optional": true
|
|
107
|
+
},
|
|
108
|
+
"expo-image": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"expo-image-picker": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
114
|
+
"expo-video": {
|
|
115
|
+
"optional": true
|
|
116
|
+
},
|
|
117
|
+
"expo-web-browser": {
|
|
118
|
+
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"hls.js": {
|
|
121
|
+
"optional": true
|
|
122
|
+
},
|
|
71
123
|
"lucide-react": {
|
|
72
124
|
"optional": true
|
|
73
125
|
},
|
|
@@ -76,11 +128,22 @@
|
|
|
76
128
|
},
|
|
77
129
|
"react-dom": {
|
|
78
130
|
"optional": true
|
|
131
|
+
},
|
|
132
|
+
"react-native": {
|
|
133
|
+
"optional": true
|
|
134
|
+
},
|
|
135
|
+
"react-native-keyboard-controller": {
|
|
136
|
+
"optional": true
|
|
137
|
+
},
|
|
138
|
+
"react-native-safe-area-context": {
|
|
139
|
+
"optional": true
|
|
79
140
|
}
|
|
80
141
|
},
|
|
81
142
|
"devDependencies": {
|
|
82
143
|
"@marktiderman/genesis-core": "^1.7.0",
|
|
144
|
+
"@marktiderman/genesis-notifications": "^0.1.1",
|
|
83
145
|
"@marktiderman/genesis-ui": "^2.13.0",
|
|
146
|
+
"@marktiderman/genesis-ui-native": "^2.4.0",
|
|
84
147
|
"@supabase/supabase-js": "^2.118.0",
|
|
85
148
|
"@tanstack/react-query": "^5.75.0",
|
|
86
149
|
"@testing-library/react": "^16.3.2",
|
|
@@ -88,6 +151,7 @@
|
|
|
88
151
|
"@types/react": "^19.2.14",
|
|
89
152
|
"@types/react-dom": "^19.2.3",
|
|
90
153
|
"happy-dom": "^20.9.0",
|
|
154
|
+
"hls.js": "1.7.3",
|
|
91
155
|
"lucide-react": "^1.28.0",
|
|
92
156
|
"react": "^19.2.5",
|
|
93
157
|
"react-dom": "^19.2.5",
|
|
@@ -125,6 +189,10 @@
|
|
|
125
189
|
{
|
|
126
190
|
"file": "migrations/0001_genesis_chat.sql",
|
|
127
191
|
"test": "migrations/tests/0001_genesis_chat.test.sql"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"file": "migrations/0002_genesis_chat_coaching.sql",
|
|
195
|
+
"test": "migrations/tests/0002_genesis_chat_coaching.test.sql"
|
|
128
196
|
}
|
|
129
197
|
],
|
|
130
198
|
"install": [
|