diff --git a/.sqlx/query-08f040920cffc4ee2c639f36b685cafed69a8d525e419eabc6a0471ade36a521.json b/.sqlx/query-08f040920cffc4ee2c639f36b685cafed69a8d525e419eabc6a0471ade36a521.json new file mode 100644 index 00000000000..383c36d9d01 --- /dev/null +++ b/.sqlx/query-08f040920cffc4ee2c639f36b685cafed69a8d525e419eabc6a0471ade36a521.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT p->>'jid' AS \"jid!\" FROM whatsapp_group_roster r\n CROSS JOIN LATERAL jsonb_array_elements(r.metadata->'participants') p\n WHERE r.account_id = $1 AND r.group_jid = $2 AND p->>'alternateJid' = $3\n AND p->>'jid' LIKE '%@s.whatsapp.net'\n LIMIT 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "jid!", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "08f040920cffc4ee2c639f36b685cafed69a8d525e419eabc6a0471ade36a521" +} diff --git a/.sqlx/query-0f01a5b08dba2c0c7069d295be4f3cf8faad938d4c5076bc339eb11929e606d7.json b/.sqlx/query-0f01a5b08dba2c0c7069d295be4f3cf8faad938d4c5076bc339eb11929e606d7.json deleted file mode 100644 index 55526cac7b0..00000000000 --- a/.sqlx/query-0f01a5b08dba2c0c7069d295be4f3cf8faad938d4c5076bc339eb11929e606d7.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT g.id,g.name,g.user_id,\n (SELECT count(*) FROM whatsapp_group_message m WHERE m.binding_id = g.id AND m.message_id IS NOT NULL\n AND m.deleted_at IS NULL AND m.origin <> 'history'\n AND NOT EXISTS (SELECT 1 FROM whatsapp_external_person p WHERE p.id = m.person_id AND p.user_id = $2)\n AND (r.message_id IS NULL OR (m.source_at,m.id) > (r.read_at,r.message_id))) AS \"unread_count!\",\n (SELECT max(source_at) FROM whatsapp_group_message WHERE binding_id = g.id AND message_id IS NOT NULL) AS last_message_at\n FROM whatsapp_group g LEFT JOIN whatsapp_group_read_state r ON r.group_id = g.id AND r.user_id = $2\n WHERE g.id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "user_id", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "unread_count!", - "type_info": "Int8" - }, - { - "ordinal": 4, - "name": "last_message_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - null, - null - ] - }, - "hash": "0f01a5b08dba2c0c7069d295be4f3cf8faad938d4c5076bc339eb11929e606d7" -} diff --git a/.sqlx/query-1aa1e6a2988d0f0211ad76883050aaae6f48eee637532ae83e661048b3e4f16f.json b/.sqlx/query-1aa1e6a2988d0f0211ad76883050aaae6f48eee637532ae83e661048b3e4f16f.json deleted file mode 100644 index 1268ccfe7a1..00000000000 --- a/.sqlx/query-1aa1e6a2988d0f0211ad76883050aaae6f48eee637532ae83e661048b3e4f16f.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT g.id,g.name,g.user_id,g.group_jid,g.state,g.adopted_at,\n (SELECT count(*) FROM whatsapp_group_message m WHERE m.binding_id=g.id AND m.message_id IS NOT NULL\n AND m.deleted_at IS NULL AND m.origin<>'history'\n AND NOT EXISTS(SELECT 1 FROM whatsapp_external_person p WHERE p.id=m.person_id AND p.user_id=$1)\n AND (r.message_id IS NULL OR (m.source_at,m.id)>(r.read_at,r.message_id))) AS \"unread_count!\",\n (SELECT max(source_at) FROM whatsapp_group_message WHERE binding_id=g.id AND message_id IS NOT NULL) AS last_message_at,\n roster.metadata AS \"metadata?\"\n FROM whatsapp_group g\n LEFT JOIN whatsapp_group_read_state r ON r.group_id=g.id AND r.user_id=$1\n LEFT JOIN whatsapp_linked_account a ON a.user_id=$1\n LEFT JOIN whatsapp_group_roster roster ON roster.account_id=a.id AND roster.group_jid=g.group_jid\n WHERE whatsapp_group_can_view(g.id,$1)\n AND (r.user_id IS NOT NULL OR EXISTS (SELECT 1 FROM entity_access ea\n WHERE ea.entity_id=g.id AND ea.entity_type='whatsapp_group'\n AND (ea.source_id=$1 OR ea.source_id IN (SELECT team_id::text FROM team_user WHERE user_id=$1)\n OR ea.source_id IN (SELECT channel_id::text FROM comms_channel_participants WHERE user_id=$1 AND left_at IS NULL))))\n ORDER BY g.adopted_at DESC LIMIT 2000", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "user_id", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "group_jid", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "state", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "adopted_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "unread_count!", - "type_info": "Int8" - }, - { - "ordinal": 7, - "name": "last_message_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "metadata?", - "type_info": "Jsonb" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - null, - null, - false - ] - }, - "hash": "1aa1e6a2988d0f0211ad76883050aaae6f48eee637532ae83e661048b3e4f16f" -} diff --git a/.sqlx/query-24e02b9f9df426d070d8f52a6a167e94f381ab8457c515c42287830741b73c7c.json b/.sqlx/query-24e02b9f9df426d070d8f52a6a167e94f381ab8457c515c42287830741b73c7c.json new file mode 100644 index 00000000000..8078d0dd53c --- /dev/null +++ b/.sqlx/query-24e02b9f9df426d070d8f52a6a167e94f381ab8457c515c42287830741b73c7c.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, backfill_imported, backfill_cursor, adopted_at FROM whatsapp_group\n WHERE user_id = $1 AND group_jid = $2 AND state = 'active'\n AND backfill_state IN ('pending', 'running', 'waiting_for_phone')\n ORDER BY id FOR UPDATE", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "backfill_imported", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "backfill_cursor", + "type_info": "Jsonb" + }, + { + "ordinal": 3, + "name": "adopted_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + true, + false + ] + }, + "hash": "24e02b9f9df426d070d8f52a6a167e94f381ab8457c515c42287830741b73c7c" +} diff --git a/.sqlx/query-25a3546a1d143befc191768ce7ca378bf2817eeec4629ae8b7b30aea1bcfe06c.json b/.sqlx/query-25a3546a1d143befc191768ce7ca378bf2817eeec4629ae8b7b30aea1bcfe06c.json new file mode 100644 index 00000000000..d8fd852bebb --- /dev/null +++ b/.sqlx/query-25a3546a1d143befc191768ce7ca378bf2817eeec4629ae8b7b30aea1bcfe06c.json @@ -0,0 +1,25 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE whatsapp_group g SET backfill_state = $3,\n backfill_requested_at = CASE WHEN $4 THEN now() ELSE g.backfill_requested_at END\n FROM (SELECT id, backfill_state FROM whatsapp_group WHERE id = $1 FOR UPDATE) old\n WHERE g.id = old.id AND g.user_id = $2\n AND g.backfill_state IN ('pending', 'running', 'waiting_for_phone')\n RETURNING old.backfill_state AS \"previous!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "previous!", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Bool" + ] + }, + "nullable": [ + true + ] + }, + "hash": "25a3546a1d143befc191768ce7ca378bf2817eeec4629ae8b7b30aea1bcfe06c" +} diff --git a/.sqlx/query-32c807a8fae0af855391049ebc09c800888830ae050c4647a795c842e60f9d0c.json b/.sqlx/query-32c807a8fae0af855391049ebc09c800888830ae050c4647a795c842e60f9d0c.json new file mode 100644 index 00000000000..42c9511e297 --- /dev/null +++ b/.sqlx/query-32c807a8fae0af855391049ebc09c800888830ae050c4647a795c842e60f9d0c.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT g.id,g.name,g.user_id,g.backfill_state,g.backfill_imported,\n (SELECT count(*) FROM whatsapp_group_message m WHERE m.binding_id = g.id AND m.message_id IS NOT NULL\n AND m.deleted_at IS NULL AND m.origin <> 'history'\n AND NOT EXISTS (SELECT 1 FROM whatsapp_external_person p WHERE p.id = m.person_id AND p.user_id = $2)\n AND (r.message_id IS NULL OR (m.source_at,m.id) > (r.read_at,r.message_id))) AS \"unread_count!\",\n (SELECT max(source_at) FROM whatsapp_group_message WHERE binding_id = g.id AND message_id IS NOT NULL) AS last_message_at\n FROM whatsapp_group g LEFT JOIN whatsapp_group_read_state r ON r.group_id = g.id AND r.user_id = $2\n WHERE g.id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "backfill_state", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "backfill_imported", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "unread_count!", + "type_info": "Int8" + }, + { + "ordinal": 6, + "name": "last_message_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + true, + false, + null, + null + ] + }, + "hash": "32c807a8fae0af855391049ebc09c800888830ae050c4647a795c842e60f9d0c" +} diff --git a/.sqlx/query-623edc4d3e056156b23a4d38687772408a07e15b7bd395cee96bfd3c8f326fd0.json b/.sqlx/query-623edc4d3e056156b23a4d38687772408a07e15b7bd395cee96bfd3c8f326fd0.json new file mode 100644 index 00000000000..2ebcb790e3e --- /dev/null +++ b/.sqlx/query-623edc4d3e056156b23a4d38687772408a07e15b7bd395cee96bfd3c8f326fd0.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT chat_jid, kind, name, last_activity_at, archived, pinned FROM whatsapp_chat\n WHERE account_id = $1 ORDER BY last_activity_at DESC NULLS LAST, chat_jid LIMIT 5000", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "chat_jid", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "kind", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "last_activity_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "archived", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "pinned", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + false + ] + }, + "hash": "623edc4d3e056156b23a4d38687772408a07e15b7bd395cee96bfd3c8f326fd0" +} diff --git a/.sqlx/query-7ba6c8eb1f7085b499db0bc85e4e16a04e83825fba90b1ee54aed1dccd2691e3.json b/.sqlx/query-7ba6c8eb1f7085b499db0bc85e4e16a04e83825fba90b1ee54aed1dccd2691e3.json new file mode 100644 index 00000000000..a9137b11463 --- /dev/null +++ b/.sqlx/query-7ba6c8eb1f7085b499db0bc85e4e16a04e83825fba90b1ee54aed1dccd2691e3.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE whatsapp_group SET backfill_state = $2, backfill_imported = $3, backfill_cursor = $4,\n backfill_requested_at = NULL\n WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Int4", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "7ba6c8eb1f7085b499db0bc85e4e16a04e83825fba90b1ee54aed1dccd2691e3" +} diff --git a/.sqlx/query-8d7c3fc8a759d0bc6e72dc9a7c97cf274d7f92f55683af8645dca6ca5125ed46.json b/.sqlx/query-8d7c3fc8a759d0bc6e72dc9a7c97cf274d7f92f55683af8645dca6ca5125ed46.json new file mode 100644 index 00000000000..f431ff62374 --- /dev/null +++ b/.sqlx/query-8d7c3fc8a759d0bc6e72dc9a7c97cf274d7f92f55683af8645dca6ca5125ed46.json @@ -0,0 +1,82 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT g.id,g.name,g.user_id,g.group_jid,g.state,g.adopted_at,g.backfill_state,g.backfill_imported,\n (SELECT count(*) FROM whatsapp_group_message m WHERE m.binding_id=g.id AND m.message_id IS NOT NULL\n AND m.deleted_at IS NULL AND m.origin<>'history'\n AND NOT EXISTS(SELECT 1 FROM whatsapp_external_person p WHERE p.id=m.person_id AND p.user_id=$1)\n AND (r.message_id IS NULL OR (m.source_at,m.id)>(r.read_at,r.message_id))) AS \"unread_count!\",\n (SELECT max(source_at) FROM whatsapp_group_message WHERE binding_id=g.id AND message_id IS NOT NULL) AS last_message_at,\n roster.metadata AS \"metadata?\"\n FROM whatsapp_group g\n LEFT JOIN whatsapp_group_read_state r ON r.group_id=g.id AND r.user_id=$1\n LEFT JOIN whatsapp_linked_account a ON a.user_id=$1\n LEFT JOIN whatsapp_group_roster roster ON roster.account_id=a.id AND roster.group_jid=g.group_jid\n WHERE whatsapp_group_can_view(g.id,$1)\n AND (r.user_id IS NOT NULL OR EXISTS (SELECT 1 FROM entity_access ea\n WHERE ea.entity_id=g.id AND ea.entity_type='whatsapp_group'\n AND (ea.source_id=$1 OR ea.source_id IN (SELECT team_id::text FROM team_user WHERE user_id=$1)\n OR ea.source_id IN (SELECT channel_id::text FROM comms_channel_participants WHERE user_id=$1 AND left_at IS NULL))))\n ORDER BY g.adopted_at DESC LIMIT 2000", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "user_id", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "group_jid", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "state", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "adopted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "backfill_state", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "backfill_imported", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "unread_count!", + "type_info": "Int8" + }, + { + "ordinal": 9, + "name": "last_message_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "metadata?", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + null, + null, + false + ] + }, + "hash": "8d7c3fc8a759d0bc6e72dc9a7c97cf274d7f92f55683af8645dca6ca5125ed46" +} diff --git a/.sqlx/query-feaac846feefd19a0d1e2a3ed8ae574ffdb48d75441cd0ec5f96e0066b1b87c1.json b/.sqlx/query-b4ca13f663d78e9839a95b921f85252fa9069d30fe664ed8abce4271b6027293.json similarity index 78% rename from .sqlx/query-feaac846feefd19a0d1e2a3ed8ae574ffdb48d75441cd0ec5f96e0066b1b87c1.json rename to .sqlx/query-b4ca13f663d78e9839a95b921f85252fa9069d30fe664ed8abce4271b6027293.json index 80ea38c1920..92572982977 100644 --- a/.sqlx/query-feaac846feefd19a0d1e2a3ed8ae574ffdb48d75441cd0ec5f96e0066b1b87c1.json +++ b/.sqlx/query-b4ca13f663d78e9839a95b921f85252fa9069d30fe664ed8abce4271b6027293.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO whatsapp_group (id, group_jid, name, user_id, state, share_permission_id) VALUES ($1,$2,$3,$4,'active',$5) RETURNING id, group_jid, user_id, state, adopted_at", + "query": "INSERT INTO whatsapp_group (id, group_jid, name, user_id, state, share_permission_id, backfill_state) VALUES ($1,$2,$3,$4,'active',$5,'pending') RETURNING id, group_jid, user_id, state, adopted_at", "describe": { "columns": [ { @@ -46,5 +46,5 @@ false ] }, - "hash": "feaac846feefd19a0d1e2a3ed8ae574ffdb48d75441cd0ec5f96e0066b1b87c1" + "hash": "b4ca13f663d78e9839a95b921f85252fa9069d30fe664ed8abce4271b6027293" } diff --git a/.sqlx/query-c412870d6e6e8be6878bfe1bd793ce3292bc74417b2f18890c481265f4f2a1d6.json b/.sqlx/query-c412870d6e6e8be6878bfe1bd793ce3292bc74417b2f18890c481265f4f2a1d6.json new file mode 100644 index 00000000000..872480ff795 --- /dev/null +++ b/.sqlx/query-c412870d6e6e8be6878bfe1bd793ce3292bc74417b2f18890c481265f4f2a1d6.json @@ -0,0 +1,65 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT g.id, g.group_jid, g.backfill_state AS \"state!\", g.backfill_imported, g.backfill_cursor,\n g.adopted_at, g.backfill_requested_at, c.anchor AS \"anchor?\"\n FROM whatsapp_group g\n LEFT JOIN whatsapp_chat c ON c.account_id = $1 AND c.chat_jid = g.group_jid\n WHERE g.user_id = $2 AND g.state = 'active'\n AND g.backfill_state IN ('pending', 'running', 'waiting_for_phone')\n ORDER BY g.adopted_at, g.id LIMIT 20", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "group_jid", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "state!", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "backfill_imported", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "backfill_cursor", + "type_info": "Jsonb" + }, + { + "ordinal": 5, + "name": "adopted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "backfill_requested_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "anchor?", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text" + ] + }, + "nullable": [ + false, + false, + true, + false, + true, + false, + true, + true + ] + }, + "hash": "c412870d6e6e8be6878bfe1bd793ce3292bc74417b2f18890c481265f4f2a1d6" +} diff --git a/.sqlx/query-ca90bf3789dc312e956c7153adab75c8a048dc949b0a59baf4e002f97b36643a.json b/.sqlx/query-ca90bf3789dc312e956c7153adab75c8a048dc949b0a59baf4e002f97b36643a.json new file mode 100644 index 00000000000..52690b2f2af --- /dev/null +++ b/.sqlx/query-ca90bf3789dc312e956c7153adab75c8a048dc949b0a59baf4e002f97b36643a.json @@ -0,0 +1,21 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO whatsapp_chat AS c (account_id, chat_jid, kind, name, last_activity_at, archived, pinned, anchor)\n VALUES ($1, $2, $3, $4, $5, COALESCE($6, false), COALESCE($7, false), $8)\n ON CONFLICT (account_id, chat_jid) DO UPDATE SET\n name = COALESCE(excluded.name, c.name),\n last_activity_at = GREATEST(c.last_activity_at, excluded.last_activity_at),\n archived = COALESCE($6, c.archived),\n pinned = COALESCE($7, c.pinned),\n anchor = CASE WHEN excluded.anchor IS NOT NULL AND (c.anchor IS NULL\n OR (excluded.anchor->>'at')::timestamptz >= (c.anchor->>'at')::timestamptz)\n THEN excluded.anchor ELSE c.anchor END,\n updated_at = now()\n WHERE c.name IS DISTINCT FROM COALESCE(excluded.name, c.name)\n OR c.last_activity_at IS DISTINCT FROM GREATEST(c.last_activity_at, excluded.last_activity_at)\n OR c.archived IS DISTINCT FROM COALESCE($6, c.archived)\n OR c.pinned IS DISTINCT FROM COALESCE($7, c.pinned)\n OR (excluded.anchor IS NOT NULL AND c.anchor IS DISTINCT FROM excluded.anchor AND (c.anchor IS NULL\n OR (excluded.anchor->>'at')::timestamptz >= (c.anchor->>'at')::timestamptz))", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Text", + "Timestamptz", + "Bool", + "Bool", + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "ca90bf3789dc312e956c7153adab75c8a048dc949b0a59baf4e002f97b36643a" +} diff --git a/.sqlx/query-d97f6d326a00dc82c793bee806800fbe5fb9b11e7269931ce956fda4f2f03bec.json b/.sqlx/query-d97f6d326a00dc82c793bee806800fbe5fb9b11e7269931ce956fda4f2f03bec.json new file mode 100644 index 00000000000..4b3f0c991e0 --- /dev/null +++ b/.sqlx/query-d97f6d326a00dc82c793bee806800fbe5fb9b11e7269931ce956fda4f2f03bec.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE whatsapp_chat c SET\n name = COALESCE($3, c.name),\n last_activity_at = GREATEST(c.last_activity_at, $4),\n archived = COALESCE($5, c.archived),\n pinned = COALESCE($6, c.pinned),\n updated_at = now()\n WHERE c.account_id = $1 AND c.chat_jid = $2\n AND (c.name IS DISTINCT FROM COALESCE($3, c.name)\n OR c.last_activity_at IS DISTINCT FROM GREATEST(c.last_activity_at, $4)\n OR c.archived IS DISTINCT FROM COALESCE($5, c.archived)\n OR c.pinned IS DISTINCT FROM COALESCE($6, c.pinned))", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Timestamptz", + "Bool", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "d97f6d326a00dc82c793bee806800fbe5fb9b11e7269931ce956fda4f2f03bec" +} diff --git a/.sqlx/query-68381ba2360701e8c5d30633d243a32d7e5646ec8b2ae66ab0ae3a8a7ded0c95.json b/.sqlx/query-f376b23fdfc65eac1e9043a5eb25fa3e94257cdf185bbfea48e7c4b76c97fbf0.json similarity index 71% rename from .sqlx/query-68381ba2360701e8c5d30633d243a32d7e5646ec8b2ae66ab0ae3a8a7ded0c95.json rename to .sqlx/query-f376b23fdfc65eac1e9043a5eb25fa3e94257cdf185bbfea48e7c4b76c97fbf0.json index 84a962e0c53..76ef2f3628c 100644 --- a/.sqlx/query-68381ba2360701e8c5d30633d243a32d7e5646ec8b2ae66ab0ae3a8a7ded0c95.json +++ b/.sqlx/query-f376b23fdfc65eac1e9043a5eb25fa3e94257cdf185bbfea48e7c4b76c97fbf0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT b.id, b.group_jid, b.user_id, b.state, b.adopted_at\n FROM whatsapp_group b JOIN whatsapp_group_membership m ON m.binding_id = b.id\n WHERE b.group_jid = $1 AND b.state = 'active'\n AND m.account_id = $2 AND m.generation = $3 AND m.consent_ended_at IS NULL\n AND m.remote_member AND m.reconciled\n AND $4 >= date_trunc('second', b.adopted_at) - make_interval(secs => $6)\n AND $4 >= date_trunc('second', m.consent_at) - make_interval(secs => $6)\n AND whatsapp_group_can_view(b.id, $5)\n ORDER BY b.id LIMIT 100", + "query": "SELECT b.id, b.group_jid, b.user_id, b.state, b.adopted_at\n FROM whatsapp_group b JOIN whatsapp_group_membership m ON m.binding_id = b.id\n WHERE b.group_jid = $1 AND b.state = 'active'\n AND m.account_id = $2 AND m.generation = $3 AND m.consent_ended_at IS NULL\n AND m.remote_member AND m.reconciled\n AND CASE WHEN $7 THEN b.user_id = $5 ELSE\n $4 >= date_trunc('second', b.adopted_at) - make_interval(secs => $6)\n AND $4 >= date_trunc('second', m.consent_at) - make_interval(secs => $6) END\n AND whatsapp_group_can_view(b.id, $5)\n ORDER BY b.id LIMIT 100", "describe": { "columns": [ { @@ -36,7 +36,8 @@ "Int8", "Timestamptz", "Text", - "Float8" + "Float8", + "Bool" ] }, "nullable": [ @@ -47,5 +48,5 @@ false ] }, - "hash": "68381ba2360701e8c5d30633d243a32d7e5646ec8b2ae66ab0ae3a8a7ded0c95" + "hash": "f376b23fdfc65eac1e9043a5eb25fa3e94257cdf185bbfea48e7c4b76c97fbf0" } diff --git a/Cargo.lock b/Cargo.lock index c1670e3cbce..43f01d6127c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21602,6 +21602,7 @@ dependencies = [ "axum", "base64 0.22.1", "chrono", + "flate2", "futures", "hmac 0.12.1", "nom 8.0.0", diff --git a/apps/web/src/features/external-view/components/in-macro-section.test.tsx b/apps/web/src/features/external-view/components/in-macro-section.test.tsx index 4eaca027bae..123843d3a23 100644 --- a/apps/web/src/features/external-view/components/in-macro-section.test.tsx +++ b/apps/web/src/features/external-view/components/in-macro-section.test.tsx @@ -6,7 +6,7 @@ import { within, } from '@solidjs/testing-library'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; -import { InMacroSection } from './in-macro-section'; +import { InMacroGroups } from './in-macro-section'; beforeEach(() => { vi.stubGlobal( @@ -26,7 +26,7 @@ afterEach(() => { it('lists readable groups with unread counts and marks read-only ones', () => { const onOpenGroup = vi.fn(); render(() => ( - { isPending={false} isError={false} activeGroupId="group-family" - open - onOpenChange={vi.fn()} onOpenGroup={onOpenGroup} onRetry={vi.fn()} /> diff --git a/apps/web/src/features/external-view/components/in-macro-section.tsx b/apps/web/src/features/external-view/components/in-macro-section.tsx index d89140b4410..6ad61415c00 100644 --- a/apps/web/src/features/external-view/components/in-macro-section.tsx +++ b/apps/web/src/features/external-view/components/in-macro-section.tsx @@ -1,9 +1,10 @@ -import { CollapsibleSection, ViewSidebar } from '@app/components/view-shell'; +import { ViewSidebar } from '@app/components/view-shell'; import type { GroupSummary } from '@app/features/whatsapp-group/core/group'; +import UsersThreeIcon from '@phosphor/users-three.svg'; import { Button } from '@ui'; import { For, Match, Show, Switch } from 'solid-js'; -function UnreadCount(props: { count: number }) { +export function UnreadCount(props: { count: number }) { return ( 0}> + + + 0 }} @@ -46,72 +50,58 @@ function GroupRow(props: { ); } -/** WhatsApp groups this viewer can read in Macro: their own and shared ones. */ -export function InMacroSection(props: { +/** + * WhatsApp groups this viewer can read in Macro, their own and shared ones, + * as rows nested under WhatsApp in External's sidebar. + */ +export function InMacroGroups(props: { groups: GroupSummary[] | undefined; isPending: boolean; isError: boolean; activeGroupId: string | undefined; - open: boolean; - onOpenChange: (open: boolean) => void; onOpenGroup: (groupId: string) => void; onRetry: () => void; }) { - const unread = () => - (props.groups ?? []).reduce((sum, group) => sum + group.unreadCount, 0); return ( - - - - In Macro - - - - - - - - - - {(groups) => ( - - - No WhatsApp groups in Macro yet -

- } - > - {(group) => ( - props.onOpenGroup(group.id)} - /> - )} -
-
- )} -
- -

- Loading groups… -

-
- -
- Couldn't load your groups. - -
-
-
-
-
+ + + {(groups) => ( + + + No groups added yet +

+ } + > + {(group) => ( + props.onOpenGroup(group.id)} + /> + )} +
+
+ )} +
+ +

+ Loading groups… +

+
+ +
+ Couldn't load your groups. + +
+
+
); } diff --git a/apps/web/src/features/external-view/external-view.tsx b/apps/web/src/features/external-view/external-view.tsx index abfe40f29ff..66894ee0f4b 100644 --- a/apps/web/src/features/external-view/external-view.tsx +++ b/apps/web/src/features/external-view/external-view.tsx @@ -16,7 +16,7 @@ import { createSignal, Show } from 'solid-js'; import { z } from 'zod'; import { whatsappGroupApi } from '../whatsapp-group/queries/production-api'; import { WhatsAppLinked } from '../whatsapp-linked/whatsapp-linked'; -import { InMacroSection } from './components/in-macro-section'; +import { InMacroGroups, UnreadCount } from './components/in-macro-section'; /** * External: accounts on outside messaging apps that bring conversations into @@ -61,19 +61,19 @@ export function ExternalView() { navigate({ route: externalRoute, params: {} }), 'external-view' ); - const [inMacroOpen, setInMacroOpen] = createSignal(true); - const inMacroSection = () => ( - ( + fireAndForget(groups.refetch(), 'external-view')} /> ); + const unread = () => + (inMacro() ?? []).reduce((sum, group) => sum + group.unreadCount, 0); const whatsappPage = () => ( <> @@ -82,14 +82,22 @@ export function ExternalView() { WhatsApp - + {/* The top bar is desktop chrome; touch layouts title the page here. */} -

+

WhatsApp

{/* Touch has no sidebar: the groups in Macro lead the page. */} -
{inMacroSection()}
+
+

+ Groups +

+ {inMacroGroups()} +
@@ -114,18 +122,25 @@ export function ExternalView() { - setGroupsOpen((open) => !open)} onClick={openWhatsApp} > - + - WhatsApp - + WhatsApp + + + + + + {inMacroGroups()} + - {inMacroSection()} diff --git a/apps/web/src/features/notifications/use-notification-updates.ts b/apps/web/src/features/notifications/use-notification-updates.ts index 86257b3e859..03235771bf7 100644 --- a/apps/web/src/features/notifications/use-notification-updates.ts +++ b/apps/web/src/features/notifications/use-notification-updates.ts @@ -12,7 +12,6 @@ import { refetchSoupEntity, } from '@queries/soup/normalized-cache'; import { teamKeys } from '@queries/team/keys'; -import { invalidateWhatsappGroups } from '@queries/whatsapp-groups'; import { onCleanup } from 'solid-js'; import { match, P } from 'ts-pattern'; import { shareEntity } from '../access-requests/core/types'; @@ -155,9 +154,8 @@ export function handleNotificationUpdate(notification: UnifiedNotification) { .with({ tag: 'call_started' }, () => { refreshChannel(notification); }) - .with({ tag: 'whatsapp_group_message' }, () => { - fireAndForget(invalidateWhatsappGroups(), 'whatsapp-group.notification'); - }) + // DSS's `whatsapp_changed` push already refreshes what an open page shows. + .with({ tag: 'whatsapp_group_message' }, () => {}) .with({ tag: 'new_email' }, () => { refreshEmailThread(notification); }) diff --git a/apps/web/src/features/whatsapp-group/AGENTS.md b/apps/web/src/features/whatsapp-group/AGENTS.md index ba2c8794ccb..b0d8110ab65 100644 --- a/apps/web/src/features/whatsapp-group/AGENTS.md +++ b/apps/web/src/features/whatsapp-group/AGENTS.md @@ -36,6 +36,15 @@ lists belong to `features/whatsapp-linked` and `features/external-view`. - Row actions are getters: `ChannelMessage` reads them once per row. - Reads do not poll; DSS pushes `whatsapp_changed`, with one fallback re-fetch 3 s after opening. Only in-flight sends poll their progress. +- History imported after adding (`backfill` on the detail) lands older than + the rows shown; the view keeps its distance from the bottom when rows appear + above, and says "Importing history…" / "Waiting for … phone" until DSS + reports `done` or `partial`. +- Images, video and audio play inline: `fileUrl` resolves the imported + document's presigned URL and `InboundMedia` hands it to Chat's + `MediaPreview` (or an audio player); `groupMessageData` keeps them out of + the document chips. DSS sends no dimensions, so images size on load. +- Senders show their WhatsApp name (`imported_author`), Macro users included. - Use backend limits for text, files, edits and deletes. - Keep phone numbers and message bodies out of telemetry. - Soup/search registration is deferred; External is the list entry point. diff --git a/apps/web/src/features/whatsapp-group/core/group.ts b/apps/web/src/features/whatsapp-group/core/group.ts index 70a0b17e2b2..27dfdbb1673 100644 --- a/apps/web/src/features/whatsapp-group/core/group.ts +++ b/apps/web/src/features/whatsapp-group/core/group.ts @@ -9,6 +9,15 @@ export type GroupSummary = { /** False for viewers who can only read. */ canSend: boolean; }; +/** + * Importing the chat's earlier messages from the owner's phone after it was + * added. Absent for groups added before imports existed. + */ +export type GroupBackfill = { + state: 'pending' | 'running' | 'waiting_for_phone' | 'done' | 'partial'; + /** Messages the phone sent, including ones already shown. */ + imported: number; +}; export type GroupDetail = { id: string; name: string; @@ -20,6 +29,7 @@ export type GroupDetail = { lastReadMessageId?: string; unreadCount: number; members: { id: string; name: string; admin: boolean }[]; + backfill?: GroupBackfill; limits: { textBytes: number; attachmentBytes: number; @@ -119,6 +129,13 @@ export function composerNote(group: GroupDetail): string { .otherwise(() => 'Sending to WhatsApp is unavailable right now'); } +/** "Message Test 11 as +1…": the composer says who is sending. */ +export function composerPlaceholder(group: GroupDetail): string { + return group.phone + ? `Message ${group.name} as ${group.phone}` + : `Message ${group.name}`; +} + /** A send's outcome after DSS recorded it. */ export function recordedOutcome( progress: SendProgress @@ -160,3 +177,30 @@ function deliveryFailure(progress: SendProgress): string { ) .otherwise(() => "WhatsApp didn't accept this message."); } + +/** While history is still coming from the owner's phone; quiet once it settled. */ +export function backfillStatus( + backfill: GroupBackfill | undefined, + /** The import runs on the owner's phone; others are told whose. */ + viewerOwns: boolean +): string | undefined { + if (!backfill) return undefined; + return match(backfill.state) + .with(P.union('pending', 'running'), () => 'Importing history…') + .with('waiting_for_phone', () => + viewerOwns + ? 'Waiting for your phone to import history' + : "Waiting for the owner's phone to import history" + ) + .with(P.union('done', 'partial'), () => undefined) + .exhaustive(); +} + +/** Shown above the oldest message once the import stopped at its limit. */ +export function historyStartNote( + backfill: GroupBackfill | undefined +): string | undefined { + return backfill?.state === 'partial' + ? 'Earlier messages stay in WhatsApp' + : undefined; +} diff --git a/apps/web/src/features/whatsapp-group/queries/capabilities.ts b/apps/web/src/features/whatsapp-group/queries/capabilities.ts index a4aa4c0f04c..2e9869b1047 100644 --- a/apps/web/src/features/whatsapp-group/queries/capabilities.ts +++ b/apps/web/src/features/whatsapp-group/queries/capabilities.ts @@ -3,8 +3,8 @@ import { Telemetry } from '@macro-inc/observability'; import { queryReadyGate } from '@queries/gate'; import { useBinaryDocumentQuery } from '@queries/storage/binary-document'; import { + invalidateWhatsappGroup, invalidateWhatsappGroupList, - invalidateWhatsappGroups, refreshWhatsappGroupTimeline, useWhatsappGroupDeliveryQuery, useWhatsappGroupQueries, @@ -160,7 +160,7 @@ export function createWhatsAppGroupCapabilities( () => api.stopSync(groupId()), groupId() ); - await invalidateWhatsappGroups(); + invalidateWhatsappGroup(groupId()); }, }; } diff --git a/apps/web/src/features/whatsapp-group/queries/production-api.ts b/apps/web/src/features/whatsapp-group/queries/production-api.ts index 45edd8a13bd..decaa8906d7 100644 --- a/apps/web/src/features/whatsapp-group/queries/production-api.ts +++ b/apps/web/src/features/whatsapp-group/queries/production-api.ts @@ -145,6 +145,9 @@ export const whatsappGroupApi: WhatsAppGroupApi = { 'WhatsApp participant', admin: person.admin, })), + backfill: view.backfill + ? { state: view.backfill.state, imported: view.backfill.imported } + : undefined, limits: { textBytes: view.limits.text_limit_bytes, attachmentBytes: view.limits.attachment_limit_bytes, diff --git a/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.test.tsx b/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.test.tsx index fc35f5e502f..6fe7304fae8 100644 --- a/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.test.tsx +++ b/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.test.tsx @@ -11,7 +11,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'; import { err } from 'neverthrow'; import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { WhatsAppGroupProvider } from '../context/whatsapp-group-context'; -import { createFakeGroup, group } from '../tests/fake-capabilities'; +import { createFakeGroup, group, messages } from '../tests/fake-capabilities'; import { WhatsAppGroupView } from './whatsapp-group-view'; const mocks = vi.hoisted(() => ({ @@ -70,7 +70,11 @@ function mount(fake: ReturnType) { + Share + + } onConnect={connect} /> @@ -86,7 +90,10 @@ it('renders WhatsApp messages as channel rows with quotes and reactions', () => const fake = createFakeGroup(); const { share } = mount(fake); expect(screen.getByText('Ana Ruiz')).toBeTruthy(); - expect(screen.getByText('Sending as +14155550142')).toBeTruthy(); + // The composer says who is sending. + expect(mocks.composer?.input.placeholder).toBe( + 'Message Design Crit as +14155550142' + ); // WhatsApp's `*updated*` renders bold. expect(screen.getByText('updated').className).toContain('font-bold'); expect( @@ -184,3 +191,89 @@ it('offers actions on rows that rendered before the viewer could send', () => { fireEvent.pointerEnter(row); expect(screen.getByRole('button', { name: 'Reply' })).toBeTruthy(); }); + +it('says while history imports and shows older messages as they land', () => { + const fake = createFakeGroup({ + group: { ...group, backfill: { state: 'running', imported: 0 } }, + messages: [messages[1]], + }); + mount(fake); + expect(screen.getByText('WhatsApp · Importing history…')).toBeTruthy(); + expect(screen.getByRole('status').textContent).toBe('Importing history…'); + + fake.setGroup({ + ...group, + backfill: { state: 'waiting_for_phone', imported: 1 }, + }); + fake.setMessages(messages); + expect(screen.getByRole('status').textContent).toBe( + 'Waiting for your phone to import history' + ); + const ids = [...document.querySelectorAll('[data-message-id]')].map((row) => + row.getAttribute('data-message-id') + ); + expect(ids).toEqual(['message-1', 'message-2']); + + fake.setGroup({ ...group, backfill: { state: 'done', imported: 1 } }); + expect(screen.queryByText(/Importing history|Waiting for/)).toBeNull(); + expect(screen.getByText('WhatsApp').textContent).toBe('WhatsApp'); + + fake.setGroup({ ...group, backfill: { state: 'partial', imported: 500 } }); + expect(screen.getByText('Earlier messages stay in WhatsApp')).toBeTruthy(); +}); + +it('tells a shared viewer whose phone the import waits for', () => { + const fake = createFakeGroup({ + group: { + ...group, + accessLevel: 'view', + canSend: false, + sendReason: 'no_account', + backfill: { state: 'waiting_for_phone', imported: 0 }, + }, + messages: [], + }); + mount(fake); + expect( + screen.getAllByText(/Waiting for the owner's phone to import history/) + ).toHaveLength(2); + // Not "No messages yet" while history is still coming. + expect(screen.queryByText(/No messages yet/)).toBeNull(); +}); + +it('plays imported images and voice notes inline, like Chat', () => { + const fake = createFakeGroup({ + messages: [ + { + ...messages[0], + body: '', + attachments: [ + { + ordinal: 0, + state: 'ready', + fileName: 'photo.jpeg', + mime: 'image/jpeg', + documentId: 'document-photo', + }, + { + ordinal: 1, + state: 'ready', + fileName: 'voice.ogg', + mime: 'audio/ogg', + documentId: 'document-voice', + }, + ], + }, + ], + }); + mount(fake); + expect( + screen.getByRole('button', { name: 'Open image viewer' }) + ).toBeTruthy(); + const voice = screen.getByLabelText('voice.ogg'); + expect(voice.getAttribute('src')).toBe( + 'https://files.example.test/document-voice' + ); + // Not as document chips. + expect(screen.queryByText('photo.jpeg')).toBeNull(); +}); diff --git a/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.tsx b/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.tsx index a27ff6a4d6a..e2d185f32f5 100644 --- a/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.tsx +++ b/apps/web/src/features/whatsapp-group/views/whatsapp-group-view.tsx @@ -9,6 +9,7 @@ import { DateDivider } from '@channel/Message/DateDivider'; import { NewDivider } from '@channel/Message/NewDivider'; import { SenderName } from '@channel/Message/SenderName'; import { MaybeMessageActionDrawerManager } from '@channel/Mobile/MessageActionDrawerManager'; +import { HeaderActionButton } from '@components/app/HeaderActionButton'; import { FloatRegionOrInline } from '@components/app/mobile/float-regions/FloatRegion'; import { StaticMarkdownContext } from '@core/component/LexicalMarkdown/component/core/StaticMarkdown'; import { toast } from '@core/component/Toast/Toast'; @@ -17,6 +18,8 @@ import type { MessageActions, } from '@core/messages/types'; import { fireAndForget } from '@core/util/fireAndForget'; +import UsersThreeIcon from '@phosphor/users-three.svg'; +import WhatsappIcon from '@phosphor/whatsapp-logo.svg'; import { Button, ConfirmDialog } from '@ui'; import { createEffect, @@ -24,17 +27,26 @@ import { createSignal, For, type JSX, + mapArray, onCleanup, onMount, Show, } from 'solid-js'; import { InboundAttachmentPlaceholder } from '../components/inbound-attachment'; +import { + InboundMedia, + type InboundMediaFile, +} from '../components/inbound-media'; import { useWhatsAppGroup } from '../context/whatsapp-group-context'; +import { type InboundAttachment, inlineMediaKind } from '../core/attachments'; import { whatsappToMarkdown } from '../core/formatting'; import { + backfillStatus, canChangeMessage, composerNote, + composerPlaceholder, type GroupMessage, + historyStartNote, } from '../core/group'; import { groupMessageData, senderKey } from '../core/message-data'; import { @@ -59,10 +71,38 @@ function quotePreview(message: GroupMessage) { return message.body || message.attachments[0]?.fileName || 'Attachment'; } +/** A message's images, video and voice notes, played inline. */ +function MessageMedia(props: { attachments: InboundAttachment[] }) { + const source = useWhatsAppGroup(); + const media = createMemo(() => + props.attachments.flatMap((attachment) => { + const kind = inlineMediaKind(attachment); + return kind && attachment.documentId + ? [{ attachment, kind, documentId: attachment.documentId }] + : []; + }) + ); + const files = mapArray(media, (file) => { + const url = source.fileUrl(() => file.documentId); + return (): InboundMediaFile => ({ + id: file.documentId, + kind: file.kind, + fileName: file.attachment.fileName, + url: url.data, + }); + }); + return ( + 0}> + file())} /> + + ); +} + export function WhatsAppGroupView(props: { userId?: string; leading?: JSX.Element; - onShare: () => void; + /** The Share control; the host owns the share dialog. */ + share: JSX.Element; onConnect: () => void; }) { const source = useWhatsAppGroup(); @@ -90,6 +130,10 @@ export function WhatsAppGroupView(props: { let scroller: HTMLDivElement | undefined; let alive = true; let newest: string | undefined; + let oldest: string | undefined; + // Distance from the bottom at the last scroll, to hold the view in place + // when imported history lands above it. + let fromBottom = 0; onCleanup(() => { alive = false; }); @@ -178,6 +222,28 @@ export function WhatsAppGroupView(props: { markRead(); }); }); + createEffect(() => { + const first = rows()[0]?.id; + const grewAbove = !!oldest && !!first && first !== oldest; + oldest = first; + if (!grewAbove || !scroller) return; + scroller.scrollTop = + scroller.scrollHeight - scroller.clientHeight - fromBottom; + }); + const memberLabel = () => { + const count = source.group.data?.members.length; + if (count === undefined) return 'Members'; + return count === 1 ? '1 member' : `${count} members`; + }; + const importing = () => + backfillStatus( + source.group.data?.backfill, + source.group.data?.accessLevel === 'owner' + ); + const historyStart = () => + source.hasOlder() + ? undefined + : historyStartNote(source.group.data?.backfill); const older = async () => { const height = scroller?.scrollHeight ?? 0; const offset = scroller?.scrollTop ?? 0; @@ -276,29 +342,32 @@ export function WhatsAppGroupView(props: { class="flex h-full min-h-0 min-w-0 flex-col bg-panel text-ink touch:pt-(--mobile-content-inset-top)" style={{ '--user-icon-width': '32px', '--message-padding-x': '16px' }} > -
+
{props.leading} -
-

+ +
+

{source.group.data?.name ?? 'WhatsApp group'}

- WhatsApp + + WhatsApp + {(status) => <> · {status()}} + +
+
+ } + tooltip="WhatsApp members" + onClick={() => setRoster((value) => !value)} + /> + {props.share}
- -