fix: chat UI
This commit is contained in:
@@ -43,7 +43,8 @@ func testChatService(t *testing.T) (*ChatService, *repository.ChatRepo, *reposit
|
||||
// Nil realtime/R2/cache clients make publishing, uploads and caching
|
||||
// no-ops — these tests are about the rules, and none of those services are
|
||||
// reachable from a test run.
|
||||
return NewChatService(chat, repository.NewBlockRepo(pg), repository.NewPostRepo(pg), nil, nil, nil, zap.NewNop()), chat, pg
|
||||
return NewChatService(chat, repository.NewBlockRepo(pg), repository.NewPostRepo(pg),
|
||||
nil, nil, nil, nil, nil, nil, zap.NewNop()), chat, pg
|
||||
}
|
||||
|
||||
// chatUserWithHandle returns both identifiers: ids for authoring, handles for
|
||||
@@ -1927,7 +1928,7 @@ func TestSharedPosts_ResolutionDoesNotScaleWithShareCount(t *testing.T) {
|
||||
|
||||
pg := &repository.Postgres{Pool: pool}
|
||||
svc := NewChatService(repository.NewChatRepo(pg), repository.NewBlockRepo(pg),
|
||||
repository.NewPostRepo(pg), nil, nil, nil, zap.NewNop())
|
||||
repository.NewPostRepo(pg), nil, nil, nil, nil, nil, nil, zap.NewNop())
|
||||
|
||||
// measure returns the statement count for reading a thread carrying
|
||||
// `shares` distinct shared posts.
|
||||
@@ -1982,3 +1983,224 @@ func TestSharedPosts_ResolutionDoesNotScaleWithShareCount(t *testing.T) {
|
||||
many, one)
|
||||
}
|
||||
}
|
||||
|
||||
// The nav badge counts threads waiting on you — and mute is meant to stop a
|
||||
// thread demanding attention, so a muted one must not light it.
|
||||
func TestUnreadCount_CountsThreadsAndSkipsMuted(t *testing.T) {
|
||||
svc, _, pg := testChatService(t)
|
||||
ctx := context.Background()
|
||||
me, a, b := chatUser(t, pg, "Me"), chatUser(t, pg, "A"), chatUser(t, pg, "B")
|
||||
|
||||
if n, err := svc.UnreadCount(ctx, me); err != nil || n != 0 {
|
||||
t.Fatalf("fresh account: count = %d, err = %v; want 0", n, err)
|
||||
}
|
||||
|
||||
withA, err := svc.OpenDM(ctx, a, me)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupConv(t, pg, withA.ID)
|
||||
withB, err := svc.OpenDM(ctx, b, me)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupConv(t, pg, withB.ID)
|
||||
|
||||
// Two messages in one thread still count as one waiting thread.
|
||||
for _, body := range []string{"hare", "krsna"} {
|
||||
if _, err := svc.Send(ctx, a, withA.ID, SendRequest{Body: body}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := svc.Send(ctx, b, withB.ID, SendRequest{Body: "hello"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, _ := svc.UnreadCount(ctx, me); n != 2 {
|
||||
t.Errorf("two threads with unread: count = %d, want 2", n)
|
||||
}
|
||||
|
||||
// Muting one drops it from the badge...
|
||||
if _, err := svc.SetMute(ctx, me, withA.ID, MuteAlways); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, _ := svc.UnreadCount(ctx, me); n != 1 {
|
||||
t.Errorf("after muting one: count = %d, want 1", n)
|
||||
}
|
||||
// ...without marking it read: the inbox still shows its unread.
|
||||
inbox, err := svc.Inbox(ctx, me, "chats", 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var mutedUnread int64
|
||||
for _, it := range inbox {
|
||||
if it.ID == withA.ID {
|
||||
mutedUnread = it.Unread
|
||||
}
|
||||
}
|
||||
if mutedUnread != 2 {
|
||||
t.Errorf("muted thread's inbox unread = %d, want 2 — mute silences, it does not read", mutedUnread)
|
||||
}
|
||||
|
||||
// Reading the other clears the badge entirely.
|
||||
if err := svc.MarkRead(ctx, me, withB.ID, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, _ := svc.UnreadCount(ctx, me); n != 0 {
|
||||
t.Errorf("after reading: count = %d, want 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
// A chat push must not fire when the thread is muted or the user opted out.
|
||||
// Both are the difference between a useful alert and an uninstall.
|
||||
func TestChatPush_SuppressedByMuteAndPreference(t *testing.T) {
|
||||
svc, _, pg := testChatService(t)
|
||||
ctx := context.Background()
|
||||
prefs := repository.NewNotificationPrefsRepo(pg)
|
||||
|
||||
t.Run("muted thread does not push", func(t *testing.T) {
|
||||
me, them := chatUser(t, pg, "Me"), chatUser(t, pg, "Them")
|
||||
conv, err := svc.OpenDM(ctx, them, me)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupConv(t, pg, conv.ID)
|
||||
if _, err := svc.SetMute(ctx, me, conv.ID, MuteAlways); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cands, err := svc.chat.PushCandidates(ctx, conv.ID, them)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range cands {
|
||||
if c.UserID == me && (c.MutedUntil == nil || !c.MutedUntil.After(time.Now())) {
|
||||
t.Error("muted participant should carry a future muted_until")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("push opt-out is recorded for the chat kind", func(t *testing.T) {
|
||||
me := chatUser(t, pg, "Me")
|
||||
if err := prefs.Disable(ctx, me,
|
||||
[]string{NotifChatMessage}, []string{repository.ChannelPush}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
off, err := prefs.DisabledSet(ctx, me)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !prefOff(off, NotifChatMessage, repository.ChannelPush) {
|
||||
t.Error("disabling Messages→Push should be visible to the chat push path")
|
||||
}
|
||||
// Turning off chat push must not silence anything else.
|
||||
if prefOff(off, NotifPostLike, repository.ChannelPush) {
|
||||
t.Error("disabling chat push also disabled post likes")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sender is never a push candidate", func(t *testing.T) {
|
||||
me, them := chatUser(t, pg, "Me"), chatUser(t, pg, "Them")
|
||||
conv, err := svc.OpenDM(ctx, me, them)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupConv(t, pg, conv.ID)
|
||||
cands, err := svc.chat.PushCandidates(ctx, conv.ID, me)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range cands {
|
||||
if c.UserID == me {
|
||||
t.Error("the sender must not be pushed their own message")
|
||||
}
|
||||
}
|
||||
if len(cands) != 1 {
|
||||
t.Errorf("candidates = %d, want 1 (the recipient)", len(cands))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a departed member stops receiving push", func(t *testing.T) {
|
||||
owner, a, b := chatUser(t, pg, "O"), chatUser(t, pg, "A"), chatUser(t, pg, "B")
|
||||
conv, err := svc.CreateGroup(ctx, owner, "Kīrtana",
|
||||
[]string{handleOf(t, pg, a), handleOf(t, pg, b)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupConv(t, pg, conv.ID)
|
||||
if err := svc.RemoveMember(ctx, b, conv.ID, handleOf(t, pg, b)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cands, err := svc.chat.PushCandidates(ctx, conv.ID, owner)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range cands {
|
||||
if c.UserID == b {
|
||||
t.Error("someone who left the group must not be pushed")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The three tick states have to be reachable in order: one tick on send, two
|
||||
// once it reaches the other device, blue once they read it. The middle state
|
||||
// only exists if delivery is acked separately from reading.
|
||||
func TestReceipts_TickProgression(t *testing.T) {
|
||||
svc, _, pg := testChatService(t)
|
||||
ctx := context.Background()
|
||||
a, b := chatUser(t, pg, "A"), chatUser(t, pg, "B")
|
||||
conv, err := svc.OpenDM(ctx, a, b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupConv(t, pg, conv.ID)
|
||||
|
||||
msg, err := svc.Send(ctx, a, conv.ID, SendRequest{Body: "hare kṛṣṇa"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Sent: nothing acked yet — one tick.
|
||||
rc, err := svc.Receipts(ctx, a, conv.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rc.DeliveredSeq >= msg.Seq || rc.ReadSeq >= msg.Seq {
|
||||
t.Fatalf("just sent: %+v — expected neither delivered nor read", rc)
|
||||
}
|
||||
|
||||
// Delivered: reached their device, still unread — two grey ticks.
|
||||
if err := svc.MarkDelivered(ctx, b, conv.ID, msg.Seq); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rc, _ = svc.Receipts(ctx, a, conv.ID)
|
||||
if rc.DeliveredSeq < msg.Seq {
|
||||
t.Errorf("after delivery ack: delivered_seq = %d, want >= %d", rc.DeliveredSeq, msg.Seq)
|
||||
}
|
||||
if rc.ReadSeq >= msg.Seq {
|
||||
t.Errorf("delivery must not imply read: %+v", rc)
|
||||
}
|
||||
|
||||
// Read: blue.
|
||||
if err := svc.MarkRead(ctx, b, conv.ID, msg.Seq); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rc, _ = svc.Receipts(ctx, a, conv.ID)
|
||||
if rc.ReadSeq < msg.Seq {
|
||||
t.Errorf("after read: read_seq = %d, want >= %d", rc.ReadSeq, msg.Seq)
|
||||
}
|
||||
|
||||
// Reading implies delivery, so someone who was offline and opens the thread
|
||||
// can't end up read-but-undelivered.
|
||||
if rc.DeliveredSeq < rc.ReadSeq {
|
||||
t.Errorf("delivered_seq (%d) fell behind read_seq (%d)", rc.DeliveredSeq, rc.ReadSeq)
|
||||
}
|
||||
|
||||
// Acks must never move a cursor backwards.
|
||||
if err := svc.MarkDelivered(ctx, b, conv.ID, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
back, _ := svc.Receipts(ctx, a, conv.ID)
|
||||
if back.DeliveredSeq < rc.DeliveredSeq || back.ReadSeq < rc.ReadSeq {
|
||||
t.Errorf("a stale ack rewound the cursors: %+v then %+v", rc, back)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user