the browser-facing portion of osu!
at master 4.6 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 2// See the LICENCE file in the repository root for full licence text. 3 4import { NotificationStackJson } from 'interfaces/notification-json'; 5import { action, computed, makeObservable, observable } from 'mobx'; 6import Notification from 'models/notification'; 7import { Name } from 'models/notification-type'; 8import { NotificationCursor } from 'notifications/notification-cursor'; 9import NotificationDeletable from 'notifications/notification-deletable'; 10import { NotificationIdentity } from 'notifications/notification-identity'; 11import NotificationReadable from 'notifications/notification-readable'; 12import { NotificationResolver } from 'notifications/notification-resolver'; 13import { NotificationContextData } from 'notifications-context'; 14 15export default class NotificationStack implements NotificationReadable, NotificationDeletable { 16 @observable cursor: NotificationCursor | null = null; 17 @observable displayOrder = 0; 18 @observable isDeleting = false; 19 @observable isLoading = false; 20 @observable isMarkingAsRead = false; 21 @observable notifications = new Map<number, Notification>(); 22 @observable total = 0; 23 24 // This is for cases when there are more notifications available to be loaded 25 // but all the currently loaded notifications have seen removed, allowing the last known notification 26 // to be shown in the stack's header instead of nothing. 27 @observable private lastNotification: Notification | null = null; 28 29 @computed 30 get canMarkAsRead() { 31 for (const [, notifications] of this.notifications) { 32 if (notifications.canMarkRead) return true; 33 } 34 35 return false; 36 } 37 38 @computed 39 get first() { 40 return this.orderedNotifications[0] ?? this.lastNotification; 41 } 42 43 @computed 44 get hasMore() { 45 return !(this.notifications.size >= this.total || this.cursor == null); 46 } 47 48 @computed 49 get hasVisibleNotifications() { 50 return this.notifications.size > 0; 51 } 52 53 @computed 54 get id() { 55 return `${this.objectType}-${this.objectId}-${this.category}`; 56 } 57 58 get identity(): NotificationIdentity { 59 return { 60 category: this.category, 61 objectId: this.objectId, 62 objectType: this.objectType, 63 }; 64 } 65 66 @computed 67 get isSingle() { 68 return this.total === 1; 69 } 70 71 @computed 72 get orderedNotifications() { 73 return [...this.notifications.values()].sort((x, y) => y.id - x.id); 74 } 75 76 get type() { 77 return this.objectType; 78 } 79 80 constructor( 81 readonly objectId: number, 82 readonly objectType: Name, 83 readonly category: string, 84 readonly resolver: NotificationResolver, 85 ) { 86 makeObservable(this); 87 } 88 89 static fromJson(json: NotificationStackJson, resolver: NotificationResolver) { 90 const obj = new NotificationStack(json.object_id, json.object_type, json.category, resolver); 91 obj.updateWithJson(json); 92 return obj; 93 } 94 95 @action 96 add(notification: Notification) { 97 this.notifications.set(notification.id, notification); 98 this.displayOrder = Math.max(notification.id, this.displayOrder); 99 } 100 101 @action 102 delete() { 103 this.resolver.delete(this); 104 } 105 106 @action 107 deleteItem(notification?: Notification) { 108 // not from this stack, ignore. 109 if (notification == null || !this.notifications.has(notification.id)) return; 110 this.resolver.delete(notification); 111 } 112 113 @action 114 loadMore(context: NotificationContextData) { 115 if (this.cursor == null) return; 116 117 this.isLoading = true; 118 119 this.resolver.loadMore(this.identity, context, this.cursor) 120 .always(action(() => { 121 this.isLoading = false; 122 })); 123 } 124 125 @action 126 markAsRead(notification?: Notification) { 127 // not from this stack, ignore. 128 if (notification == null || !this.notifications.has(notification.id)) return; 129 this.resolver.queueMarkAsRead(notification); 130 } 131 132 @action 133 markStackAsRead() { 134 this.resolver.queueMarkAsRead(this); 135 } 136 137 @action 138 remove(notification: Notification) { 139 if (this.notifications.size === 1) { 140 // doesn't matter if the notification that's being removed doesn't actually exist in the stack, 141 // this can still be set. 142 this.lastNotification = this.notifications.values().next().value; 143 } 144 145 const existed = this.notifications.delete(notification.id); 146 if (existed) this.total--; 147 return existed; 148 } 149 150 @action 151 updateWithJson(json: NotificationStackJson) { 152 this.cursor = json.cursor; 153 this.total = json.total; 154 } 155} 156 157export function idFromJson(json: NotificationStackJson) { 158 return `${json.object_type}-${json.object_id}-${json.category}`; 159}