WIP PWA for Grain
0
fork

Configure Feed

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

feat: add infinite scroll to notifications

+75 -13
+62 -7
src/components/pages/grain-notifications.js
··· 10 10 _notifications: { state: true }, 11 11 _loading: { state: true }, 12 12 _error: { state: true }, 13 - _user: { state: true } 13 + _user: { state: true }, 14 + _hasMore: { state: true }, 15 + _cursor: { state: true } 14 16 }; 15 17 16 18 static styles = css` ··· 105 107 display: block; 106 108 width: fit-content; 107 109 } 110 + #sentinel { 111 + height: 1px; 112 + } 108 113 `; 109 114 115 + #observer = null; 116 + 110 117 constructor() { 111 118 super(); 112 119 this._notifications = []; 113 120 this._loading = true; 114 121 this._error = null; 115 122 this._user = auth.user; 123 + this._hasMore = true; 124 + this._cursor = null; 116 125 } 117 126 118 127 connectedCallback() { ··· 133 142 disconnectedCallback() { 134 143 super.disconnectedCallback(); 135 144 this._unsubscribe?.(); 145 + this.#observer?.disconnect(); 146 + } 147 + 148 + firstUpdated() { 149 + if (this._user) { 150 + this.#setupInfiniteScroll(); 151 + } 152 + } 153 + 154 + #setupInfiniteScroll() { 155 + const sentinel = this.shadowRoot.getElementById('sentinel'); 156 + if (!sentinel) return; 157 + 158 + this.#observer = new IntersectionObserver( 159 + (entries) => { 160 + if (entries[0].isIntersecting) { 161 + this.#loadMore(); 162 + } 163 + }, 164 + { rootMargin: '200px' } 165 + ); 166 + 167 + this.#observer.observe(sentinel); 136 168 } 137 169 138 170 async #loadNotifications() { ··· 144 176 try { 145 177 this._loading = true; 146 178 this._error = null; 147 - const result = await grainApi.getNotifications(this._user.did); 179 + const result = await grainApi.getNotifications(this._user.did, { first: 20 }); 148 180 this._notifications = result.notifications; 181 + this._hasMore = result.pageInfo.hasNextPage; 182 + this._cursor = result.pageInfo.endCursor; 149 183 } catch (err) { 150 184 console.error('Failed to load notifications:', err); 185 + this._error = err.message; 186 + } finally { 187 + this._loading = false; 188 + } 189 + } 190 + 191 + async #loadMore() { 192 + if (this._loading || !this._hasMore || !this._user?.did) return; 193 + 194 + try { 195 + this._loading = true; 196 + const result = await grainApi.getNotifications(this._user.did, { 197 + first: 20, 198 + after: this._cursor 199 + }); 200 + 201 + this._notifications = [...this._notifications, ...result.notifications]; 202 + this._hasMore = result.pageInfo.hasNextPage; 203 + this._cursor = result.pageInfo.endCursor; 204 + } catch (err) { 205 + console.error('Failed to load more notifications:', err); 151 206 this._error = err.message; 152 207 } finally { 153 208 this._loading = false; ··· 305 360 <p class="error">${this._error}</p> 306 361 ` : ''} 307 362 308 - ${this._loading ? html` 309 - <grain-spinner></grain-spinner> 310 - ` : ''} 311 - 312 363 ${!this._loading && !this._error && this._notifications.length === 0 ? html` 313 364 <p class="empty">No notifications yet</p> 314 365 ` : ''} 315 366 316 - ${!this._loading && this._notifications.length > 0 ? html` 367 + ${this._notifications.length > 0 ? html` 317 368 <ul class="notification-list"> 318 369 ${this._notifications.map(n => this.#renderNotification(n))} 319 370 </ul> 320 371 ` : ''} 372 + 373 + <div id="sentinel"></div> 374 + 375 + ${this._loading ? html`<grain-spinner></grain-spinner>` : ''} 321 376 </grain-feed-layout> 322 377 `; 323 378 }
+13 -6
src/services/grain-api.js
··· 555 555 }; 556 556 } 557 557 558 - async getNotifications(viewerDid, { first = 50 } = {}) { 558 + async getNotifications(viewerDid, { first = 20, after = null } = {}) { 559 559 const query = ` 560 - query Notifications($viewerDid: String!, $first: Int) { 561 - notifications(viewerDid: $viewerDid, first: $first) { 560 + query Notifications($viewerDid: String!, $first: Int, $after: String) { 561 + notifications(viewerDid: $viewerDid, first: $first, after: $after) { 562 562 edges { 563 563 node { 564 564 __typename ··· 675 675 } 676 676 } 677 677 } 678 + pageInfo { 679 + hasNextPage 680 + endCursor 681 + } 678 682 } 679 683 } 680 684 `; 681 685 682 - const response = await this.#execute(query, { viewerDid, first }); 686 + const response = await this.#execute(query, { viewerDid, first, after }); 683 687 return this.#transformNotificationsResponse(response, viewerDid); 684 688 } 685 689 686 690 #transformNotificationsResponse(response, viewerDid) { 687 691 const connection = response.data?.notifications; 688 - if (!connection) return { notifications: [] }; 692 + if (!connection) return { notifications: [], pageInfo: { hasNextPage: false, endCursor: null } }; 689 693 690 694 const notifications = connection.edges 691 695 .map(edge => { ··· 768 772 }) 769 773 .filter(Boolean); 770 774 771 - return { notifications }; 775 + return { 776 + notifications, 777 + pageInfo: connection.pageInfo || { hasNextPage: false, endCursor: null } 778 + }; 772 779 } 773 780 774 781 #getNotificationReason(node, viewerDid) {