feat: sync message translation support

This commit is contained in:
A 2026-07-12 14:20:26 +08:00
parent cbccd6a8d9
commit ea6cc72886
26 changed files with 1189 additions and 0 deletions

View file

@ -31,6 +31,10 @@ type DialogStore interface {
SetChatTheme(ctx context.Context, userID int64, peer domain.Peer, emoticon string) (bool, error)
SetPeerSettingsBarHidden(ctx context.Context, userID int64, peer domain.Peer) (bool, error)
PeerSettingsBarHidden(ctx context.Context, userID int64, peer domain.Peer) (bool, error)
// SetTranslationDisabled persists the per-account peer preference used by
// messages.togglePeerTranslations without creating a synthetic dialog.
SetTranslationDisabled(ctx context.Context, userID int64, peer domain.Peer, disabled bool) (bool, error)
TranslationDisabled(ctx context.Context, userID int64, peer domain.Peer) (bool, error)
ListFolders(ctx context.Context, userID int64) (domain.DialogFolderList, error)
GetFolder(ctx context.Context, userID int64, folderID int) (domain.DialogFolder, bool, error)
UpsertFolder(ctx context.Context, userID int64, folder domain.DialogFolder) error

View file

@ -19,6 +19,12 @@ type DialogStore struct {
folderTags map[int64]bool
// archivePinned 记录 archive folder 行置顶状态;无记录时官方默认 true。
archivePinned map[int64]bool
translations map[translationPreferenceKey]bool
}
type translationPreferenceKey struct {
userID int64
peer domain.Peer
}
type dialogDraftKey struct {
@ -36,6 +42,7 @@ func NewDialogStore() *DialogStore {
folderOrder: make(map[int64][]int),
folderTags: make(map[int64]bool),
archivePinned: make(map[int64]bool),
translations: make(map[translationPreferenceKey]bool),
}
}
@ -429,6 +436,26 @@ func (s *DialogStore) PeerSettingsBarHidden(_ context.Context, userID int64, pee
return false, nil
}
func (s *DialogStore) SetTranslationDisabled(_ context.Context, userID int64, peer domain.Peer, disabled bool) (bool, error) {
key := translationPreferenceKey{userID: userID, peer: peer}
s.mu.Lock()
defer s.mu.Unlock()
previous := s.translations[key]
if disabled {
s.translations[key] = true
} else {
delete(s.translations, key)
}
return previous != disabled, nil
}
func (s *DialogStore) TranslationDisabled(_ context.Context, userID int64, peer domain.Peer) (bool, error) {
s.mu.RLock()
disabled := s.translations[translationPreferenceKey{userID: userID, peer: peer}]
s.mu.RUnlock()
return disabled, nil
}
func (s *DialogStore) ListFolders(_ context.Context, userID int64) (domain.DialogFolderList, error) {
s.mu.RLock()
defer s.mu.RUnlock()

View file

@ -699,6 +699,50 @@ func (s *DialogStore) PeerSettingsBarHidden(ctx context.Context, userID int64, p
return hidden, nil
}
func (s *DialogStore) SetTranslationDisabled(ctx context.Context, userID int64, peer domain.Peer, disabled bool) (bool, error) {
if !disabled {
tag, err := s.db.Exec(ctx, `
DELETE FROM peer_translation_settings
WHERE user_id = $1 AND peer_type = $2 AND peer_id = $3`, userID, string(peer.Type), peer.ID)
if err != nil {
return false, fmt.Errorf("enable peer translations: %w", err)
}
return tag.RowsAffected() > 0, nil
}
var changed bool
err := s.db.QueryRow(ctx, `
WITH changed AS (
INSERT INTO peer_translation_settings (user_id, peer_type, peer_id, disabled)
VALUES ($1, $2, $3, true)
ON CONFLICT (user_id, peer_type, peer_id) DO UPDATE
SET disabled = true, updated_at = now()
WHERE peer_translation_settings.disabled IS DISTINCT FROM true
RETURNING true
)
SELECT EXISTS (SELECT 1 FROM changed)`,
userID, string(peer.Type), peer.ID).Scan(&changed)
if err != nil {
return false, fmt.Errorf("set translation disabled: %w", err)
}
return changed, nil
}
func (s *DialogStore) TranslationDisabled(ctx context.Context, userID int64, peer domain.Peer) (bool, error) {
var disabled bool
err := s.db.QueryRow(ctx, `
SELECT disabled
FROM peer_translation_settings
WHERE user_id = $1 AND peer_type = $2 AND peer_id = $3`,
userID, string(peer.Type), peer.ID).Scan(&disabled)
if errors.Is(err, pgx.ErrNoRows) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("get translation disabled: %w", err)
}
return disabled, nil
}
func (s *DialogStore) ListFolders(ctx context.Context, userID int64) (domain.DialogFolderList, error) {
rows, err := s.q.ListDialogFolders(ctx, userID)
if err != nil {

View file

@ -0,0 +1,42 @@
package postgres
import (
"context"
"testing"
"telesrv/internal/domain"
)
func TestTranslationSettingsOwnerPeerScopedRoundTrip(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
suffix := randomSuffix(t)
users := NewUserStore(pool)
owner := createTestUser(t, ctx, users, "+1980"+suffix+"01", "Translate", "Owner")
other := createTestUser(t, ctx, users, "+1980"+suffix+"02", "Translate", "Other")
peerUser := createTestUser(t, ctx, users, "+1980"+suffix+"03", "Translate", "Peer")
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, other.ID, peerUser.ID})
})
store := NewDialogStore(pool)
peer := domain.Peer{Type: domain.PeerTypeUser, ID: peerUser.ID}
if changed, err := store.SetTranslationDisabled(ctx, owner.ID, peer, true); err != nil || !changed {
t.Fatalf("disable = %v/%v", changed, err)
}
if changed, err := store.SetTranslationDisabled(ctx, owner.ID, peer, true); err != nil || changed {
t.Fatalf("duplicate disable = %v/%v", changed, err)
}
if disabled, err := store.TranslationDisabled(ctx, owner.ID, peer); err != nil || !disabled {
t.Fatalf("owner disabled = %v/%v", disabled, err)
}
if disabled, err := store.TranslationDisabled(ctx, other.ID, peer); err != nil || disabled {
t.Fatalf("other disabled = %v/%v", disabled, err)
}
if changed, err := store.SetTranslationDisabled(ctx, owner.ID, peer, false); err != nil || !changed {
t.Fatalf("enable = %v/%v", changed, err)
}
if disabled, err := store.TranslationDisabled(ctx, owner.ID, peer); err != nil || disabled {
t.Fatalf("enabled read = %v/%v", disabled, err)
}
}