Show group replies icon for stories with replies
This commit is contained in:
parent
ba55285c74
commit
471a9e2e98
13 changed files with 170 additions and 136 deletions
|
@ -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(),
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue