db: rejoining a channel starts read state caught up

A rejoin reset only removed_at, so the unread position fell back to the
first joined_at and counted messages from while the member was away.
Reset joined_at on reactivation at every membership upsert; an active
member's upsert (a role change) keeps it.

Signed-off-by: hMurderbot <219c58afff8ac2c76e4e527a329aa61c2ebbb896786b1916d4f9cca5140ecd04@buzz.block.builderlab.xyz>
This commit is contained in:
hMurderbot
2026-10-06 18:30:13 -07:00
parent dceafd3ee7
commit c0bf08ff18
4 changed files with 64 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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() {