Show group replies icon for stories with replies

This commit is contained in:
Josh Perez 2022-10-22 02:26:16 -04:00 committed by GitHub
parent ba55285c74
commit 471a9e2e98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 170 additions and 136 deletions

View file

@ -1009,8 +1009,11 @@ export async function startApp(): Promise<void> {
};
try {
// This needs to load before we prime the data because we expect
// ConversationController to be loaded and ready to use by then.
await window.ConversationController.load();
await Promise.all([
window.ConversationController.load(),
Stickers.load(),
loadRecentEmojis(),
loadInitialBadgesState(),

View file

@ -179,6 +179,8 @@ export const StoriesPane = ({
conversationId={story.conversationId}
group={story.group}
getPreferredBadge={getPreferredBadge}
hasReplies={story.hasReplies}
hasRepliesFromSelf={story.hasRepliesFromSelf}
i18n={i18n}
key={story.storyView.timestamp}
onHideStory={toggleHideStories}

View file

@ -45,12 +45,12 @@ const Template: Story<PropsType> = args => <StoryListItem {...args} />;
export const SomeonesStory = Template.bind({});
SomeonesStory.args = {
hasReplies: true,
group: getDefaultConversation({ title: 'Sports Group' }),
story: {
attachment: fakeAttachment({
thumbnail: fakeThumbnail('/fixtures/tina-rolf-269345-unsplash.jpg'),
}),
hasReplies: true,
isUnread: true,
messageId: '123',
messageIdForLogging: 'for logging 123',

View file

@ -21,6 +21,8 @@ import { getAvatarColor } from '../types/Colors';
export type PropsType = Pick<ConversationStoryType, 'group' | 'isHidden'> & {
conversationId: string;
getPreferredBadge: PreferredBadgeSelectorType;
hasReplies?: boolean;
hasRepliesFromSelf?: boolean;
i18n: LocalizerType;
onGoToConversation: (conversationId: string) => unknown;
onHideStory: (conversationId: string) => unknown;
@ -79,6 +81,8 @@ export const StoryListItem = ({
conversationId,
getPreferredBadge,
group,
hasReplies,
hasRepliesFromSelf,
i18n,
isHidden,
onGoToConversation,
@ -89,14 +93,7 @@ export const StoryListItem = ({
}: PropsType): JSX.Element => {
const [hasConfirmHideStory, setHasConfirmHideStory] = useState(false);
const {
attachment,
hasReplies,
hasRepliesFromSelf,
isUnread,
sender,
timestamp,
} = story;
const { attachment, isUnread, sender, timestamp } = story;
const { firstName, title } = sender;

View file

@ -11,16 +11,51 @@ import { getAttachmentsForMessage } from '../state/selectors/message';
import { isNotNil } from '../util/isNotNil';
import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull';
import { isGroup } from '../util/whatTypeOfConversation';
let storyData: Array<MessageAttributesType> | undefined;
let storyData:
| Array<
MessageAttributesType & {
hasReplies?: boolean;
hasRepliesFromSelf?: boolean;
}
>
| undefined;
export async function loadStories(): Promise<void> {
storyData = await dataInterface.getOlderStories({});
const stories = await dataInterface.getAllStories({});
storyData = await Promise.all(
stories.map(async story => {
const conversation = window.ConversationController.get(
story.conversationId
);
if (!isGroup(conversation?.attributes)) {
return story;
}
const [hasReplies, hasRepliesFromSelf] = await Promise.all([
dataInterface.hasStoryReplies(story.id),
dataInterface.hasStoryRepliesFromSelf(story.id),
]);
return {
...story,
hasReplies,
hasRepliesFromSelf,
};
})
);
await repairUnexpiredStories();
}
export function getStoryDataFromMessageAttributes(
message: MessageAttributesType
message: MessageAttributesType & {
hasReplies?: boolean;
hasRepliesFromSelf?: boolean;
}
): StoryDataType | undefined {
const { attachments, deletedForEveryone } = message;
const unresolvedAttachment = attachments ? attachments[0] : undefined;
@ -43,6 +78,8 @@ export function getStoryDataFromMessageAttributes(
'canReplyToStory',
'conversationId',
'deletedForEveryone',
'hasReplies',
'hasRepliesFromSelf',
'reactions',
'readAt',
'readStatus',

View file

@ -256,7 +256,9 @@ const dataInterface: ClientInterface = {
getNextTapToViewMessageTimestampToAgeOut,
getTapToViewMessagesNeedingErase,
getOlderMessagesByConversation,
getOlderStories,
getAllStories,
hasStoryReplies,
hasStoryRepliesFromSelf,
getNewerMessagesByConversation,
getMessageMetricsForConversation,
getConversationRangeCenteredOnMessage,
@ -1349,14 +1351,20 @@ async function getOlderMessagesByConversation(
return handleMessageJSON(messages);
}
async function getOlderStories(options: {
async function getAllStories(options: {
conversationId?: string;
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: UUIDStringType;
}): Promise<Array<MessageType>> {
return channels.getOlderStories(options);
return channels.getAllStories(options);
}
async function hasStoryReplies(storyId: string): Promise<boolean> {
return channels.hasStoryReplies(storyId);
}
async function hasStoryRepliesFromSelf(storyId: string): Promise<boolean> {
return channels.hasStoryRepliesFromSelf(storyId);
}
async function getNewerMessagesByConversation(

View file

@ -508,13 +508,12 @@ export type DataInterface = {
getNextTapToViewMessageTimestampToAgeOut: () => Promise<undefined | number>;
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
// getOlderMessagesByConversation is JSON on server, full message on Client
getOlderStories: (options: {
getAllStories: (options: {
conversationId?: string;
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: UUIDStringType;
}) => Promise<Array<MessageType>>;
hasStoryReplies: (storyId: string) => Promise<boolean>;
hasStoryRepliesFromSelf: (storyId: string) => Promise<boolean>;
// getNewerMessagesByConversation is JSON on server, full message on Client
getMessageMetricsForConversation: (
conversationId: string,

View file

@ -246,7 +246,9 @@ const dataInterface: ServerInterface = {
getNextTapToViewMessageTimestampToAgeOut,
getTapToViewMessagesNeedingErase,
getOlderMessagesByConversation,
getOlderStories,
getAllStories,
hasStoryReplies,
hasStoryRepliesFromSelf,
getNewerMessagesByConversation,
getTotalUnreadForConversation,
getMessageMetricsForConversation,
@ -2501,17 +2503,11 @@ function getOlderMessagesByConversationSync(
.reverse();
}
async function getOlderStories({
async function getAllStories({
conversationId,
limit = 9999,
receivedAt = Number.MAX_VALUE,
sentAt,
sourceUuid,
}: {
conversationId?: string;
limit?: number;
receivedAt?: number;
sentAt?: number;
sourceUuid?: UUIDStringType;
}): Promise<Array<MessageType>> {
const db = getInstance();
@ -2523,25 +2519,50 @@ async function getOlderStories({
WHERE
type IS 'story' AND
($conversationId IS NULL OR conversationId IS $conversationId) AND
($sourceUuid IS NULL OR sourceUuid IS $sourceUuid) AND
(received_at < $receivedAt
OR (received_at IS $receivedAt AND sent_at < $sentAt)
)
ORDER BY received_at ASC, sent_at ASC
LIMIT $limit;
($sourceUuid IS NULL OR sourceUuid IS $sourceUuid)
ORDER BY received_at ASC, sent_at ASC;
`
)
.all({
conversationId: conversationId || null,
receivedAt,
sentAt: sentAt || null,
sourceUuid: sourceUuid || null,
limit,
});
return rows.map(row => jsonToObject(row.json));
}
async function hasStoryReplies(storyId: string): Promise<boolean> {
const db = getInstance();
const row: { count: number } = db
.prepare<Query>(
`
SELECT COUNT(*) as count
FROM messages
WHERE storyId IS $storyId;
`
)
.get({ storyId });
return row.count !== 0;
}
async function hasStoryRepliesFromSelf(storyId: string): Promise<boolean> {
const db = getInstance();
const sql = `
SELECT COUNT(*) as count
FROM messages
WHERE
storyId IS $storyId AND
type IS 'outgoing'
`;
const row: { count: number } = db.prepare<Query>(sql).get({ storyId });
return row.count !== 0;
}
async function getNewerMessagesByConversation(
conversationId: string,
options: {

View file

@ -55,6 +55,8 @@ import { viewedReceiptsJobQueue } from '../../jobs/viewedReceiptsJobQueue';
export type StoryDataType = {
attachment?: AttachmentType;
hasReplies?: boolean;
hasRepliesFromSelf?: boolean;
messageId: string;
startedDownload?: boolean;
} & Pick<
@ -156,7 +158,10 @@ type QueueStoryDownloadActionType = {
type ReplyToStoryActionType = {
type: typeof REPLY_TO_STORY;
payload: MessageAttributesType;
payload: {
message: MessageAttributesType;
storyId: string;
};
};
type ResolveAttachmentUrlActionType = {
@ -470,7 +475,10 @@ function replyToStory(
if (messageAttributes) {
dispatch({
type: REPLY_TO_STORY,
payload: messageAttributes,
payload: {
message: messageAttributes,
storyId: story.messageId,
},
});
}
};
@ -1211,6 +1219,8 @@ export function reducer(
'deletedForEveryone',
'expirationStartTimestamp',
'expireTimer',
'hasReplies',
'hasRepliesFromSelf',
'messageId',
'reactions',
'readAt',
@ -1311,6 +1321,19 @@ export function reducer(
if (action.type === LOAD_STORY_REPLIES) {
return {
...state,
stories: state.stories.map(story => {
if (story.messageId === action.payload.messageId) {
return {
...story,
hasReplies: action.payload.replies.length > 0,
hasRepliesFromSelf: action.payload.replies.some(
reply => reply.type === 'outgoing'
),
};
}
return story;
}),
replyState: action.payload,
};
}
@ -1373,15 +1396,31 @@ export function reducer(
if (action.type === REPLY_TO_STORY) {
const { replyState } = state;
const stories = state.stories.map(story => {
if (story.messageId === action.payload.storyId) {
return {
...story,
hasRepliesFromSelf: true,
};
}
return story;
});
if (!replyState) {
return state;
return {
...state,
stories,
};
}
return {
...state,
stories,
replyState: {
messageId: replyState.messageId,
replies: [...replyState.replies, action.payload],
replies: [...replyState.replies, action.payload.message],
},
};
}

View file

@ -249,6 +249,8 @@ export function getConversationStory(
return {
conversationId: conversation.id,
group: conversation.id !== sender.id ? conversation : undefined,
hasReplies: story.hasReplies,
hasRepliesFromSelf: story.hasRepliesFromSelf,
isHidden: Boolean(sender.hideStory),
storyView,
};
@ -430,6 +432,11 @@ export const getStories = createSelector(
storiesMap.set(conversationStory.conversationId, {
...existingConversationStory,
...conversationStory,
hasReplies:
existingConversationStory?.hasReplies || conversationStory.hasReplies,
hasRepliesFromSelf:
existingConversationStory?.hasRepliesFromSelf ||
conversationStory.hasRepliesFromSelf,
storyView: conversationStory.storyView,
});
});

View file

@ -49,7 +49,6 @@ export function getFakeStoryView(
attachment: getAttachmentWithThumbnail(
attachmentUrl || '/fixtures/tina-rolf-269345-unsplash.jpg'
),
hasReplies: Boolean(casual.coin_flip),
isUnread: Boolean(casual.coin_flip),
messageId,
messageIdForLogging: `${messageId} (for logging)`,
@ -70,8 +69,14 @@ export function getFakeStory({
}): ConversationStoryType {
const storyView = getFakeStoryView(attachmentUrl, timestamp);
const hasReplies = group ? Boolean(casual.coin_flip) : false;
const hasRepliesFromSelf =
group && hasReplies ? Boolean(casual.coin_flip) : false;
return {
conversationId: storyView.sender.id,
hasReplies,
hasRepliesFromSelf,
group,
storyView,
};

View file

@ -9,7 +9,7 @@ import type { UUIDStringType } from '../../types/UUID';
import type { MessageAttributesType } from '../../model-types.d';
const { removeAll, _getAllMessages, saveMessages, getOlderStories } =
const { removeAll, _getAllMessages, saveMessages, getAllStories } =
dataInterface;
function getUuid(): UUIDStringType {
@ -21,7 +21,7 @@ describe('sql/stories', () => {
await removeAll();
});
describe('getOlderStories', () => {
describe('getAllStories', () => {
it('returns N most recent stories overall, or in converation, or by author', async () => {
assert.lengthOf(await _getAllMessages(), 0);
@ -88,9 +88,7 @@ describe('sql/stories', () => {
assert.lengthOf(await _getAllMessages(), 5);
const stories = await getOlderStories({
limit: 5,
});
const stories = await getAllStories({});
assert.lengthOf(stories, 4, 'expect four total stories');
// They are in ASC order
@ -105,9 +103,8 @@ describe('sql/stories', () => {
'stories last should be story1'
);
const storiesInConversation = await getOlderStories({
const storiesInConversation = await getAllStories({
conversationId,
limit: 5,
});
assert.lengthOf(
storiesInConversation,
@ -127,9 +124,8 @@ describe('sql/stories', () => {
'storiesInConversation last should be story1'
);
const storiesByAuthor = await getOlderStories({
const storiesByAuthor = await getAllStories({
sourceUuid,
limit: 5,
});
assert.lengthOf(storiesByAuthor, 2, 'expect two stories by author');
@ -145,85 +141,5 @@ describe('sql/stories', () => {
'storiesByAuthor last should be story2'
);
});
it('returns N stories older than provided receivedAt/sentAt', async () => {
assert.lengthOf(await _getAllMessages(), 0);
const start = Date.now();
const conversationId = getUuid();
const ourUuid = getUuid();
const story1: MessageAttributesType = {
id: getUuid(),
body: 'message 1',
type: 'incoming',
conversationId,
sent_at: start - 2,
received_at: start - 2,
timestamp: start - 2,
};
const story2: MessageAttributesType = {
id: getUuid(),
body: 'story 2',
type: 'story',
conversationId,
sent_at: start - 1,
received_at: start - 1,
timestamp: start - 1,
};
const story3: MessageAttributesType = {
id: getUuid(),
body: 'story 3',
type: 'story',
conversationId,
sent_at: start - 1,
received_at: start,
timestamp: start,
};
const story4: MessageAttributesType = {
id: getUuid(),
body: 'story 4',
type: 'story',
conversationId,
sent_at: start,
received_at: start,
timestamp: start,
};
const story5: MessageAttributesType = {
id: getUuid(),
body: 'story 5',
type: 'story',
conversationId,
sent_at: start + 1,
received_at: start + 1,
timestamp: start + 1,
};
await saveMessages([story1, story2, story3, story4, story5], {
forceSave: true,
ourUuid,
});
assert.lengthOf(await _getAllMessages(), 5);
const stories = await getOlderStories({
receivedAt: story4.received_at,
sentAt: story4.sent_at,
limit: 5,
});
assert.lengthOf(stories, 2, 'expect two stories');
// They are in ASC order
assert.strictEqual(
stories[0].id,
story2.id,
'stories first should be story3'
);
assert.strictEqual(
stories[1].id,
story3.id,
'stories last should be story2'
);
});
});
});

View file

@ -42,6 +42,8 @@ export type ReplyStateType = {
export type ConversationStoryType = {
conversationId: string;
hasReplies?: boolean;
hasRepliesFromSelf?: boolean;
group?: Pick<
ConversationType,
| 'acceptedMessageRequest'
@ -70,8 +72,6 @@ export type StorySendStateType = {
export type StoryViewType = {
attachment?: AttachmentType;
canReply?: boolean;
hasReplies?: boolean;
hasRepliesFromSelf?: boolean;
isHidden?: boolean;
isUnread?: boolean;
messageId: string;