Bluesky app fork with some witchin' additions ๐Ÿ’ซ

[๐Ÿด] State transitions (#3880)

* Handle init/resume/suspend/background and polling

* Add debug and temp guard

* Make state transitions sync

* Make init sync also

* Checkpoint: confusing but working state machine

* Reducer-esque

* Remove poll events

* Guard fetchConvo

(cherry picked from commit 8385579d31500bb4bfb60afeecdc1eb3ddd7e747)

* Clean up polling, make sync

(cherry picked from commit 7f75cd04c3bf81c94662785748698640a84bef51)

* Update history handling

(cherry picked from commit b82b552ba4040adf7ead2377541132a386964ff8)

* Check for screen focus in app state listener

* Get rid of ad-hoc status checks

authored by

Eric Bailey and committed by
GitHub
f78126e0 87cb4c10

+490 -208
+2 -2
src/screens/Messages/Conversation/MessageListError.tsx
··· 18 18 const {_} = useLingui() 19 19 const message = React.useMemo(() => { 20 20 return { 21 - [ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`), 22 - [ConvoItemError.ResumeFailed]: _( 21 + [ConvoItemError.Network]: _( 23 22 msg`There was an issue connecting to the chat.`, 24 23 ), 24 + [ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`), 25 25 [ConvoItemError.PollFailed]: _( 26 26 msg`This chat was disconnected due to a network error.`, 27 27 ),
+16 -2
src/screens/Messages/Conversation/index.tsx
··· 18 18 import {CenteredView} from 'view/com/util/Views' 19 19 import {MessagesList} from '#/screens/Messages/Conversation/MessagesList' 20 20 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 21 + import {Button, ButtonText} from '#/components/Button' 21 22 import {ConvoMenu} from '#/components/dms/ConvoMenu' 22 23 import {ListMaybePlaceholder} from '#/components/Lists' 23 24 import {Text} from '#/components/Typography' ··· 51 52 } 52 53 53 54 if (chat.status === ConvoStatus.Error) { 54 - // TODO error 55 - return null 55 + // TODO 56 + return ( 57 + <View> 58 + <CenteredView style={{flex: 1}} sideBorders> 59 + <Text>Something went wrong</Text> 60 + <Button 61 + label="Retry" 62 + onPress={() => { 63 + chat.error.retry() 64 + }}> 65 + <ButtonText>Retry</ButtonText> 66 + </Button> 67 + </CenteredView> 68 + </View> 69 + ) 56 70 } 57 71 58 72 /*
+451 -203
src/state/messages/convo.ts
··· 2 2 import { 3 3 BskyAgent, 4 4 ChatBskyConvoDefs, 5 + ChatBskyConvoGetLog, 5 6 ChatBskyConvoSendMessage, 6 7 } from '@atproto-labs/api' 7 8 import {nanoid} from 'nanoid/non-secure' ··· 18 19 export enum ConvoStatus { 19 20 Uninitialized = 'uninitialized', 20 21 Initializing = 'initializing', 21 - Resuming = 'resuming', 22 22 Ready = 'ready', 23 23 Error = 'error', 24 24 Backgrounded = 'backgrounded', ··· 27 27 28 28 export enum ConvoItemError { 29 29 HistoryFailed = 'historyFailed', 30 - ResumeFailed = 'resumeFailed', 31 30 PollFailed = 'pollFailed', 31 + Network = 'network', 32 32 } 33 33 34 - export enum ConvoError { 34 + export enum ConvoErrorCode { 35 35 InitFailed = 'initFailed', 36 36 } 37 37 38 + export type ConvoError = { 39 + code: ConvoErrorCode 40 + exception?: Error 41 + retry: () => void 42 + } 43 + 44 + export enum ConvoDispatchEvent { 45 + Init = 'init', 46 + Ready = 'ready', 47 + Resume = 'resume', 48 + Background = 'background', 49 + Suspend = 'suspend', 50 + Error = 'error', 51 + } 52 + 53 + export type ConvoDispatch = 54 + | { 55 + event: ConvoDispatchEvent.Init 56 + } 57 + | { 58 + event: ConvoDispatchEvent.Ready 59 + } 60 + | { 61 + event: ConvoDispatchEvent.Resume 62 + } 63 + | { 64 + event: ConvoDispatchEvent.Background 65 + } 66 + | { 67 + event: ConvoDispatchEvent.Suspend 68 + } 69 + | { 70 + event: ConvoDispatchEvent.Error 71 + payload: ConvoError 72 + } 73 + 38 74 export type ConvoItem = 39 75 | { 40 76 type: 'message' | 'pending-message' ··· 134 170 fetchMessageHistory: () => Promise<void> 135 171 } 136 172 | { 137 - status: ConvoStatus.Resuming 138 - items: ConvoItem[] 139 - convo: ChatBskyConvoDefs.ConvoView 140 - error: undefined 141 - sender: AppBskyActorDefs.ProfileViewBasic 142 - recipients: AppBskyActorDefs.ProfileViewBasic[] 143 - isFetchingHistory: boolean 144 - deleteMessage: (messageId: string) => Promise<void> 145 - sendMessage: ( 146 - message: ChatBskyConvoSendMessage.InputSchema['message'], 147 - ) => Promise<void> 148 - fetchMessageHistory: () => Promise<void> 149 - } 150 - | { 151 173 status: ConvoStatus.Error 152 174 items: [] 153 175 convo: undefined ··· 160 182 fetchMessageHistory: undefined 161 183 } 162 184 163 - const ACTIVE_POLL_INTERVAL = 2e3 185 + const ACTIVE_POLL_INTERVAL = 1e3 164 186 const BACKGROUND_POLL_INTERVAL = 10e3 187 + 188 + // TODO temporary 189 + let DEBUG_ACTIVE_CHAT: string | undefined 165 190 166 191 export function isConvoItemMessage( 167 192 item: ConvoItem, ··· 175 200 } 176 201 177 202 export class Convo { 203 + private id: string 204 + 178 205 private agent: BskyAgent 179 206 private __tempFromUserDid: string 180 207 181 - private pollInterval = ACTIVE_POLL_INTERVAL 182 208 private status: ConvoStatus = ConvoStatus.Uninitialized 209 + private pollInterval = ACTIVE_POLL_INTERVAL 183 210 private error: 184 211 | { 185 - code: ConvoError 212 + code: ConvoErrorCode 186 213 exception?: Error 187 214 retry: () => void 188 215 } ··· 190 217 private historyCursor: string | undefined | null = undefined 191 218 private isFetchingHistory = false 192 219 private eventsCursor: string | undefined = undefined 193 - private pollingFailure = false 194 220 195 221 private pastMessages: Map< 196 222 string, ··· 208 234 private footerItems: Map<string, ConvoItem> = new Map() 209 235 private headerItems: Map<string, ConvoItem> = new Map() 210 236 211 - private pendingEventIngestion: Promise<void> | undefined 212 237 private isProcessingPendingMessages = false 238 + private pendingPoll: Promise<void> | undefined 239 + private nextPoll: NodeJS.Timeout | undefined 213 240 214 241 convoId: string 215 242 convo: ChatBskyConvoDefs.ConvoView | undefined ··· 218 245 snapshot: ConvoState | undefined 219 246 220 247 constructor(params: ConvoParams) { 248 + this.id = nanoid(3) 221 249 this.convoId = params.convoId 222 250 this.agent = params.agent 223 251 this.__tempFromUserDid = params.__tempFromUserDid ··· 227 255 this.sendMessage = this.sendMessage.bind(this) 228 256 this.deleteMessage = this.deleteMessage.bind(this) 229 257 this.fetchMessageHistory = this.fetchMessageHistory.bind(this) 258 + 259 + if (DEBUG_ACTIVE_CHAT) { 260 + logger.error(`Convo: another chat was already active`, { 261 + convoId: this.convoId, 262 + }) 263 + } else { 264 + DEBUG_ACTIVE_CHAT = this.convoId 265 + } 230 266 } 231 267 232 268 private commit() { ··· 271 307 } 272 308 case ConvoStatus.Suspended: 273 309 case ConvoStatus.Backgrounded: 274 - case ConvoStatus.Resuming: 275 310 case ConvoStatus.Ready: { 276 311 return { 277 312 status: this.status, ··· 317 352 } 318 353 } 319 354 320 - async init() { 321 - logger.debug('Convo: init', {}, logger.DebugContext.convo) 355 + dispatch(action: ConvoDispatch) { 356 + const prevStatus = this.status 322 357 323 - if ( 324 - this.status === ConvoStatus.Uninitialized || 325 - this.status === ConvoStatus.Error 326 - ) { 327 - try { 328 - this.status = ConvoStatus.Initializing 329 - this.commit() 330 - 331 - await this.refreshConvo() 332 - this.status = ConvoStatus.Ready 333 - this.commit() 334 - 335 - await this.fetchMessageHistory() 336 - 337 - this.pollEvents() 338 - } catch (e: any) { 339 - logger.error('Convo: failed to init') 340 - this.error = { 341 - exception: e, 342 - code: ConvoError.InitFailed, 343 - retry: () => { 344 - this.error = undefined 345 - this.init() 346 - }, 358 + switch (this.status) { 359 + case ConvoStatus.Uninitialized: { 360 + switch (action.event) { 361 + case ConvoDispatchEvent.Init: { 362 + this.status = ConvoStatus.Initializing 363 + this.setup() 364 + break 365 + } 366 + } 367 + break 368 + } 369 + case ConvoStatus.Initializing: { 370 + switch (action.event) { 371 + case ConvoDispatchEvent.Ready: { 372 + this.status = ConvoStatus.Ready 373 + this.pollInterval = ACTIVE_POLL_INTERVAL 374 + this.fetchMessageHistory().then(() => { 375 + this.restartPoll() 376 + }) 377 + break 378 + } 379 + case ConvoDispatchEvent.Background: { 380 + this.status = ConvoStatus.Backgrounded 381 + this.pollInterval = BACKGROUND_POLL_INTERVAL 382 + this.fetchMessageHistory().then(() => { 383 + this.restartPoll() 384 + }) 385 + break 386 + } 387 + case ConvoDispatchEvent.Suspend: { 388 + this.status = ConvoStatus.Suspended 389 + break 390 + } 391 + case ConvoDispatchEvent.Error: { 392 + this.status = ConvoStatus.Error 393 + this.error = action.payload 394 + break 395 + } 396 + } 397 + break 398 + } 399 + case ConvoStatus.Ready: { 400 + switch (action.event) { 401 + case ConvoDispatchEvent.Resume: { 402 + this.refreshConvo() 403 + this.restartPoll() 404 + break 405 + } 406 + case ConvoDispatchEvent.Background: { 407 + this.status = ConvoStatus.Backgrounded 408 + this.pollInterval = BACKGROUND_POLL_INTERVAL 409 + this.restartPoll() 410 + break 411 + } 412 + case ConvoDispatchEvent.Suspend: { 413 + this.status = ConvoStatus.Suspended 414 + this.cancelNextPoll() 415 + break 416 + } 417 + case ConvoDispatchEvent.Error: { 418 + this.status = ConvoStatus.Error 419 + this.error = action.payload 420 + this.cancelNextPoll() 421 + break 422 + } 423 + } 424 + break 425 + } 426 + case ConvoStatus.Backgrounded: { 427 + switch (action.event) { 428 + case ConvoDispatchEvent.Resume: { 429 + this.status = ConvoStatus.Ready 430 + this.pollInterval = ACTIVE_POLL_INTERVAL 431 + this.refreshConvo() 432 + // TODO truncate history if needed 433 + this.restartPoll() 434 + break 435 + } 436 + case ConvoDispatchEvent.Suspend: { 437 + this.status = ConvoStatus.Suspended 438 + this.cancelNextPoll() 439 + break 440 + } 441 + case ConvoDispatchEvent.Error: { 442 + this.status = ConvoStatus.Error 443 + this.error = action.payload 444 + this.cancelNextPoll() 445 + break 446 + } 347 447 } 348 - this.status = ConvoStatus.Error 349 - this.commit() 448 + break 449 + } 450 + case ConvoStatus.Suspended: { 451 + switch (action.event) { 452 + case ConvoDispatchEvent.Init: { 453 + this.status = ConvoStatus.Ready 454 + this.pollInterval = ACTIVE_POLL_INTERVAL 455 + this.refreshConvo() 456 + // TODO truncate history if needed 457 + this.restartPoll() 458 + break 459 + } 460 + case ConvoDispatchEvent.Resume: { 461 + this.status = ConvoStatus.Ready 462 + this.pollInterval = ACTIVE_POLL_INTERVAL 463 + this.refreshConvo() 464 + this.restartPoll() 465 + break 466 + } 467 + case ConvoDispatchEvent.Error: { 468 + this.status = ConvoStatus.Error 469 + this.error = action.payload 470 + break 471 + } 472 + } 473 + break 474 + } 475 + case ConvoStatus.Error: { 476 + switch (action.event) { 477 + case ConvoDispatchEvent.Init: { 478 + this.reset() 479 + break 480 + } 481 + case ConvoDispatchEvent.Resume: { 482 + this.reset() 483 + break 484 + } 485 + case ConvoDispatchEvent.Suspend: { 486 + this.status = ConvoStatus.Suspended 487 + break 488 + } 489 + case ConvoDispatchEvent.Error: { 490 + this.status = ConvoStatus.Error 491 + this.error = action.payload 492 + break 493 + } 494 + } 495 + break 350 496 } 351 - } else { 352 - logger.warn(`Convo: cannot init from ${this.status}`) 497 + default: 498 + break 353 499 } 500 + 501 + logger.debug( 502 + `Convo: dispatch '${action.event}'`, 503 + { 504 + id: this.id, 505 + prev: prevStatus, 506 + next: this.status, 507 + }, 508 + logger.DebugContext.convo, 509 + ) 510 + 511 + this.commit() 354 512 } 355 513 356 - async resume() { 357 - logger.debug('Convo: resume', {}, logger.DebugContext.convo) 514 + private reset() { 515 + this.convo = undefined 516 + this.sender = undefined 517 + this.recipients = undefined 518 + this.snapshot = undefined 358 519 359 - if ( 360 - this.status === ConvoStatus.Suspended || 361 - this.status === ConvoStatus.Backgrounded 362 - ) { 363 - const fromStatus = this.status 520 + this.status = ConvoStatus.Uninitialized 521 + this.error = undefined 522 + this.historyCursor = undefined 523 + this.eventsCursor = undefined 524 + 525 + this.pastMessages = new Map() 526 + this.newMessages = new Map() 527 + this.pendingMessages = new Map() 528 + this.deletedMessages = new Set() 529 + this.footerItems = new Map() 530 + this.headerItems = new Map() 364 531 365 - try { 366 - this.status = ConvoStatus.Resuming 367 - this.commit() 532 + this.dispatch({event: ConvoDispatchEvent.Init}) 533 + } 368 534 369 - await this.refreshConvo() 370 - this.status = ConvoStatus.Ready 371 - this.commit() 535 + private async setup() { 536 + try { 537 + const {convo, sender, recipients} = await this.fetchConvo() 372 538 373 - // throw new Error('UNCOMMENT TO TEST RESUME FAILURE') 539 + this.convo = convo 540 + this.sender = sender 541 + this.recipients = recipients 374 542 375 - this.pollInterval = ACTIVE_POLL_INTERVAL 376 - this.pollEvents() 377 - } catch (e) { 378 - logger.error('Convo: failed to resume') 543 + /* 544 + * Some validation prior to `Ready` status 545 + */ 546 + if (!this.convo) { 547 + throw new Error('Convo: could not find convo') 548 + } 549 + if (!this.sender) { 550 + throw new Error('Convo: could not find sender in convo') 551 + } 552 + if (!this.recipients) { 553 + throw new Error('Convo: could not find recipients in convo') 554 + } 379 555 380 - this.footerItems.set(ConvoItemError.ResumeFailed, { 381 - type: 'error-recoverable', 382 - key: ConvoItemError.ResumeFailed, 383 - code: ConvoItemError.ResumeFailed, 556 + // await new Promise(y => setTimeout(y, 2000)) 557 + // throw new Error('UNCOMMENT TO TEST INIT FAILURE') 558 + this.dispatch({event: ConvoDispatchEvent.Ready}) 559 + } catch (e: any) { 560 + logger.error('Convo: setup() failed') 561 + 562 + this.dispatch({ 563 + event: ConvoDispatchEvent.Error, 564 + payload: { 565 + exception: e, 566 + code: ConvoErrorCode.InitFailed, 384 567 retry: () => { 385 - this.footerItems.delete(ConvoItemError.ResumeFailed) 386 - this.resume() 568 + this.reset() 387 569 }, 388 - }) 570 + }, 571 + }) 572 + } 573 + } 389 574 390 - this.status = fromStatus 391 - this.commit() 392 - } 393 - } else { 394 - logger.warn(`Convo: cannot resume from ${this.status}`) 395 - } 575 + init() { 576 + this.dispatch({event: ConvoDispatchEvent.Init}) 577 + } 578 + 579 + resume() { 580 + this.dispatch({event: ConvoDispatchEvent.Resume}) 396 581 } 397 582 398 - async background() { 399 - logger.debug('Convo: backgrounded', {}, logger.DebugContext.convo) 400 - this.status = ConvoStatus.Backgrounded 401 - this.pollInterval = BACKGROUND_POLL_INTERVAL 402 - this.commit() 583 + background() { 584 + this.dispatch({event: ConvoDispatchEvent.Background}) 403 585 } 404 586 405 - async suspend() { 406 - logger.debug('Convo: suspended', {}, logger.DebugContext.convo) 407 - this.status = ConvoStatus.Suspended 408 - this.commit() 587 + suspend() { 588 + this.dispatch({event: ConvoDispatchEvent.Suspend}) 589 + DEBUG_ACTIVE_CHAT = undefined 590 + } 591 + 592 + private pendingFetchConvo: 593 + | Promise<{ 594 + convo: ChatBskyConvoDefs.ConvoView 595 + sender: AppBskyActorDefs.ProfileViewBasic | undefined 596 + recipients: AppBskyActorDefs.ProfileViewBasic[] 597 + }> 598 + | undefined 599 + async fetchConvo() { 600 + if (this.pendingFetchConvo) return this.pendingFetchConvo 601 + 602 + this.pendingFetchConvo = new Promise<{ 603 + convo: ChatBskyConvoDefs.ConvoView 604 + sender: AppBskyActorDefs.ProfileViewBasic | undefined 605 + recipients: AppBskyActorDefs.ProfileViewBasic[] 606 + }>(async (resolve, reject) => { 607 + try { 608 + const response = await this.agent.api.chat.bsky.convo.getConvo( 609 + { 610 + convoId: this.convoId, 611 + }, 612 + { 613 + headers: { 614 + Authorization: this.__tempFromUserDid, 615 + }, 616 + }, 617 + ) 618 + 619 + const convo = response.data.convo 620 + 621 + resolve({ 622 + convo, 623 + sender: convo.members.find(m => m.did === this.__tempFromUserDid), 624 + recipients: convo.members.filter( 625 + m => m.did !== this.__tempFromUserDid, 626 + ), 627 + }) 628 + } catch (e) { 629 + reject(e) 630 + } finally { 631 + this.pendingFetchConvo = undefined 632 + } 633 + }) 634 + 635 + return this.pendingFetchConvo 409 636 } 410 637 411 638 async refreshConvo() { 412 - const response = await this.agent.api.chat.bsky.convo.getConvo( 413 - { 414 - convoId: this.convoId, 415 - }, 416 - { 417 - headers: { 418 - Authorization: this.__tempFromUserDid, 639 + try { 640 + const {convo, sender, recipients} = await this.fetchConvo() 641 + // throw new Error('UNCOMMENT TO TEST REFRESH FAILURE') 642 + this.convo = convo || this.convo 643 + this.sender = sender || this.sender 644 + this.recipients = recipients || this.recipients 645 + } catch (e: any) { 646 + logger.error(`Convo: failed to refresh convo`) 647 + 648 + this.footerItems.set(ConvoItemError.Network, { 649 + type: 'error-recoverable', 650 + key: ConvoItemError.Network, 651 + code: ConvoItemError.Network, 652 + retry: () => { 653 + this.footerItems.delete(ConvoItemError.Network) 654 + this.resume() 419 655 }, 420 - }, 421 - ) 422 - this.convo = response.data.convo 423 - this.sender = this.convo.members.find(m => m.did === this.__tempFromUserDid) 424 - this.recipients = this.convo.members.filter( 425 - m => m.did !== this.__tempFromUserDid, 426 - ) 427 - 428 - /* 429 - * Prevent invalid states 430 - */ 431 - if (!this.sender) { 432 - throw new Error('Convo: could not find sender in convo') 433 - } 434 - if (!this.recipients) { 435 - throw new Error('Convo: could not find recipients in convo') 656 + }) 657 + this.commit() 436 658 } 437 659 } 438 660 ··· 517 739 } 518 740 } 519 741 520 - private async pollEvents() { 521 - if ( 522 - this.status === ConvoStatus.Ready || 523 - this.status === ConvoStatus.Backgrounded 524 - ) { 525 - if (this.pendingEventIngestion) return 742 + private restartPoll() { 743 + this.cancelNextPoll() 744 + this.pollLatestEvents() 745 + } 746 + 747 + private cancelNextPoll() { 748 + if (this.nextPoll) clearTimeout(this.nextPoll) 749 + } 526 750 527 - /* 528 - * Represents a failed state, which is retryable. 529 - */ 530 - if (this.pollingFailure) return 751 + private pollLatestEvents() { 752 + /* 753 + * Uncomment to view poll events 754 + */ 755 + logger.debug('Convo: poll events', {id: this.id}, logger.DebugContext.convo) 531 756 532 - setTimeout(async () => { 533 - this.pendingEventIngestion = this.ingestLatestEvents() 534 - await this.pendingEventIngestion 535 - this.pendingEventIngestion = undefined 536 - this.pollEvents() 757 + try { 758 + this.fetchLatestEvents().then(({events}) => { 759 + this.applyLatestEvents(events) 760 + }) 761 + this.nextPoll = setTimeout(() => { 762 + this.pollLatestEvents() 537 763 }, this.pollInterval) 764 + } catch (e: any) { 765 + logger.error('Convo: poll events failed') 766 + 767 + this.cancelNextPoll() 768 + 769 + this.footerItems.set(ConvoItemError.PollFailed, { 770 + type: 'error-recoverable', 771 + key: ConvoItemError.PollFailed, 772 + code: ConvoItemError.PollFailed, 773 + retry: () => { 774 + this.footerItems.delete(ConvoItemError.PollFailed) 775 + this.commit() 776 + this.pollLatestEvents() 777 + }, 778 + }) 779 + 780 + this.commit() 538 781 } 539 782 } 540 783 541 - async ingestLatestEvents() { 542 - try { 543 - // throw new Error('UNCOMMENT TO TEST POLL FAILURE') 544 - const response = await this.agent.api.chat.bsky.convo.getLog( 545 - { 546 - cursor: this.eventsCursor, 547 - }, 548 - { 549 - headers: { 550 - Authorization: this.__tempFromUserDid, 784 + private pendingFetchLatestEvents: 785 + | Promise<{ 786 + events: ChatBskyConvoGetLog.OutputSchema['logs'] 787 + }> 788 + | undefined 789 + async fetchLatestEvents() { 790 + if (this.pendingFetchLatestEvents) return this.pendingFetchLatestEvents 791 + 792 + this.pendingFetchLatestEvents = new Promise<{ 793 + events: ChatBskyConvoGetLog.OutputSchema['logs'] 794 + }>(async (resolve, reject) => { 795 + try { 796 + // throw new Error('UNCOMMENT TO TEST POLL FAILURE') 797 + const response = await this.agent.api.chat.bsky.convo.getLog( 798 + { 799 + cursor: this.eventsCursor, 551 800 }, 552 - }, 553 - ) 554 - const {logs} = response.data 801 + { 802 + headers: { 803 + Authorization: this.__tempFromUserDid, 804 + }, 805 + }, 806 + ) 807 + const {logs} = response.data 808 + resolve({events: logs}) 809 + } catch (e) { 810 + reject(e) 811 + } finally { 812 + this.pendingFetchLatestEvents = undefined 813 + } 814 + }) 555 815 556 - let needsCommit = false 816 + return this.pendingFetchLatestEvents 817 + } 818 + 819 + private applyLatestEvents(events: ChatBskyConvoGetLog.OutputSchema['logs']) { 820 + let needsCommit = false 557 821 558 - for (const log of logs) { 822 + for (const ev of events) { 823 + /* 824 + * If there's a rev, we should handle it. If there's not a rev, we don't 825 + * know what it is. 826 + */ 827 + if (typeof ev.rev === 'string') { 559 828 /* 560 - * If there's a rev, we should handle it. If there's not a rev, we don't 561 - * know what it is. 829 + * We only care about new events 562 830 */ 563 - if (typeof log.rev === 'string') { 831 + if (ev.rev > (this.eventsCursor = this.eventsCursor || ev.rev)) { 564 832 /* 565 - * We only care about new events 833 + * Update rev regardless of if it's a ev type we care about or not 834 + */ 835 + this.eventsCursor = ev.rev 836 + 837 + /* 838 + * This is VERY important. We don't want to insert any messages from 839 + * your other chats. 566 840 */ 567 - if (log.rev > (this.eventsCursor = this.eventsCursor || log.rev)) { 568 - /* 569 - * Update rev regardless of if it's a log type we care about or not 570 - */ 571 - this.eventsCursor = log.rev 841 + if (ev.convoId !== this.convoId) continue 572 842 843 + if ( 844 + ChatBskyConvoDefs.isLogCreateMessage(ev) && 845 + ChatBskyConvoDefs.isMessageView(ev.message) 846 + ) { 847 + if (this.newMessages.has(ev.message.id)) { 848 + // Trust the ev as the source of truth on ordering 849 + this.newMessages.delete(ev.message.id) 850 + } 851 + this.newMessages.set(ev.message.id, ev.message) 852 + needsCommit = true 853 + } else if ( 854 + ChatBskyConvoDefs.isLogDeleteMessage(ev) && 855 + ChatBskyConvoDefs.isDeletedMessageView(ev.message) 856 + ) { 573 857 /* 574 - * This is VERY important. We don't want to insert any messages from 575 - * your other chats. 858 + * Update if we have this in state. If we don't, don't worry about it. 576 859 */ 577 - if (log.convoId !== this.convoId) continue 578 - 579 - if ( 580 - ChatBskyConvoDefs.isLogCreateMessage(log) && 581 - ChatBskyConvoDefs.isMessageView(log.message) 582 - ) { 583 - if (this.newMessages.has(log.message.id)) { 584 - // Trust the log as the source of truth on ordering 585 - this.newMessages.delete(log.message.id) 586 - } 587 - this.newMessages.set(log.message.id, log.message) 588 - needsCommit = true 589 - } else if ( 590 - ChatBskyConvoDefs.isLogDeleteMessage(log) && 591 - ChatBskyConvoDefs.isDeletedMessageView(log.message) 592 - ) { 860 + if (this.pastMessages.has(ev.message.id)) { 593 861 /* 594 - * Update if we have this in state. If we don't, don't worry about it. 862 + * For now, we remove deleted messages from the thread, if we receive one. 863 + * 864 + * To support them, it'd look something like this: 865 + * this.pastMessages.set(ev.message.id, ev.message) 595 866 */ 596 - if (this.pastMessages.has(log.message.id)) { 597 - /* 598 - * For now, we remove deleted messages from the thread, if we receive one. 599 - * 600 - * To support them, it'd look something like this: 601 - * this.pastMessages.set(log.message.id, log.message) 602 - */ 603 - this.pastMessages.delete(log.message.id) 604 - this.newMessages.delete(log.message.id) 605 - this.deletedMessages.delete(log.message.id) 606 - needsCommit = true 607 - } 867 + this.pastMessages.delete(ev.message.id) 868 + this.newMessages.delete(ev.message.id) 869 + this.deletedMessages.delete(ev.message.id) 870 + needsCommit = true 608 871 } 609 872 } 610 873 } 611 874 } 875 + } 612 876 613 - if (needsCommit) { 614 - this.commit() 615 - } 616 - } catch (e: any) { 617 - logger.error('Convo: failed to poll events') 618 - this.pollingFailure = true 619 - this.footerItems.set(ConvoItemError.PollFailed, { 620 - type: 'error-recoverable', 621 - key: ConvoItemError.PollFailed, 622 - code: ConvoItemError.PollFailed, 623 - retry: () => { 624 - this.footerItems.delete(ConvoItemError.PollFailed) 625 - this.pollingFailure = false 626 - this.commit() 627 - this.pollEvents() 628 - }, 629 - }) 877 + if (needsCommit) { 630 878 this.commit() 631 879 } 632 880 }
+21 -1
src/state/messages/index.tsx
··· 1 1 import React, {useContext, useState, useSyncExternalStore} from 'react' 2 + import {AppState} from 'react-native' 2 3 import {BskyAgent} from '@atproto-labs/api' 3 - import {useFocusEffect} from '@react-navigation/native' 4 + import {useFocusEffect, useIsFocused} from '@react-navigation/native' 4 5 5 6 import {Convo, ConvoParams, ConvoState} from '#/state/messages/convo' 6 7 import {useAgent} from '#/state/session' ··· 20 21 children, 21 22 convoId, 22 23 }: Pick<ConvoParams, 'convoId'> & {children: React.ReactNode}) { 24 + const isScreenFocused = useIsFocused() 23 25 const {serviceUrl} = useDmServiceUrlStorage() 24 26 const {getAgent} = useAgent() 25 27 const [convo] = useState( ··· 43 45 } 44 46 }, [convo]), 45 47 ) 48 + 49 + React.useEffect(() => { 50 + const handleAppStateChange = (nextAppState: string) => { 51 + if (isScreenFocused) { 52 + if (nextAppState === 'active') { 53 + convo.resume() 54 + } else { 55 + convo.background() 56 + } 57 + } 58 + } 59 + 60 + const sub = AppState.addEventListener('change', handleAppStateChange) 61 + 62 + return () => { 63 + sub.remove() 64 + } 65 + }, [convo, isScreenFocused]) 46 66 47 67 return <ChatContext.Provider value={service}>{children}</ChatContext.Provider> 48 68 }