Skip to content

Commit

Permalink
[PAY-1535] Refresh chat messages on render and pull to refresh on cha…
Browse files Browse the repository at this point in the history
…ts list (#3673)
  • Loading branch information
rickyrombo authored Jun 30, 2023
1 parent 2fccb0b commit c099918
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 38 deletions.
150 changes: 139 additions & 11 deletions packages/common/src/store/pages/chat/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ const {
fetchUnreadMessagesCountFailed,
goToChat,
fetchChatIfNecessary,
fetchLatestChats,
fetchMoreChats,
fetchMoreChatsSucceeded,
fetchMoreChatsFailed,
fetchLatestMessages,
fetchMoreMessages,
fetchMoreMessagesSucceeded,
fetchMoreMessagesFailed,
Expand Down Expand Up @@ -75,6 +77,9 @@ const {
} = chatSelectors
const { toast } = toastActions

const CHAT_PAGE_SIZE = 30
const MESSAGES_PAGE_SIZE = 50

/**
* Helper to dispatch actions for fetching chat users
*/
Expand Down Expand Up @@ -111,6 +116,49 @@ function* doFetchUnreadMessagesCount() {
}
}

/**
* Gets all chats fresher than what we currently have
*/
function* doFetchLatestChats() {
try {
const audiusSdk = yield* getContext('audiusSdk')
const sdk = yield* call(audiusSdk)
const summary = yield* select(getChatsSummary)
let before: string | undefined
let hasMoreChats = true
let data: UserChat[] = []
let firstResponse: TypedCommsResponse<UserChat[]> | undefined
while (hasMoreChats) {
const response = yield* call([sdk.chats, sdk.chats.getAll], {
before,
after: summary?.next_cursor,
limit: CHAT_PAGE_SIZE
})
hasMoreChats = response.data.length > 0
before = summary?.prev_cursor
data = data.concat(response.data)
if (!firstResponse) {
firstResponse = response
}
}
yield* fetchUsersForChats(data)
yield* put(
fetchMoreChatsSucceeded({
...firstResponse,
data
})
)
} catch (e) {
console.error('fetchLatestChatsFailed', e)
yield* put(fetchMoreChatsFailed())
const reportToSentry = yield* getContext('reportToSentry')
reportToSentry({
level: ErrorLevel.Error,
error: e as Error
})
}
}

function* doFetchMoreChats() {
try {
const audiusSdk = yield* getContext('audiusSdk')
Expand All @@ -119,7 +167,7 @@ function* doFetchMoreChats() {
const before = summary?.prev_cursor
const response = yield* call([sdk.chats, sdk.chats.getAll], {
before,
limit: 30
limit: CHAT_PAGE_SIZE
})
yield* fetchUsersForChats(response.data)
yield* put(fetchMoreChatsSucceeded(response))
Expand All @@ -134,6 +182,76 @@ function* doFetchMoreChats() {
}
}

/**
* Gets all messages newer than what we currently have
*/
function* doFetchLatestMessages(
action: ReturnType<typeof fetchLatestMessages>
) {
const { chatId } = action.payload
try {
const audiusSdk = yield* getContext('audiusSdk')
const sdk = yield* call(audiusSdk)

// Update the chat too to keep everything in sync
yield* call(doFetchChat, { chatId })
const chat = yield* select((state) => getChat(state, chatId))
const after = chat?.messagesSummary?.next_cursor

// On first fetch of messages, we won't have an after cursor.
// Do `fetchMoreMessages` instead for initial fetch of messages and get all up to the first unread
if (!after) {
yield* call(doFetchMoreMessages, action)
return
}

let hasMoreUnread = true
let data: ChatMessage[] = []
let before: string | undefined
let firstResponse: TypedCommsResponse<ChatMessage[]> | undefined
while (hasMoreUnread) {
// This will get all messages sent after what we currently got, starting at the most recent,
// and batching by MESSAGE_PAGE_SIZE. Sends one extra request to get the 0 response but oh well
const response = yield* call([sdk.chats, sdk.chats.getMessages], {
chatId,
before,
after,
limit: MESSAGES_PAGE_SIZE
})
data = data.concat(response.data)
// If we have no more messages with our after filter, we're done
hasMoreUnread = response.data.length > 0
before = response.summary?.prev_cursor
if (!firstResponse) {
firstResponse = response
}
}
yield* put(
fetchMoreMessagesSucceeded({
chatId,
response: {
...firstResponse,
data
}
})
)
} catch (e) {
console.error('fetchLatestChatMessagesFailed', e)
yield* put(fetchMoreMessagesFailed({ chatId }))
const reportToSentry = yield* getContext('reportToSentry')
reportToSentry({
level: ErrorLevel.Error,
error: e as Error,
additionalInfo: {
chatId
}
})
}
}

/**
* Get older messages than what we currently have for a chat
*/
function* doFetchMoreMessages(action: ReturnType<typeof fetchMoreMessages>) {
const { chatId } = action.payload
try {
Expand All @@ -150,20 +268,20 @@ function* doFetchMoreMessages(action: ReturnType<typeof fetchMoreMessages>) {
let hasMoreUnread = true
let data: ChatMessage[] = []
while (hasMoreUnread) {
const limit = 10
const response = yield* call([sdk.chats, sdk.chats!.getMessages], {
const response = yield* call([sdk.chats, sdk.chats.getMessages], {
chatId,
before,
limit
limit: MESSAGES_PAGE_SIZE
})
// Only save the last response summary. Pagination is one-way
lastResponse = response
data = data.concat(response.data)
// If the unread count is greater than the previous fetched messages (next_cursor)
// plus this batch (limit), we should keep fetching
// plus this batch, we should keep fetching
hasMoreUnread =
!!chat?.unread_message_count &&
chat.unread_message_count > (response.summary?.next_count ?? 0) + limit
chat.unread_message_count >
(response.summary?.next_count ?? 0) + data.length
before = response.summary?.prev_cursor
}
if (!lastResponse) {
Expand All @@ -179,7 +297,7 @@ function* doFetchMoreMessages(action: ReturnType<typeof fetchMoreMessages>) {
})
)
} catch (e) {
console.error('fetchNewChatMessagesFailed', e)
console.error('fetchMoreChatMessagesFailed', e)
yield* put(fetchMoreMessagesFailed({ chatId }))
const reportToSentry = yield* getContext('reportToSentry')
reportToSentry({
Expand Down Expand Up @@ -631,11 +749,19 @@ function* watchSendMessage() {
yield takeEvery(sendMessage, doSendMessage)
}

function* watchFetchChats() {
function* watchFetchLatestChats() {
yield takeLatest(fetchLatestChats, doFetchLatestChats)
}

function* watchFetchMoreChats() {
yield takeLatest(fetchMoreChats, doFetchMoreChats)
}

function* watchFetchChatMessages() {
function* watchFetchLatestMessages() {
yield takeEvery(fetchLatestMessages, doFetchLatestMessages)
}

function* watchFetchMoreMessages() {
yield takeEvery(fetchMoreMessages, doFetchMoreMessages)
}

Expand Down Expand Up @@ -683,8 +809,10 @@ export const sagas = () => {
return [
watchFetchUnreadMessagesCount,
watchFetchChatIfNecessary,
watchFetchChats,
watchFetchChatMessages,
watchFetchLatestChats,
watchFetchMoreChats,
watchFetchLatestMessages,
watchFetchMoreMessages,
watchSetMessageReaction,
watchCreateChat,
watchMarkChatAsRead,
Expand Down
71 changes: 57 additions & 14 deletions packages/common/src/store/pages/chat/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type ChatID = string

type ChatState = {
chats: EntityState<UserChatWithMessagesStatus> & {
status: Status
status: Status | 'REFRESHING'
summary?: TypedCommsResponse<UserChat>['summary']
}
messages: Record<
Expand Down Expand Up @@ -156,26 +156,61 @@ const slice = createSlice({
goToChat: (_state, _action: PayloadAction<{ chatId?: string }>) => {
// triggers saga
},
fetchLatestChats: (state) => {
// triggers saga
state.chats.status = 'REFRESHING'
},
fetchMoreChats: (state) => {
// triggers saga
state.chats.status = Status.LOADING
},
fetchMoreChatsSucceeded: (
state,
action: PayloadAction<TypedCommsResponse<UserChat[]>>
action: PayloadAction<
Pick<TypedCommsResponse<UserChat[]>, 'data' | 'summary'>
>
) => {
const { data, summary } = action.payload
state.chats.status = Status.SUCCESS
state.chats.summary = action.payload.summary
for (const chat of action.payload.data) {
if (!state.chats.summary) {
state.chats.summary = summary
} else {
if (
summary?.next_cursor &&
dayjs(summary?.next_cursor).isAfter(state.chats.summary.next_cursor)
) {
state.chats.summary.next_cursor = summary?.next_cursor
state.chats.summary.next_count = summary?.next_count
}
if (
summary?.prev_cursor &&
dayjs(summary?.prev_cursor).isBefore(state.chats.summary.prev_cursor)
) {
state.chats.summary.prev_cursor = summary?.prev_cursor
state.chats.summary.prev_count = summary?.prev_count
}
}
for (const chat of data) {
if (!(chat.chat_id in state.messages)) {
state.messages[chat.chat_id] = chatMessagesAdapter.getInitialState()
}
}
chatsAdapter.addMany(state.chats, action.payload.data)
chatsAdapter.upsertMany(state.chats, data)
},
fetchMoreChatsFailed: (state) => {
state.chats.status = Status.ERROR
},
fetchLatestMessages: (state, action: PayloadAction<{ chatId: string }>) => {
// triggers saga
if (!state.messages[action.payload.chatId]) {
state.messages[action.payload.chatId] = {
...chatMessagesAdapter.getInitialState(),
status: Status.LOADING
}
} else {
state.messages[action.payload.chatId].status = Status.LOADING
}
},
fetchMoreMessages: (state, action: PayloadAction<{ chatId: string }>) => {
// triggers saga
if (!state.messages[action.payload.chatId]) {
Expand All @@ -190,7 +225,7 @@ const slice = createSlice({
fetchMoreMessagesSucceeded: (
state,
action: PayloadAction<{
response: TypedCommsResponse<ChatMessage[]>
response: Pick<TypedCommsResponse<ChatMessage[]>, 'summary' | 'data'>
chatId: string
}>
) => {
Expand All @@ -203,15 +238,21 @@ const slice = createSlice({
return
}

// Update the summary to include the min of next_cursor/next_count and
// prev_cursor/prev_count.
// Update the summary to include the max of next_cursor and
// min of prev_cursor.
const existingSummary = state.chats.entities[chatId]?.messagesSummary
const summaryToUse = { ...summary, ...existingSummary }
if (summary.next_count < (existingSummary?.next_count ?? Infinity)) {
if (
!existingSummary ||
dayjs(summary.next_cursor).isAfter(existingSummary.next_cursor)
) {
summaryToUse.next_count = summary.next_count
summaryToUse.next_cursor = summary.next_cursor
}
if (summary.prev_count < (existingSummary?.prev_count ?? Infinity)) {
if (
!existingSummary ||
dayjs(summary.prev_cursor).isBefore(existingSummary.prev_cursor)
) {
summaryToUse.prev_count = summary.prev_count
summaryToUse.prev_cursor = summary.prev_cursor
}
Expand All @@ -227,10 +268,12 @@ const slice = createSlice({
return { ...item, hasTail: hasTail(item, data[index - 1]) }
})
chatMessagesAdapter.upsertMany(state.messages[chatId], messagesWithTail)
recalculatePreviousMessageHasTail(
state.messages[chatId],
summary.next_count - 1
)
if (data.length > 0) {
recalculatePreviousMessageHasTail(
state.messages[chatId],
summary.next_count - 1
)
}
},
fetchMoreMessagesFailed: (
state,
Expand Down
Loading

0 comments on commit c099918

Please sign in to comment.