Thread muting [APP-29] (#500)
* Implement thread muting * Apply filtering on background fetched notifs * Implement thread-muting tests
This commit is contained in:
parent
3e78c71018
commit
22884b53ad
16 changed files with 470 additions and 108 deletions
|
@ -42,6 +42,17 @@ export class PostThreadItemModel {
|
|||
return this.postRecord?.reply?.parent.uri
|
||||
}
|
||||
|
||||
get rootUri(): string {
|
||||
if (this.postRecord?.reply?.root.uri) {
|
||||
return this.postRecord.reply.root.uri
|
||||
}
|
||||
return this.uri
|
||||
}
|
||||
|
||||
get isThreadMuted() {
|
||||
return this.rootStore.mutedThreads.uris.has(this.rootUri)
|
||||
}
|
||||
|
||||
constructor(
|
||||
public rootStore: RootStoreModel,
|
||||
reactKey: string,
|
||||
|
@ -188,6 +199,14 @@ export class PostThreadItemModel {
|
|||
}
|
||||
}
|
||||
|
||||
async toggleThreadMute() {
|
||||
if (this.isThreadMuted) {
|
||||
this.rootStore.mutedThreads.uris.delete(this.rootUri)
|
||||
} else {
|
||||
this.rootStore.mutedThreads.uris.add(this.rootUri)
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.rootStore.agent.deletePost(this.post.uri)
|
||||
this.rootStore.emitPostDeleted(this.post.uri)
|
||||
|
@ -230,6 +249,19 @@ export class PostThreadModel {
|
|||
return this.error !== ''
|
||||
}
|
||||
|
||||
get rootUri(): string {
|
||||
if (this.thread) {
|
||||
if (this.thread.postRecord?.reply?.root.uri) {
|
||||
return this.thread.postRecord.reply.root.uri
|
||||
}
|
||||
}
|
||||
return this.resolvedUri
|
||||
}
|
||||
|
||||
get isThreadMuted() {
|
||||
return this.rootStore.mutedThreads.uris.has(this.rootUri)
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
|
@ -279,6 +311,14 @@ export class PostThreadModel {
|
|||
this.refresh()
|
||||
}
|
||||
|
||||
async toggleThreadMute() {
|
||||
if (this.isThreadMuted) {
|
||||
this.rootStore.mutedThreads.uris.delete(this.rootUri)
|
||||
} else {
|
||||
this.rootStore.mutedThreads.uris.add(this.rootUri)
|
||||
}
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
|
|
|
@ -48,6 +48,17 @@ export class PostModel implements RemoveIndex<Post.Record> {
|
|||
return this.hasLoaded && !this.hasContent
|
||||
}
|
||||
|
||||
get rootUri(): string {
|
||||
if (this.reply?.root.uri) {
|
||||
return this.reply.root.uri
|
||||
}
|
||||
return this.uri
|
||||
}
|
||||
|
||||
get isThreadMuted() {
|
||||
return this.rootStore.mutedThreads.uris.has(this.rootUri)
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
|
@ -55,6 +66,14 @@ export class PostModel implements RemoveIndex<Post.Record> {
|
|||
await this._load()
|
||||
}
|
||||
|
||||
async toggleThreadMute() {
|
||||
if (this.isThreadMuted) {
|
||||
this.rootStore.mutedThreads.uris.delete(this.rootUri)
|
||||
} else {
|
||||
this.rootStore.mutedThreads.uris.add(this.rootUri)
|
||||
}
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
|
|
|
@ -160,6 +160,13 @@ export class NotificationsFeedItemModel {
|
|||
return ''
|
||||
}
|
||||
|
||||
get reasonSubjectRootUri(): string | undefined {
|
||||
if (this.additionalPost) {
|
||||
return this.additionalPost.rootUri
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
toSupportedRecord(v: unknown): SupportedRecord | undefined {
|
||||
for (const ns of [
|
||||
AppBskyFeedPost,
|
||||
|
@ -227,7 +234,7 @@ export class NotificationsFeedModel {
|
|||
|
||||
// data
|
||||
notifications: NotificationsFeedItemModel[] = []
|
||||
queuedNotifications: undefined | ListNotifications.Notification[] = undefined
|
||||
queuedNotifications: undefined | NotificationsFeedItemModel[] = undefined
|
||||
unreadCount = 0
|
||||
|
||||
// this is used to help trigger push notifications
|
||||
|
@ -354,7 +361,13 @@ export class NotificationsFeedModel {
|
|||
queue.push(notif)
|
||||
}
|
||||
|
||||
this._setQueued(this._filterNotifications(queue))
|
||||
// NOTE
|
||||
// because filtering depends on the added information we have to fetch
|
||||
// the full models here. this is *not* ideal performance and we need
|
||||
// to update the notifications route to give all the info we need
|
||||
// -prf
|
||||
const queueModels = await this._fetchItemModels(queue)
|
||||
this._setQueued(this._filterNotifications(queueModels))
|
||||
this._countUnread()
|
||||
} catch (e) {
|
||||
this.rootStore.log.error('NotificationsModel:syncQueue failed', {e})
|
||||
|
@ -452,7 +465,8 @@ export class NotificationsFeedModel {
|
|||
res.data.notifications[0],
|
||||
)
|
||||
await notif.fetchAdditionalData()
|
||||
return notif
|
||||
const filtered = this._filterNotifications([notif])
|
||||
return filtered[0]
|
||||
}
|
||||
|
||||
// state transitions
|
||||
|
@ -505,23 +519,26 @@ export class NotificationsFeedModel {
|
|||
}
|
||||
|
||||
_filterNotifications(
|
||||
items: ListNotifications.Notification[],
|
||||
): ListNotifications.Notification[] {
|
||||
items: NotificationsFeedItemModel[],
|
||||
): NotificationsFeedItemModel[] {
|
||||
return items.filter(item => {
|
||||
return (
|
||||
this.rootStore.preferences.getLabelPreference(item.labels).pref !==
|
||||
const hideByLabel =
|
||||
this.rootStore.preferences.getLabelPreference(item.labels).pref ===
|
||||
'hide'
|
||||
let mutedThread = !!(
|
||||
item.reasonSubjectRootUri &&
|
||||
this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri)
|
||||
)
|
||||
return !hideByLabel && !mutedThread
|
||||
})
|
||||
}
|
||||
|
||||
async _processNotifications(
|
||||
async _fetchItemModels(
|
||||
items: ListNotifications.Notification[],
|
||||
): Promise<NotificationsFeedItemModel[]> {
|
||||
const promises = []
|
||||
const itemModels: NotificationsFeedItemModel[] = []
|
||||
items = this._filterNotifications(items)
|
||||
for (const item of groupNotifications(items)) {
|
||||
for (const item of items) {
|
||||
const itemModel = new NotificationsFeedItemModel(
|
||||
this.rootStore,
|
||||
`item-${_idCounter++}`,
|
||||
|
@ -541,7 +558,14 @@ export class NotificationsFeedModel {
|
|||
return itemModels
|
||||
}
|
||||
|
||||
_setQueued(queued: undefined | ListNotifications.Notification[]) {
|
||||
async _processNotifications(
|
||||
items: ListNotifications.Notification[],
|
||||
): Promise<NotificationsFeedItemModel[]> {
|
||||
const itemModels = await this._fetchItemModels(groupNotifications(items))
|
||||
return this._filterNotifications(itemModels)
|
||||
}
|
||||
|
||||
_setQueued(queued: undefined | NotificationsFeedItemModel[]) {
|
||||
this.queuedNotifications = queued
|
||||
}
|
||||
|
||||
|
|
|
@ -72,6 +72,17 @@ export class PostsFeedItemModel {
|
|||
makeAutoObservable(this, {rootStore: false})
|
||||
}
|
||||
|
||||
get rootUri(): string {
|
||||
if (this.reply?.root.uri) {
|
||||
return this.reply.root.uri
|
||||
}
|
||||
return this.post.uri
|
||||
}
|
||||
|
||||
get isThreadMuted() {
|
||||
return this.rootStore.mutedThreads.uris.has(this.rootUri)
|
||||
}
|
||||
|
||||
copy(v: FeedViewPost) {
|
||||
this.post = v.post
|
||||
this.reply = v.reply
|
||||
|
@ -145,6 +156,14 @@ export class PostsFeedItemModel {
|
|||
}
|
||||
}
|
||||
|
||||
async toggleThreadMute() {
|
||||
if (this.isThreadMuted) {
|
||||
this.rootStore.mutedThreads.uris.delete(this.rootUri)
|
||||
} else {
|
||||
this.rootStore.mutedThreads.uris.add(this.rootUri)
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.rootStore.agent.deletePost(this.post.uri)
|
||||
this.rootStore.emitPostDeleted(this.post.uri)
|
||||
|
|
29
src/state/models/muted-threads.ts
Normal file
29
src/state/models/muted-threads.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* This is a temporary client-side system for storing muted threads
|
||||
* When the system lands on prod we should switch to that
|
||||
*/
|
||||
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import {isObj, hasProp, isStrArray} from 'lib/type-guards'
|
||||
|
||||
export class MutedThreads {
|
||||
uris: Set<string> = new Set()
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{serialize: false, hydrate: false},
|
||||
{autoBind: true},
|
||||
)
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return {uris: Array.from(this.uris)}
|
||||
}
|
||||
|
||||
hydrate(v: unknown) {
|
||||
if (isObj(v) && hasProp(v, 'uris') && isStrArray(v.uris)) {
|
||||
this.uris = new Set(v.uris)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import {InvitedUsers} from './invited-users'
|
|||
import {PreferencesModel} from './ui/preferences'
|
||||
import {resetToTab} from '../../Navigation'
|
||||
import {ImageSizesCache} from './cache/image-sizes'
|
||||
import {MutedThreads} from './muted-threads'
|
||||
|
||||
export const appInfo = z.object({
|
||||
build: z.string(),
|
||||
|
@ -41,6 +42,7 @@ export class RootStoreModel {
|
|||
profiles = new ProfilesCache(this)
|
||||
linkMetas = new LinkMetasCache(this)
|
||||
imageSizes = new ImageSizesCache()
|
||||
mutedThreads = new MutedThreads()
|
||||
|
||||
constructor(agent: BskyAgent) {
|
||||
this.agent = agent
|
||||
|
@ -64,6 +66,7 @@ export class RootStoreModel {
|
|||
shell: this.shell.serialize(),
|
||||
preferences: this.preferences.serialize(),
|
||||
invitedUsers: this.invitedUsers.serialize(),
|
||||
mutedThreads: this.mutedThreads.serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +93,9 @@ export class RootStoreModel {
|
|||
if (hasProp(v, 'invitedUsers')) {
|
||||
this.invitedUsers.hydrate(v.invitedUsers)
|
||||
}
|
||||
if (hasProp(v, 'mutedThreads')) {
|
||||
this.mutedThreads.hydrate(v.mutedThreads)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue