Skip to content

Commit

Permalink
Allow multiple reactions to stories
Browse files Browse the repository at this point in the history
  • Loading branch information
josh-signal committed Apr 28, 2022
1 parent 42554eb commit 6d576ed
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 119 deletions.
1 change: 0 additions & 1 deletion ts/components/StoryListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export type StoryViewType = {
isHidden?: boolean;
isUnread?: boolean;
messageId: string;
selectedReaction?: string;
sender: Pick<
ConversationType,
| 'acceptedMessageRequest'
Expand Down
3 changes: 2 additions & 1 deletion ts/jobs/helpers/sendReaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@ export async function sendReaction(
const newReactions = reactionUtil.markOutgoingReactionSent(
getReactions(message),
pendingReaction,
successfulConversationIds
successfulConversationIds,
message.attributes
);
setReactions(message, newReactions);

Expand Down
36 changes: 23 additions & 13 deletions ts/models/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ import { shouldDownloadStory } from '../util/shouldDownloadStory';
import { shouldShowStoriesView } from '../state/selectors/stories';
import type { ContactWithHydratedAvatar } from '../textsecure/SendMessage';
import { SeenStatus } from '../MessageSeenStatus';
import { isNewReactionReplacingPrevious } from '../reactions/util';

/* eslint-disable camelcase */
/* eslint-disable more/no-then */
Expand Down Expand Up @@ -233,12 +234,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const { storyChanged } = window.reduxActions.stories;

if (isStory(this.attributes)) {
const ourConversationId =
window.ConversationController.getOurConversationIdOrThrow();
const storyData = getStoryDataFromMessageAttributes(
this.attributes,
ourConversationId
);
const storyData = getStoryDataFromMessageAttributes(this.attributes);

if (!storyData) {
return;
Expand Down Expand Up @@ -2892,14 +2888,15 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {

const reactions = reactionUtil.addOutgoingReaction(
this.get('reactions') || [],
newReaction
newReaction,
isStory(this.attributes)
);
this.set({ reactions });
} else {
const oldReactions = this.get('reactions') || [];
let reactions: Array<MessageReactionType>;
const oldReaction = oldReactions.find(
re => re.fromId === reaction.get('fromId')
const oldReaction = oldReactions.find(re =>
isNewReactionReplacingPrevious(re, reaction.attributes, this.attributes)
);
if (oldReaction) {
this.clearNotifications(oldReaction);
Expand All @@ -2914,12 +2911,20 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
if (reaction.get('source') === ReactionSource.FromSync) {
reactions = oldReactions.filter(
re =>
re.fromId !== reaction.get('fromId') ||
re.timestamp > reaction.get('timestamp')
!isNewReactionReplacingPrevious(
re,
reaction.attributes,
this.attributes
) || re.timestamp > reaction.get('timestamp')
);
} else {
reactions = oldReactions.filter(
re => re.fromId !== reaction.get('fromId')
re =>
!isNewReactionReplacingPrevious(
re,
reaction.attributes,
this.attributes
)
);
}
this.set({ reactions });
Expand Down Expand Up @@ -2948,7 +2953,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}

reactions = oldReactions.filter(
re => re.fromId !== reaction.get('fromId')
re =>
!isNewReactionReplacingPrevious(
re,
reaction.attributes,
this.attributes
)
);
reactions.push(reactionToAdd);
this.set({ reactions });
Expand Down
30 changes: 26 additions & 4 deletions ts/reactions/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
// SPDX-License-Identifier: AGPL-3.0-only

import { findLastIndex, has, identity, omit, negate } from 'lodash';
import type { MessageReactionType } from '../model-types.d';
import type {
MessageAttributesType,
MessageReactionType,
} from '../model-types.d';
import { areObjectEntriesEqual } from '../util/areObjectEntriesEqual';
import { isStory } from '../state/selectors/message';

const isReactionEqual = (
a: undefined | Readonly<MessageReactionType>,
Expand Down Expand Up @@ -31,8 +35,13 @@ const isOutgoingReactionCompletelyUnsent = ({

export function addOutgoingReaction(
oldReactions: ReadonlyArray<MessageReactionType>,
newReaction: Readonly<MessageReactionType>
newReaction: Readonly<MessageReactionType>,
isStoryMessage = false
): Array<MessageReactionType> {
if (isStoryMessage) {
return [...oldReactions, newReaction];
}

const pendingOutgoingReactions = new Set(
oldReactions.filter(isOutgoingReactionPending)
);
Expand Down Expand Up @@ -101,6 +110,17 @@ export function* getUnsentConversationIds({
}
}

// This function is used when filtering reactions so that we can limit normal
// messages to a single reactions but allow multiple reactions from the same
// sender for stories.
export function isNewReactionReplacingPrevious(
reaction: MessageReactionType,
newReaction: MessageReactionType,
messageAttributes: MessageAttributesType
): boolean {
return !isStory(messageAttributes) && reaction.fromId === newReaction.fromId;
}

export const markOutgoingReactionFailed = (
reactions: Array<MessageReactionType>,
reaction: Readonly<MessageReactionType>
Expand All @@ -116,7 +136,8 @@ export const markOutgoingReactionFailed = (
export const markOutgoingReactionSent = (
reactions: ReadonlyArray<MessageReactionType>,
reaction: Readonly<MessageReactionType>,
conversationIdsSentTo: Iterable<string>
conversationIdsSentTo: Iterable<string>,
messageAttributes: MessageAttributesType
): Array<MessageReactionType> => {
const result: Array<MessageReactionType> = [];

Expand All @@ -135,7 +156,8 @@ export const markOutgoingReactionSent = (
if (!isReactionEqual(re, reaction)) {
const shouldKeep = !isFullySent
? true
: re.fromId !== reaction.fromId || re.timestamp > reaction.timestamp;
: !isNewReactionReplacingPrevious(re, reaction, messageAttributes) ||
re.timestamp > reaction.timestamp;
if (shouldKeep) {
result.push(re);
}
Expand Down
14 changes: 3 additions & 11 deletions ts/services/storyLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ export async function loadStories(): Promise<void> {
}

export function getStoryDataFromMessageAttributes(
message: MessageAttributesType,
ourConversationId?: string
message: MessageAttributesType
): StoryDataType | undefined {
const { attachments } = message;
const unresolvedAttachment = attachments ? attachments[0] : undefined;
Expand All @@ -33,17 +32,13 @@ export function getStoryDataFromMessageAttributes(
? getAttachmentsForMessage(message)
: [unresolvedAttachment];

const selectedReaction = (
(message.reactions || []).find(re => re.fromId === ourConversationId) || {}
).emoji;

return {
attachment,
messageId: message.id,
selectedReaction,
...pick(message, [
'conversationId',
'deletedForEveryone',
'reactions',
'readStatus',
'sendStateByConversationId',
'source',
Expand All @@ -57,11 +52,8 @@ export function getStoryDataFromMessageAttributes(
export function getStoriesForRedux(): Array<StoryDataType> {
strictAssert(storyData, 'storyData has not been loaded');

const ourConversationId =
window.ConversationController.getOurConversationId();

const stories = storyData
.map(story => getStoryDataFromMessageAttributes(story, ourConversationId))
.map(getStoryDataFromMessageAttributes)
.filter(isNotNil);

storyData = undefined;
Expand Down
56 changes: 16 additions & 40 deletions ts/state/ducks/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
export type StoryDataType = {
attachment?: AttachmentType;
messageId: string;
selectedReaction?: string;
} & Pick<
MessageAttributesType,
| 'conversationId'
| 'deletedForEveryone'
| 'reactions'
| 'readStatus'
| 'sendStateByConversationId'
| 'source'
Expand All @@ -65,7 +65,6 @@ export type StoriesStateType = {

const LOAD_STORY_REPLIES = 'stories/LOAD_STORY_REPLIES';
const MARK_STORY_READ = 'stories/MARK_STORY_READ';
const REACT_TO_STORY = 'stories/REACT_TO_STORY';
const REPLY_TO_STORY = 'stories/REPLY_TO_STORY';
export const RESOLVE_ATTACHMENT_URL = 'stories/RESOLVE_ATTACHMENT_URL';
const STORY_CHANGED = 'stories/STORY_CHANGED';
Expand All @@ -84,14 +83,6 @@ type MarkStoryReadActionType = {
payload: string;
};

type ReactToStoryActionType = {
type: typeof REACT_TO_STORY;
payload: {
messageId: string;
selectedReaction: string;
};
};

type ReplyToStoryActionType = {
type: typeof REPLY_TO_STORY;
payload: MessageAttributesType;
Expand Down Expand Up @@ -119,7 +110,6 @@ export type StoriesActionType =
| MarkStoryReadActionType
| MessageChangedActionType
| MessageDeletedActionType
| ReactToStoryActionType
| ReplyToStoryActionType
| ResolveAttachmentUrlActionType
| StoryChangedActionType
Expand Down Expand Up @@ -286,27 +276,24 @@ function queueStoryDownload(

function reactToStory(
nextReaction: string,
messageId: string,
previousReaction?: string
): ThunkAction<void, RootStateType, unknown, ReactToStoryActionType> {
messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => {
try {
await enqueueReactionForSend({
messageId,
emoji: nextReaction,
remove: nextReaction === previousReaction,
});
dispatch({
type: REACT_TO_STORY,
payload: {
messageId,
selectedReaction: nextReaction,
},
remove: false,
});
} catch (error) {
log.error('Error enqueuing reaction', error, messageId, nextReaction);
showToast(ToastReactionFailed);
}

dispatch({
type: 'NOOP',
payload: null,
});
};
}

Expand Down Expand Up @@ -403,8 +390,8 @@ export function reducer(
'conversationId',
'deletedForEveryone',
'messageId',
'reactions',
'readStatus',
'selectedReaction',
'sendStateByConversationId',
'source',
'sourceUuid',
Expand All @@ -424,9 +411,14 @@ export function reducer(
!isDownloaded(prevStory.attachment) &&
isDownloaded(newStory.attachment);
const readStatusChanged = prevStory.readStatus !== newStory.readStatus;
const reactionsChanged =
prevStory.reactions?.length !== newStory.reactions?.length;

const shouldReplace =
isDownloadingAttachment || hasAttachmentDownloaded || readStatusChanged;
isDownloadingAttachment ||
hasAttachmentDownloaded ||
readStatusChanged ||
reactionsChanged;
if (!shouldReplace) {
return state;
}
Expand All @@ -448,22 +440,6 @@ export function reducer(
};
}

if (action.type === REACT_TO_STORY) {
return {
...state,
stories: state.stories.map(story => {
if (story.messageId === action.payload.messageId) {
return {
...story,
selectedReaction: action.payload.selectedReaction,
};
}

return story;
}),
};
}

if (action.type === MARK_STORY_READ) {
return {
...state,
Expand Down

0 comments on commit 6d576ed

Please sign in to comment.