From 0331b151053eb1cd18f1c93c99c1efda037863e2 Mon Sep 17 00:00:00 2001 From: "nikola.nnaumovski" Date: Wed, 21 Oct 2026 16:39:03 +0200 Subject: [PATCH] fix(poll): keep translations from poll.updated in the poll state handlePollUpdated copied the event through extractPollData, which builds an UpdatePollRequest and lists its fields one by one. It leaves out the server-owned name_i18n and description_i18n, so a poll.updated stored the new question but dropped its translations. Readers in other languages kept the stale translation after a question was edited, and a language added on demand with translateMessage never reached the cached poll. Option translations were not affected, because options are copied whole. Copy both fields from the event, also when they are absent. The event carries the whole poll, and a change made under channel-only translation removes the old translations, so an absent field clears the stale ones. extractPollData is public and keeps its request shape. --- src/poll.ts | 10 +++++- test/unit/poll.test.js | 60 ++++++++++++++++++++++++++++++++++ test/unit/poll_manager.test.ts | 15 +++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/poll.ts b/src/poll.ts index 79431d694..0f9515f76 100644 --- a/src/poll.ts +++ b/src/poll.ts @@ -158,7 +158,15 @@ export class Poll { const { id: _id, ...pollData } = extractPollData(event.poll); if (pollData.is_closed) this.closeConfirmed = true; // @ts-expect-error type mismatch - this.state.partialNext({ ...pollData, lastActivityAt: event.created_at }); + this.state.partialNext({ + ...pollData, + // Server-owned translations, which `extractPollData` (an update request) leaves out. Copied + // even when absent: the event carries the whole poll, and a change made under channel-only + // translation drops the stale ones. + description_i18n: event.poll.description_i18n, + lastActivityAt: event.created_at, + name_i18n: event.poll.name_i18n, + }); this.upsertOfflineDb(); }; diff --git a/test/unit/poll.test.js b/test/unit/poll.test.js index de9bcfeb3..988a77ac1 100644 --- a/test/unit/poll.test.js +++ b/test/unit/poll.test.js @@ -188,6 +188,66 @@ describe('Poll', () => { expect(poll.data.description).to.equal(description); }); + describe('translations on poll.updated', () => { + const nameI18n = { en_text: 'XY', es_text: 'XY (es)', language: 'en' }; + const descriptionI18n = { en_text: 'About', es_text: 'Acerca de', language: 'en' }; + + it('stores the translations of an edited question', () => { + const poll = new Poll({ client, poll: { ...pollResponse, name_i18n: nameI18n } }); + const editedNameI18n = { en_text: 'XYZ', es_text: 'XYZ (es)', language: 'en' }; + + poll.handlePollUpdated({ + type: 'poll.updated', + poll: { ...pollResponse, name: 'XYZ', name_i18n: editedNameI18n }, + }); + + expect(poll.data.name).to.equal('XYZ'); + expect(poll.data.name_i18n).to.eql(editedNameI18n); + }); + + it('stores a language added after the poll was sent', () => { + const poll = new Poll({ client, poll: { ...pollResponse, name_i18n: nameI18n } }); + const withGerman = { ...nameI18n, de_text: 'XY (de)' }; + + poll.handlePollUpdated({ + type: 'poll.updated', + poll: { ...pollResponse, name_i18n: withGerman }, + }); + + expect(poll.data.name_i18n).to.eql(withGerman); + }); + + it('stores the translations of the description', () => { + const poll = new Poll({ client, poll: pollResponse }); + + poll.handlePollUpdated({ + type: 'poll.updated', + poll: { + ...pollResponse, + description: 'About', + description_i18n: descriptionI18n, + }, + }); + + expect(poll.data.description_i18n).to.eql(descriptionI18n); + }); + + it('clears translations the event no longer carries', () => { + const poll = new Poll({ + client, + poll: { ...pollResponse, description_i18n: descriptionI18n, name_i18n: nameI18n }, + }); + + poll.handlePollUpdated({ + type: 'poll.updated', + poll: { ...pollResponse, name: 'XYZ' }, + }); + + expect(poll.data.name_i18n).to.be.undefined; + expect(poll.data.description_i18n).to.be.undefined; + }); + }); + it("should not update poll state when handlePollUpdated is called with other poll's event", () => { const poll = new Poll({ client, poll: { ...pollResponse, id: 'X' } }); const description = 'Description update'; diff --git a/test/unit/poll_manager.test.ts b/test/unit/poll_manager.test.ts index 6d1da0cd5..3661af186 100644 --- a/test/unit/poll_manager.test.ts +++ b/test/unit/poll_manager.test.ts @@ -589,6 +589,21 @@ describe('PollManager', () => { expect(spy2.calledOnce).to.be.false; }); + it('should keep the translations of poll.updated in the cached poll', () => { + const nameI18n = { de_text: 'Neuer Titel', en_text: 'New title', language: 'en' }; + + client.dispatchEvent({ + type: 'poll.updated', + poll: { + ...pollMessage1.poll, + name: 'New title', + name_i18n: nameI18n, + } as PollResponse, + }); + + expect(pollManager.fromState(pollId1)?.data.name_i18n).to.eql(nameI18n); + }); + const eventHandlerPairs = [ ['poll.closed', 'handlePollClosed'], ['poll.vote_casted', 'handleVoteCasted'],