Redesign social conversations and shared music
ci / api / api (push) Successful in 19m12s
ci / ts / ts (packages + web) (push) Successful in 2m44s
ci / module boundaries / boundaries (push) Successful in 18m23s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 59s
ci / db / db (push) Failing after 18s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m18s

This commit is contained in:
mrleo1nid committed 2026-10-23 11:27:47 +03:00
1 parent 11a16cd023
commit 1a693848f2
23 files changed
+1145 -579

No files matched your search

+1 -1
View File
@@ -87,7 +87,7 @@ func (h *Handler) GetDirectMessages(ctx context.Context, r gen.GetDirectMessages
if utf8.RuneCountInString(q) > 100 {
return nil, invalid()
}
v, e := h.store.messagePage(ctx, id, r.PeerId, r.Params.Before, q)
v, e := h.store.filteredMessages(ctx, id, r.PeerId, r.Params.Before, q, r.Params.Music != nil && *r.Params.Music)
return gen.GetDirectMessages200JSONResponse(v), e
}
func validateMessage(m gen.SendDirectMessage) error {
+47
View File
@@ -0,0 +1,47 @@
package social
import (
"context"
"github.com/google/uuid"
"pipu/api/internal/api/gen"
)
// Read all previews in one query. Recheck friendship and both block directions
// here so a removed relationship cannot expose history through the inbox.
func (s *Store) attachLastMessages(ctx context.Context, viewer uuid.UUID, people []gen.SocialPerson) error {
positions := map[uuid.UUID]int{}
ids := []uuid.UUID{}
for i, p := range people {
if p.Relationship == "friend" {
positions[p.Id] = i
ids = append(ids, p.Id)
}
}
if len(ids) == 0 {
return nil
}
rows, e := s.pool.Query(ctx, `SELECT p.peer_id,m.id,m.sender_id,
CASE WHEN m.deleted_at IS NULL THEN left(m.text,160) ELSE '' END,
m.attachment_kind,m.created_at,m.deleted_at IS NOT NULL
FROM unnest($2::uuid[]) AS p(peer_id)
JOIN friendships f ON f.low_id=LEAST($1::uuid,p.peer_id) AND f.high_id=GREATEST($1::uuid,p.peer_id) AND f.accepted
JOIN LATERAL (SELECT * FROM direct_messages
WHERE LEAST(sender_id,recipient_id)=LEAST($1::uuid,p.peer_id)
AND GREATEST(sender_id,recipient_id)=GREATEST($1::uuid,p.peer_id)
ORDER BY seq DESC LIMIT 1) m ON true
WHERE NOT EXISTS(SELECT 1 FROM social_blocks b WHERE (b.user_id=$1 AND b.peer_id=p.peer_id) OR (b.peer_id=$1 AND b.user_id=p.peer_id))`, viewer, ids)
if e != nil {
return e
}
defer rows.Close()
for rows.Next() {
var peer uuid.UUID
var preview gen.SocialMessagePreview
if e = rows.Scan(&peer, &preview.Id, &preview.SenderId, &preview.Text, &preview.AttachmentKind, &preview.CreatedAt, &preview.Deleted); e != nil {
return e
}
people[positions[peer]].LastMessage = &preview
}
return rows.Err()
}
@@ -67,6 +67,9 @@ func TestSocialIsolationAndLifecycle(t *testing.T) {
require.NoError(t, e)
require.Equal(t, 1, inbox.Unread)
// Another user cannot acknowledge this user's notification.
require.NotNil(t, inbox.People[0].LastMessage)
require.Equal(t, m.Id, inbox.People[0].LastMessage.Id)
require.Equal(t, "Привет", inbox.People[0].LastMessage.Text)
require.NoError(t, s.action(ctx, c, gen.SocialAction{Action: "read_notification", TargetId: inbox.Notifications[0].Id}))
inbox, e = s.inbox(ctx, b)
require.NoError(t, e)
@@ -82,6 +85,11 @@ func TestSocialIsolationAndLifecycle(t *testing.T) {
before, e := s.messages(ctx, b, a, &second.Seq)
require.NoError(t, e)
require.Len(t, before, 1)
require.NoError(t, s.updateMessage(ctx, a, b, second.Id, gen.UpdateChatMessageJSONRequestBody{Action: "delete"}))
inbox, e = s.inbox(ctx, b)
require.NoError(t, e)
require.True(t, inbox.People[0].LastMessage.Deleted)
require.Empty(t, inbox.People[0].LastMessage.Text)
require.NoError(t, action(b, a, "block"))
require.NoError(t, action(a, b, "block"))
for _, id := range []uuid.UUID{a, b} {
@@ -89,6 +97,7 @@ func TestSocialIsolationAndLifecycle(t *testing.T) {
require.NoError(t, err)
require.Len(t, blocked.People, 1)
require.EqualValues(t, "blocked", blocked.People[0].Relationship)
require.Nil(t, blocked.People[0].LastMessage)
}
require.NoError(t, action(a, b, "unblock"))
_, e = s.send(ctx, a, b, gen.SendDirectMessage{Id: uuid.New(), Text: "blocked", AttachmentKind: "none"})
@@ -118,6 +127,20 @@ func TestSocialIsolationAndLifecycle(t *testing.T) {
attachment, e := s.send(ctx, a, c, gen.SendDirectMessage{Id: uuid.New(), AttachmentKind: "playlist", AttachmentId: &share})
require.NoError(t, e)
require.Equal(t, &share, attachment.AttachmentId)
_, e = s.send(ctx, a, c, gen.SendDirectMessage{Id: uuid.New(), Text: "After the music", AttachmentKind: "none"})
require.NoError(t, e)
music, e := s.filteredMessages(ctx, c, a, nil, "", true)
require.NoError(t, e)
require.Len(t, music, 1)
require.Equal(t, attachment.Id, music[0].Id)
require.False(t, music[0].Read, "browsing shared music must not acknowledge messages")
music, e = s.filteredMessages(ctx, c, a, &attachment.Seq, "", true)
require.NoError(t, e)
require.Empty(t, music)
require.NoError(t, s.updateMessage(ctx, a, c, attachment.Id, gen.UpdateChatMessageJSONRequestBody{Action: "delete"}))
music, e = s.filteredMessages(ctx, c, a, nil, "", true)
require.NoError(t, e)
require.Empty(t, music, "deleted music must not appear in shared attachments")
_, e = pool.Exec(ctx, `UPDATE playlists SET share_id=NULL WHERE id=$1`, playlist)
require.NoError(t, e)
_, e = s.send(ctx, a, c, gen.SendDirectMessage{Id: uuid.New(), AttachmentKind: "playlist", AttachmentId: &share})
+7 -1
View File
@@ -92,6 +92,9 @@ func (s *Store) inbox(ctx context.Context, id uuid.UUID) (gen.SocialInbox, error
if e = s.attachActivity(ctx, id, out.People); e != nil {
return out, e
}
if e = s.attachLastMessages(ctx, id, out.People); e != nil {
return out, e
}
if e = s.pool.QueryRow(ctx, `SELECT activity_visibility,friends_recommendations FROM users WHERE id=$1`, id).Scan(&out.ActivityVisibility, &out.FriendsRecommendations); e != nil {
return out, e
}
@@ -238,6 +241,9 @@ func (s *Store) messages(ctx context.Context, id, peer uuid.UUID, before *int64)
return s.messagePage(ctx, id, peer, before, "")
}
func (s *Store) messagePage(ctx context.Context, id, peer uuid.UUID, before *int64, q string) ([]gen.DirectMessage, error) {
return s.filteredMessages(ctx, id, peer, before, q, false)
}
func (s *Store) filteredMessages(ctx context.Context, id, peer uuid.UUID, before *int64, q string, music bool) ([]gen.DirectMessage, error) {
tx, e := pairTx(ctx, s.pool, id, peer)
if e != nil {
return nil, e
@@ -246,7 +252,7 @@ func (s *Store) messagePage(ctx context.Context, id, peer uuid.UUID, before *int
if e = allowed(ctx, tx, id, peer); e != nil {
return nil, e
}
rows, e := tx.Query(ctx, `SELECT `+messageColumns+` FROM direct_messages WHERE LEAST(sender_id,recipient_id)=LEAST($1::uuid,$2::uuid) AND GREATEST(sender_id,recipient_id)=GREATEST($1::uuid,$2::uuid) AND ($3::bigint IS NULL OR seq<$3) AND ($4='' OR (deleted_at IS NULL AND strpos(lower(text),lower($4))>0)) ORDER BY seq DESC LIMIT 50`, id, peer, before, q)
rows, e := tx.Query(ctx, `SELECT `+messageColumns+` FROM direct_messages WHERE LEAST(sender_id,recipient_id)=LEAST($1::uuid,$2::uuid) AND GREATEST(sender_id,recipient_id)=GREATEST($1::uuid,$2::uuid) AND ($3::bigint IS NULL OR seq<$3) AND ($4='' OR (deleted_at IS NULL AND strpos(lower(text),lower($4))>0)) AND (NOT $5::boolean OR (deleted_at IS NULL AND attachment_kind IN ('track','playlist'))) ORDER BY seq DESC LIMIT 50`, id, peer, before, q, music)
if e != nil {
return nil, e
}