diff --git a/crates/buzz-db/src/store/channel.rs b/crates/buzz-db/src/store/channel.rs index 5e89b1101b..1ad48ca5c5 100644 --- a/crates/buzz-db/src/store/channel.rs +++ b/crates/buzz-db/src/store/channel.rs @@ -150,6 +150,9 @@ pub async fn create_channel( INSERT INTO channel_members (community_id, channel_id, pubkey, role, invited_by) VALUES ($1, $2, $3, 'owner', $4) ON CONFLICT (community_id, channel_id, pubkey) DO UPDATE SET + -- A rejoin starts caught up: unread counts from here. + joined_at = CASE WHEN channel_members.removed_at IS NULL + THEN channel_members.joined_at ELSE now() END, removed_at = NULL, removed_by = NULL, role = EXCLUDED.role @@ -249,6 +252,9 @@ pub async fn create_channel_with_id( INSERT INTO channel_members (community_id, channel_id, pubkey, role, invited_by) VALUES ($1, $2, $3, 'owner', $4) ON CONFLICT (community_id, channel_id, pubkey) DO UPDATE SET + -- A rejoin starts caught up: unread counts from here. + joined_at = CASE WHEN channel_members.removed_at IS NULL + THEN channel_members.joined_at ELSE now() END, removed_at = NULL, removed_by = NULL, role = EXCLUDED.role diff --git a/crates/buzz-db/src/store/channel_members.rs b/crates/buzz-db/src/store/channel_members.rs index c369bf59a0..fc4967e601 100644 --- a/crates/buzz-db/src/store/channel_members.rs +++ b/crates/buzz-db/src/store/channel_members.rs @@ -257,6 +257,9 @@ pub async fn insert_auto_membership_in_transaction( INSERT INTO channel_members (community_id, channel_id, pubkey, role, invited_by) VALUES ($1, $2, $3, 'member'::member_role, $4) ON CONFLICT (community_id, channel_id, pubkey) DO UPDATE SET + -- A rejoin starts caught up: unread counts from here. + joined_at = CASE WHEN channel_members.removed_at IS NULL + THEN channel_members.joined_at ELSE now() END, removed_at = NULL, removed_by = NULL, role = EXCLUDED.role @@ -601,6 +604,9 @@ pub async fn add_member( INSERT INTO channel_members (community_id, channel_id, pubkey, role, invited_by) VALUES ($1, $2, $3, $4::member_role, $5) ON CONFLICT (community_id, channel_id, pubkey) DO UPDATE SET + -- A rejoin starts caught up: unread counts from here. + joined_at = CASE WHEN channel_members.removed_at IS NULL + THEN channel_members.joined_at ELSE now() END, removed_at = NULL, removed_by = NULL, role = EXCLUDED.role diff --git a/crates/buzz-db/src/store/dm.rs b/crates/buzz-db/src/store/dm.rs index 89e4a0e522..34693bb716 100644 --- a/crates/buzz-db/src/store/dm.rs +++ b/crates/buzz-db/src/store/dm.rs @@ -187,6 +187,9 @@ pub async fn create_dm( INSERT INTO channel_members (community_id, channel_id, pubkey, role, invited_by) VALUES ($1, $2, $3, 'member', $4) ON CONFLICT (community_id, channel_id, pubkey) DO UPDATE SET + -- A rejoin starts caught up: unread counts from here. + joined_at = CASE WHEN channel_members.removed_at IS NULL + THEN channel_members.joined_at ELSE now() END, removed_at = NULL, removed_by = NULL, role = EXCLUDED.role diff --git a/crates/buzz-db/src/store/personal_read/postgres_tests.rs b/crates/buzz-db/src/store/personal_read/postgres_tests.rs index 705a714226..41b16a7376 100644 --- a/crates/buzz-db/src/store/personal_read/postgres_tests.rs +++ b/crates/buzz-db/src/store/personal_read/postgres_tests.rs @@ -315,6 +315,55 @@ async fn joining_starts_caught_up_and_counts_only_what_arrives_after() { ); } +#[tokio::test] +#[ignore = "requires Postgres"] +async fn rejoining_starts_caught_up_again() { + let (db, pool, community, channel, owner, _) = fixture().await; + let member = Keys::generate(); + let pk = member.public_key().to_bytes(); + start(&pool, community, &member).await; + db.add_member(community, channel, &pk, MemberRole::Member, None) + .await + .unwrap(); + sqlx::query( + "UPDATE channel_members SET joined_at=now()-interval '1 hour' + WHERE community_id=$1 AND channel_id=$2 AND pubkey=$3", + ) + .bind(community.as_uuid()) + .bind(channel) + .bind(pk.as_slice()) + .execute(&pool) + .await + .unwrap(); + db.remove_member(community, channel, &pk, &pk) + .await + .unwrap(); + let away = post(&db, community, channel, &Keys::generate(), now(), vec![]).await; + // Re-adding an active member (a role change) must not move the position. + let owner_pk = owner.public_key().to_bytes(); + db.add_member( + community, + channel, + &owner_pk, + MemberRole::Owner, + Some(&owner_pk), + ) + .await + .unwrap(); + assert_eq!(sidebar(&db, community, &owner).await.unread, 2); + db.add_member(community, channel, &pk, MemberRole::Member, None) + .await + .unwrap(); + let row = sidebar(&db, community, &member).await; + assert_eq!(row.unread, 0, "messages from while away are read"); + assert_eq!( + status(&db, community, &member, channel, None, &[&away]).await, + ["read"] + ); + post(&db, community, channel, &Keys::generate(), now(), vec![]).await; + assert_eq!(sidebar(&db, community, &member).await.unread, 1); +} + #[tokio::test] #[ignore = "requires Postgres"] async fn read_state_starts_caught_up_at_the_first_intent() {