WIP PWA for Grain
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add getNotifications to grainApi

+251
+251
src/services/grain-api.js
··· 554 554 comments 555 555 }; 556 556 } 557 + 558 + async getNotifications(viewerDid, { first = 50 } = {}) { 559 + const query = ` 560 + query Notifications($viewerDid: String!, $first: Int) { 561 + notifications(viewerDid: $viewerDid, first: $first) { 562 + edges { 563 + node { 564 + __typename 565 + ... on SocialGrainFavorite { 566 + uri 567 + did 568 + createdAt 569 + subject 570 + subjectResolved { 571 + ... on SocialGrainGallery { 572 + uri 573 + title 574 + actorHandle 575 + socialGrainGalleryItemViaGallery(first: 1, sortBy: [{ field: position, direction: ASC }]) { 576 + edges { 577 + node { 578 + itemResolved { 579 + ... on SocialGrainPhoto { 580 + photo { url(preset: "feed_thumbnail") } 581 + } 582 + } 583 + } 584 + } 585 + } 586 + } 587 + } 588 + socialGrainActorProfileByDid { 589 + displayName 590 + actorHandle 591 + avatar { url(preset: "avatar") } 592 + } 593 + } 594 + ... on SocialGrainGraphFollow { 595 + uri 596 + did 597 + createdAt 598 + socialGrainActorProfileByDid { 599 + displayName 600 + actorHandle 601 + avatar { url(preset: "avatar") } 602 + } 603 + } 604 + ... on SocialGrainComment { 605 + uri 606 + did 607 + createdAt 608 + text 609 + subject 610 + focus 611 + replyTo 612 + facets 613 + subjectResolved { 614 + ... on SocialGrainGallery { 615 + uri 616 + title 617 + actorHandle 618 + socialGrainGalleryItemViaGallery(first: 1, sortBy: [{ field: position, direction: ASC }]) { 619 + edges { 620 + node { 621 + itemResolved { 622 + ... on SocialGrainPhoto { 623 + photo { url(preset: "feed_thumbnail") } 624 + } 625 + } 626 + } 627 + } 628 + } 629 + } 630 + } 631 + focusResolved { 632 + ... on SocialGrainPhoto { 633 + uri 634 + alt 635 + photo { url(preset: "feed_thumbnail") } 636 + } 637 + } 638 + replyToResolved { 639 + ... on SocialGrainComment { 640 + uri 641 + text 642 + actorHandle 643 + } 644 + } 645 + socialGrainActorProfileByDid { 646 + displayName 647 + actorHandle 648 + avatar { url(preset: "avatar") } 649 + } 650 + } 651 + ... on SocialGrainGallery { 652 + uri 653 + did 654 + createdAt 655 + title 656 + description 657 + facets 658 + actorHandle 659 + socialGrainGalleryItemViaGallery(first: 1, sortBy: [{ field: position, direction: ASC }]) { 660 + edges { 661 + node { 662 + itemResolved { 663 + ... on SocialGrainPhoto { 664 + photo { url(preset: "feed_thumbnail") } 665 + } 666 + } 667 + } 668 + } 669 + } 670 + socialGrainActorProfileByDid { 671 + displayName 672 + actorHandle 673 + avatar { url(preset: "avatar") } 674 + } 675 + } 676 + } 677 + } 678 + } 679 + } 680 + `; 681 + 682 + const response = await this.#execute(query, { viewerDid, first }); 683 + return this.#transformNotificationsResponse(response, viewerDid); 684 + } 685 + 686 + #transformNotificationsResponse(response, viewerDid) { 687 + const connection = response.data?.notifications; 688 + if (!connection) return { notifications: [] }; 689 + 690 + const notifications = connection.edges 691 + .map(edge => { 692 + const node = edge.node; 693 + const reason = this.#getNotificationReason(node, viewerDid); 694 + if (!reason) return null; 695 + 696 + const profile = node.socialGrainActorProfileByDid; 697 + const author = { 698 + handle: profile?.actorHandle || '', 699 + displayName: profile?.displayName || '', 700 + avatarUrl: profile?.avatar?.url || '' 701 + }; 702 + 703 + const base = { 704 + uri: node.uri, 705 + createdAt: node.createdAt, 706 + reason, 707 + author 708 + }; 709 + 710 + switch (node.__typename) { 711 + case 'SocialGrainFavorite': { 712 + const gallery = node.subjectResolved; 713 + const thumb = gallery?.socialGrainGalleryItemViaGallery?.edges?.[0]?.node?.itemResolved?.photo?.url; 714 + return { 715 + ...base, 716 + gallery: gallery ? { 717 + uri: gallery.uri, 718 + title: gallery.title, 719 + handle: gallery.actorHandle, 720 + thumbnailUrl: thumb || '' 721 + } : null 722 + }; 723 + } 724 + case 'SocialGrainGraphFollow': 725 + return base; 726 + case 'SocialGrainComment': { 727 + const gallery = node.subjectResolved; 728 + const galleryThumb = gallery?.socialGrainGalleryItemViaGallery?.edges?.[0]?.node?.itemResolved?.photo?.url; 729 + const focusPhoto = node.focusResolved; 730 + const replyTo = node.replyToResolved; 731 + return { 732 + ...base, 733 + text: node.text, 734 + gallery: gallery ? { 735 + uri: gallery.uri, 736 + title: gallery.title, 737 + handle: gallery.actorHandle, 738 + thumbnailUrl: galleryThumb || '' 739 + } : null, 740 + focusPhoto: focusPhoto ? { 741 + uri: focusPhoto.uri, 742 + alt: focusPhoto.alt, 743 + thumbnailUrl: focusPhoto.photo?.url || '' 744 + } : null, 745 + replyTo: replyTo ? { 746 + uri: replyTo.uri, 747 + text: replyTo.text, 748 + handle: replyTo.actorHandle 749 + } : null 750 + }; 751 + } 752 + case 'SocialGrainGallery': { 753 + const thumb = node.socialGrainGalleryItemViaGallery?.edges?.[0]?.node?.itemResolved?.photo?.url; 754 + return { 755 + ...base, 756 + gallery: { 757 + uri: node.uri, 758 + title: node.title, 759 + description: node.description, 760 + handle: node.actorHandle, 761 + thumbnailUrl: thumb || '' 762 + } 763 + }; 764 + } 765 + default: 766 + return null; 767 + } 768 + }) 769 + .filter(Boolean); 770 + 771 + return { notifications }; 772 + } 773 + 774 + #getNotificationReason(node, viewerDid) { 775 + switch (node.__typename) { 776 + case 'SocialGrainFavorite': 777 + return 'gallery-favorite'; 778 + case 'SocialGrainGraphFollow': 779 + return 'follow'; 780 + case 'SocialGrainComment': 781 + if (this.#hasMentionFacet(node.facets, viewerDid)) { 782 + return 'gallery-comment-mention'; 783 + } 784 + if (node.replyTo) { 785 + return 'reply'; 786 + } 787 + return 'gallery-comment'; 788 + case 'SocialGrainGallery': 789 + if (this.#hasMentionFacet(node.facets, viewerDid)) { 790 + return 'gallery-mention'; 791 + } 792 + return null; 793 + default: 794 + return null; 795 + } 796 + } 797 + 798 + #hasMentionFacet(facets, viewerDid) { 799 + if (!Array.isArray(facets)) return false; 800 + return facets.some(facet => { 801 + const features = facet.features; 802 + if (!Array.isArray(features)) return false; 803 + return features.some(f => 804 + f.$type === 'app.bsky.richtext.facet#mention' && f.did === viewerDid 805 + ); 806 + }); 807 + } 557 808 } 558 809 559 810 export const grainApi = new GrainApiService();