Skip to content

Commit b26b98c

Browse files
authored
fix: poll edge cases (#2768)
* fix: poll related edge cases with offline storage * feat: offline db for polls wip * fix: reconcile own_votes properly * fix: all underlying offline store issues with polls * fix: properly resolve own_votes and latest_answers * fix: remove faulty poll check * chore: remove commented out code * chore: remove log * fix: multiple answers bug and remove logs * chore: remove index as we have primary key
1 parent 980c383 commit b26b98c

File tree

15 files changed

+277
-40
lines changed

15 files changed

+277
-40
lines changed

package/src/components/Channel/Channel.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -852,13 +852,7 @@ const ChannelWithContext = <
852852
setThreadMessages(updatedThreadMessages);
853853
}
854854

855-
if (
856-
channel &&
857-
thread?.id &&
858-
event.message?.id === thread.id &&
859-
!threadInstance &&
860-
!thread.poll_id
861-
) {
855+
if (channel && thread?.id && event.message?.id === thread.id && !threadInstance) {
862856
const updatedThread = channel.state.formatMessage(event.message);
863857
setThread(updatedThread);
864858
}

package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ export type LatestMessagePreview<
4040
export type LatestMessagePreviewSelectorReturnType = {
4141
createdBy?: UserResponse | null;
4242
latestVotesByOption?: Record<string, PollVote[]>;
43+
name?: string;
4344
};
4445

4546
const selector = <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(
4647
nextValue: PollState<StreamChatGenerics>,
4748
): LatestMessagePreviewSelectorReturnType => ({
4849
createdBy: nextValue.created_by,
4950
latestVotesByOption: nextValue.latest_votes_by_option,
51+
name: nextValue.name,
5052
});
5153

5254
const getMessageSenderName = <
@@ -145,8 +147,8 @@ const getLatestMessageDisplayText = <
145147
{ bold: false, text: t('🏙 Attachment...') },
146148
];
147149
}
148-
if (message.poll && pollState) {
149-
const { createdBy, latestVotesByOption } = pollState;
150+
if (message.poll_id && pollState) {
151+
const { createdBy, latestVotesByOption, name } = pollState;
150152
let latestVotes;
151153
if (latestVotesByOption) {
152154
latestVotes = Object.values(latestVotesByOption)
@@ -161,7 +163,7 @@ const getLatestMessageDisplayText = <
161163
}
162164
const previewMessage = `${
163165
client.userID === previewUser?.id ? 'You' : previewUser?.name
164-
} ${previewAction}: ${message.poll.name}`;
166+
} ${previewAction}: ${name}`;
165167
return [
166168
{ bold: false, text: '📊 ' },
167169
{ bold: false, text: previewMessage },
@@ -311,7 +313,7 @@ export const useLatestMessagePreview = <
311313
const poll = client.polls.fromState(pollId);
312314
const pollState: LatestMessagePreviewSelectorReturnType =
313315
useStateStore(poll?.state, selector) ?? {};
314-
const { createdBy, latestVotesByOption } = pollState;
316+
const { createdBy, latestVotesByOption, name } = pollState;
315317

316318
useEffect(
317319
() =>
@@ -326,7 +328,15 @@ export const useLatestMessagePreview = <
326328
}),
327329
),
328330
// eslint-disable-next-line react-hooks/exhaustive-deps
329-
[channelLastMessageString, forceUpdate, readEvents, readStatus, latestVotesByOption, createdBy],
331+
[
332+
channelLastMessageString,
333+
forceUpdate,
334+
readEvents,
335+
readStatus,
336+
latestVotesByOption,
337+
createdBy,
338+
name,
339+
],
330340
);
331341

332342
return latestMessagePreview;

package/src/components/Chat/hooks/handleEventToSyncDB.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,14 @@ export const handleEventToSyncDB = <
233233
'poll.vote_removed',
234234
].includes(type)
235235
) {
236-
const poll = event.poll;
236+
const { poll, poll_vote, type } = event;
237237
if (poll) {
238238
return updatePollMessage({
239+
eventType: type,
239240
flush,
240241
poll,
242+
poll_vote,
243+
userID: client?.userID || '',
241244
});
242245
}
243246
}

package/src/components/Poll/CreatePollContent.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,12 @@ export const CreatePollContent = () => {
164164
{t<string>('Multiple answers')}
165165
</Text>
166166
<Switch
167-
onValueChange={() => setMultipleAnswersAllowed(!multipleAnswersAllowed)}
167+
onValueChange={() => {
168+
if (multipleAnswersAllowed) {
169+
setMaxVotesPerPersonEnabled(false);
170+
}
171+
setMultipleAnswersAllowed(!multipleAnswersAllowed);
172+
}}
168173
value={multipleAnswersAllowed}
169174
/>
170175
</View>

package/src/components/Reply/Reply.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import dayjs from 'dayjs';
66

77
import merge from 'lodash/merge';
88

9-
import type { Attachment } from 'stream-chat';
9+
import type { Attachment, PollState } from 'stream-chat';
1010

11+
import { useChatContext } from '../../contexts';
1112
import { useMessageContext } from '../../contexts/messageContext/MessageContext';
1213
import {
1314
MessageInputContext,
@@ -22,6 +23,7 @@ import {
2223
TranslationContextValue,
2324
useTranslationContext,
2425
} from '../../contexts/translationContext/TranslationContext';
26+
import { useStateStore } from '../../hooks';
2527
import { DefaultStreamChatGenerics, FileTypes } from '../../types/types';
2628
import { getResizedImageUrl } from '../../utils/getResizedImageUrl';
2729
import { getTrimmedAttachmentTitle } from '../../utils/getTrimmedAttachmentTitle';
@@ -31,6 +33,7 @@ import { FileIcon as FileIconDefault } from '../Attachment/FileIcon';
3133
import { VideoThumbnail } from '../Attachment/VideoThumbnail';
3234
import { MessageAvatar as MessageAvatarDefault } from '../Message/MessageSimple/MessageAvatar';
3335
import { MessageTextContainer } from '../Message/MessageSimple/MessageTextContainer';
36+
import { MessageType } from '../MessageList/hooks/useMessageList';
3437

3538
const styles = StyleSheet.create({
3639
container: {
@@ -72,6 +75,16 @@ const styles = StyleSheet.create({
7275
},
7376
});
7477

78+
export type ReplySelectorReturnType = {
79+
name?: string;
80+
};
81+
82+
const selector = <StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics>(
83+
nextValue: PollState<StreamChatGenerics>,
84+
): ReplySelectorReturnType => ({
85+
name: nextValue.name,
86+
});
87+
7588
type ReplyPropsWithContext<
7689
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
7790
> = Pick<MessageInputContextValue<StreamChatGenerics>, 'quotedMessage'> &
@@ -134,6 +147,7 @@ const ReplyWithContext = <
134147
>(
135148
props: ReplyPropsWithContext<StreamChatGenerics>,
136149
) => {
150+
const { client } = useChatContext();
137151
const {
138152
attachmentSize = 40,
139153
FileAttachmentIcon,
@@ -167,6 +181,9 @@ const ReplyWithContext = <
167181
},
168182
} = useTheme();
169183

184+
const poll = client.polls.fromState((quotedMessage as MessageType)?.poll_id ?? '');
185+
const { name: pollName }: ReplySelectorReturnType = useStateStore(poll?.state, selector) ?? {};
186+
170187
const messageText = typeof quotedMessage === 'boolean' ? '' : quotedMessage.text || '';
171188

172189
const emojiOnlyText = useMemo(() => {
@@ -262,8 +279,8 @@ const ReplyWithContext = <
262279
text:
263280
quotedMessage.type === 'deleted'
264281
? `_${t('Message deleted')}_`
265-
: quotedMessage.poll
266-
? `📊 ${quotedMessage.poll.name}`
282+
: pollName
283+
? `📊 ${pollName}`
267284
: quotedMessage.text
268285
? quotedMessage.text.length > 170
269286
? `${quotedMessage.text.slice(0, 170)}...`

package/src/store/QuickSqliteClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import type { PreparedQueries, Table } from './types';
3030
*
3131
*/
3232
export class QuickSqliteClient {
33-
static dbVersion = 6;
33+
static dbVersion = 7;
3434

3535
static dbName = DB_NAME;
3636
static dbLocation = DB_LOCATION;

package/src/store/apis/getChannelMessages.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type { DefaultStreamChatGenerics } from '../../types/types';
88
import { isBlockedMessage } from '../../utils/utils';
99
import { mapStorableToMessage } from '../mappers/mapStorableToMessage';
1010
import { QuickSqliteClient } from '../QuickSqliteClient';
11-
import type { TableRowJoinedUser } from '../types';
11+
import { createSelectQuery } from '../sqlite-utils/createSelectQuery';
12+
import type { TableRow, TableRowJoinedUser } from '../types';
1213

1314
export const getChannelMessages = <
1415
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
@@ -35,6 +36,21 @@ export const getChannelMessages = <
3536
}
3637
messageIdVsReactions[reaction.messageId].push(reaction);
3738
});
39+
const messageIdsVsPolls: Record<string, TableRow<'poll'>> = {};
40+
const pollsById: Record<string, TableRow<'poll'>> = {};
41+
const messagesWithPolls = messageRows.filter((message) => !!message.poll_id);
42+
const polls = QuickSqliteClient.executeSql.apply(
43+
null,
44+
createSelectQuery('poll', ['*'], {
45+
id: messagesWithPolls.map((message) => message.poll_id),
46+
}),
47+
);
48+
polls.forEach((poll) => {
49+
pollsById[poll.id] = poll;
50+
});
51+
messagesWithPolls.forEach((message) => {
52+
messageIdsVsPolls[message.poll_id] = pollsById[message.poll_id];
53+
});
3854

3955
// Populate the messages.
4056
const cidVsMessages: Record<string, MessageResponse<StreamChatGenerics>[]> = {};
@@ -48,6 +64,7 @@ export const getChannelMessages = <
4864
mapStorableToMessage<StreamChatGenerics>({
4965
currentUserId,
5066
messageRow: m,
67+
pollRow: messageIdsVsPolls[m.poll_id],
5168
reactionRows: messageIdVsReactions[m.id],
5269
}),
5370
);

package/src/store/apis/updateMessage.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export const updateMessage = ({
3030
return queries;
3131
}
3232

33-
const storableMessage = mapMessageToStorable(message);
33+
const storableMessage = mapMessageToStorable({
34+
...message,
35+
});
3436

3537
queries.push(
3638
createUpdateQuery('messages', storableMessage, {

package/src/store/apis/updatePollMessage.ts

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,67 @@
1-
import type { PollResponse } from 'stream-chat';
1+
import { isVoteAnswer, PollAnswer, PollResponse, PollVote } from 'stream-chat';
22

3+
import { DefaultStreamChatGenerics } from '../../types/types';
4+
import { mapPollToStorable } from '../mappers/mapPollToStorable';
5+
import { mapStorableToPoll } from '../mappers/mapStorableToPoll';
36
import { QuickSqliteClient } from '../QuickSqliteClient';
47
import { createSelectQuery } from '../sqlite-utils/createSelectQuery';
58
import { createUpdateQuery } from '../sqlite-utils/createUpdateQuery';
69
import type { PreparedQueries } from '../types';
710

8-
export const updatePollMessage = ({
11+
export const updatePollMessage = <
12+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
13+
>({
14+
eventType,
915
flush = true,
1016
poll,
17+
poll_vote,
18+
userID,
1119
}: {
12-
poll: PollResponse;
20+
eventType: string;
21+
poll: PollResponse<StreamChatGenerics>;
22+
userID: string;
1323
flush?: boolean;
24+
poll_vote?: PollVote<StreamChatGenerics> | PollAnswer<StreamChatGenerics>;
1425
}) => {
1526
const queries: PreparedQueries[] = [];
1627

17-
const messagesWithPoll = QuickSqliteClient.executeSql.apply(
28+
const pollsFromDB = QuickSqliteClient.executeSql.apply(
1829
null,
19-
createSelectQuery('messages', ['*'], {
20-
poll_id: poll.id,
30+
createSelectQuery('poll', ['*'], {
31+
id: poll.id,
2132
}),
2233
);
2334

24-
for (const message of messagesWithPoll) {
25-
const storablePoll = JSON.stringify({
35+
for (const pollFromDB of pollsFromDB) {
36+
const serializedPoll = mapStorableToPoll(pollFromDB);
37+
const { latest_answers = [], own_votes = [] } = serializedPoll;
38+
let newOwnVotes = own_votes;
39+
if (poll_vote && poll_vote.user?.id === userID) {
40+
newOwnVotes =
41+
eventType === 'poll.vote_removed'
42+
? newOwnVotes.filter((vote) => vote.id !== poll_vote.id)
43+
: [poll_vote, ...newOwnVotes.filter((vote) => vote.id !== poll_vote.id)];
44+
}
45+
let newLatestAnswers = latest_answers;
46+
if (poll_vote && isVoteAnswer(poll_vote)) {
47+
newLatestAnswers =
48+
eventType === 'poll.vote_removed'
49+
? newLatestAnswers.filter((answer) => answer.id !== poll_vote?.id)
50+
: [poll_vote, ...newLatestAnswers.filter((answer) => answer.id !== poll_vote?.id)];
51+
}
52+
53+
const storablePoll = mapPollToStorable({
2654
...poll,
27-
latest_votes: message.poll.latest_votes,
28-
own_votes: message.poll.own_votes,
55+
latest_answers: newLatestAnswers,
56+
own_votes: newOwnVotes,
2957
});
30-
const storableMessage = { ...message, poll: storablePoll };
3158

3259
queries.push(
33-
createUpdateQuery('messages', storableMessage, {
34-
id: message.id,
60+
createUpdateQuery('poll', storablePoll, {
61+
id: poll.id,
3562
}),
3663
);
3764
QuickSqliteClient.logger?.('info', 'updatePoll', {
38-
message: storableMessage,
3965
poll: storablePoll,
4066
});
4167
}

package/src/store/apis/upsertMessages.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { MessageResponse } from 'stream-chat';
22

33
import { mapMessageToStorable } from '../mappers/mapMessageToStorable';
4+
import { mapPollToStorable } from '../mappers/mapPollToStorable';
45
import { mapReactionToStorable } from '../mappers/mapReactionToStorable';
56
import { mapUserToStorable } from '../mappers/mapUserToStorable';
67
import { QuickSqliteClient } from '../QuickSqliteClient';
@@ -16,6 +17,7 @@ export const upsertMessages = ({
1617
const storableMessages: Array<ReturnType<typeof mapMessageToStorable>> = [];
1718
const storableUsers: Array<ReturnType<typeof mapUserToStorable>> = [];
1819
const storableReactions: Array<ReturnType<typeof mapReactionToStorable>> = [];
20+
const storablePolls: Array<ReturnType<typeof mapPollToStorable>> = [];
1921

2022
messages?.forEach((message: MessageResponse) => {
2123
storableMessages.push(mapMessageToStorable(message));
@@ -28,6 +30,9 @@ export const upsertMessages = ({
2830
}
2931
storableReactions.push(mapReactionToStorable(r));
3032
});
33+
if (message.poll) {
34+
storablePolls.push(mapPollToStorable(message.poll));
35+
}
3136
});
3237

3338
const finalQueries = [
@@ -36,11 +41,13 @@ export const upsertMessages = ({
3641
...storableReactions.map((storableReaction) =>
3742
createUpsertQuery('reactions', storableReaction),
3843
),
44+
...storablePolls.map((storablePoll) => createUpsertQuery('poll', storablePoll)),
3945
];
4046

4147
QuickSqliteClient.logger?.('info', 'upsertMessages', {
4248
flush,
4349
messages: storableMessages,
50+
polls: storablePolls,
4451
reactions: storableReactions,
4552
users: storableUsers,
4653
});

0 commit comments

Comments
 (0)