Initial pass at push notifications + some fixes to the session management (#91)
* Fix: test the session during resume to ensure it's valid * Don't delete sessions for now * Add notifee and request notif permissions on first login * Set unread notifications badge on app icon * Trigger a notifee card on new notifications * Experimental: use react-native-background-fetch to check for notifications * Add missing mocks * Fix to resumeSession()zio/stable
parent
21f5f4de15
commit
869f6c4e0e
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
requestPermission: jest.fn(),
|
||||||
|
onForegroundEvent: jest.fn(),
|
||||||
|
setBadgeCount: jest.fn(),
|
||||||
|
displayNotification: jest.fn(),
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default {
|
||||||
|
configure: jest.fn().mockResolvedValue(0),
|
||||||
|
finish: jest.fn(),
|
||||||
|
}
|
|
@ -64,7 +64,7 @@ export const mockedProfileStore = {
|
||||||
isUser: true,
|
isUser: true,
|
||||||
isScene: false,
|
isScene: false,
|
||||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||||
refresh: jest.fn(),
|
refresh: jest.fn().mockResolvedValue({}),
|
||||||
toggleFollowing: jest.fn().mockResolvedValue({}),
|
toggleFollowing: jest.fn().mockResolvedValue({}),
|
||||||
updateProfile: jest.fn(),
|
updateProfile: jest.fn(),
|
||||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _load, _replaceAll
|
// unknown required because of the missing private methods: _xLoading, _xIdle, _load, _replaceAll
|
||||||
|
@ -106,7 +106,7 @@ export const mockedMembersStore = {
|
||||||
isEmpty: false,
|
isEmpty: false,
|
||||||
isMember: jest.fn(),
|
isMember: jest.fn(),
|
||||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||||
refresh: jest.fn(),
|
refresh: jest.fn().mockResolvedValue({}),
|
||||||
loadMore: jest.fn(),
|
loadMore: jest.fn(),
|
||||||
removeMember: jest.fn(),
|
removeMember: jest.fn(),
|
||||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _replaceAll, _append
|
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _replaceAll, _append
|
||||||
|
@ -149,7 +149,7 @@ export const mockedMembershipsStore = {
|
||||||
isEmpty: false,
|
isEmpty: false,
|
||||||
isMemberOf: jest.fn(),
|
isMemberOf: jest.fn(),
|
||||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||||
refresh: jest.fn(),
|
refresh: jest.fn().mockResolvedValue({}),
|
||||||
loadMore: jest.fn(),
|
loadMore: jest.fn(),
|
||||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _replaceAll, _append
|
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _replaceAll, _append
|
||||||
} as unknown as MembershipsViewModel
|
} as unknown as MembershipsViewModel
|
||||||
|
@ -413,6 +413,7 @@ export const mockedNotificationsViewItemStore = {
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
}),
|
}),
|
||||||
fetchAdditionalData: jest.fn(),
|
fetchAdditionalData: jest.fn(),
|
||||||
|
toNotifeeOpts: jest.fn(),
|
||||||
} as NotificationsViewItemModel
|
} as NotificationsViewItemModel
|
||||||
|
|
||||||
export const mockedNotificationsStore = {
|
export const mockedNotificationsStore = {
|
||||||
|
@ -510,7 +511,7 @@ export const mockedNavigationTabStore = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
navigate: jest.fn(),
|
navigate: jest.fn(),
|
||||||
refresh: jest.fn(),
|
refresh: jest.fn().mockResolvedValue({}),
|
||||||
goBack: jest.fn(),
|
goBack: jest.fn(),
|
||||||
fixedTabReset: jest.fn(),
|
fixedTabReset: jest.fn(),
|
||||||
goForward: jest.fn(),
|
goForward: jest.fn(),
|
||||||
|
@ -539,7 +540,7 @@ export const mockedNavigationStore = {
|
||||||
tabCount: 1,
|
tabCount: 1,
|
||||||
isCurrentScreen: jest.fn(),
|
isCurrentScreen: jest.fn(),
|
||||||
navigate: jest.fn(),
|
navigate: jest.fn(),
|
||||||
refresh: jest.fn(),
|
refresh: jest.fn().mockResolvedValue({}),
|
||||||
setTitle: jest.fn(),
|
setTitle: jest.fn(),
|
||||||
handleLink: jest.fn(),
|
handleLink: jest.fn(),
|
||||||
switchTo: jest.fn(),
|
switchTo: jest.fn(),
|
||||||
|
@ -587,7 +588,7 @@ export const mockedMeStore = {
|
||||||
clear: jest.fn(),
|
clear: jest.fn(),
|
||||||
load: jest.fn(),
|
load: jest.fn(),
|
||||||
clearNotificationCount: jest.fn(),
|
clearNotificationCount: jest.fn(),
|
||||||
fetchStateUpdate: jest.fn(),
|
fetchNotifications: jest.fn(),
|
||||||
refreshMemberships: jest.fn(),
|
refreshMemberships: jest.fn(),
|
||||||
} as MeModel
|
} as MeModel
|
||||||
|
|
||||||
|
@ -679,7 +680,7 @@ export const mockedProfileUiStore = {
|
||||||
setSelectedViewIndex: jest.fn(),
|
setSelectedViewIndex: jest.fn(),
|
||||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
refresh: jest.fn(),
|
refresh: jest.fn().mockResolvedValue({}),
|
||||||
loadMore: jest.fn(),
|
loadMore: jest.fn(),
|
||||||
} as ProfileUiModel
|
} as ProfileUiModel
|
||||||
|
|
||||||
|
@ -788,7 +789,7 @@ export const mockedSuggestedActorsStore = {
|
||||||
hasError: false,
|
hasError: false,
|
||||||
isEmpty: false,
|
isEmpty: false,
|
||||||
setup: jest.fn().mockResolvedValue(null),
|
setup: jest.fn().mockResolvedValue(null),
|
||||||
refresh: jest.fn(),
|
refresh: jest.fn().mockResolvedValue({}),
|
||||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _appendAll, _append
|
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _appendAll, _append
|
||||||
} as unknown as SuggestedActorsViewModel
|
} as unknown as SuggestedActorsViewModel
|
||||||
|
|
||||||
|
@ -828,7 +829,7 @@ export const mockedUserFollowersStore = {
|
||||||
hasError: false,
|
hasError: false,
|
||||||
isEmpty: false,
|
isEmpty: false,
|
||||||
setup: jest.fn(),
|
setup: jest.fn(),
|
||||||
refresh: jest.fn(),
|
refresh: jest.fn().mockResolvedValue({}),
|
||||||
loadMore: jest.fn(),
|
loadMore: jest.fn(),
|
||||||
// unknown required because of the missing private methods: _xIdle, _xLoading, _fetch, _replaceAll, _append
|
// unknown required because of the missing private methods: _xIdle, _xLoading, _fetch, _replaceAll, _append
|
||||||
} as unknown as UserFollowersViewModel
|
} as unknown as UserFollowersViewModel
|
||||||
|
@ -869,7 +870,7 @@ export const mockedUserFollowsStore = {
|
||||||
hasError: false,
|
hasError: false,
|
||||||
isEmpty: false,
|
isEmpty: false,
|
||||||
setup: jest.fn(),
|
setup: jest.fn(),
|
||||||
refresh: jest.fn(),
|
refresh: jest.fn().mockResolvedValue({}),
|
||||||
loadMore: jest.fn(),
|
loadMore: jest.fn(),
|
||||||
// unknown required because of the missing private methods: _xIdle, _xLoading, _fetch, _replaceAll, _append
|
// unknown required because of the missing private methods: _xIdle, _xLoading, _fetch, _replaceAll, _append
|
||||||
} as unknown as UserFollowsViewModel
|
} as unknown as UserFollowsViewModel
|
||||||
|
|
|
@ -160,7 +160,7 @@ describe('MeModel', () => {
|
||||||
|
|
||||||
it('should update notifs count with fetchStateUpdate()', async () => {
|
it('should update notifs count with fetchStateUpdate()', async () => {
|
||||||
meModel.notifications = {
|
meModel.notifications = {
|
||||||
refresh: jest.fn(),
|
refresh: jest.fn().mockResolvedValue({}),
|
||||||
} as unknown as NotificationsViewModel
|
} as unknown as NotificationsViewModel
|
||||||
|
|
||||||
jest
|
jest
|
||||||
|
@ -173,7 +173,7 @@ describe('MeModel', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await meModel.fetchStateUpdate()
|
await meModel.fetchNotifications()
|
||||||
expect(meModel.notificationCount).toBe(1)
|
expect(meModel.notificationCount).toBe(1)
|
||||||
expect(meModel.notifications.refresh).toHaveBeenCalled()
|
expect(meModel.notifications.refresh).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
|
@ -340,6 +340,8 @@ PODS:
|
||||||
- React-perflogger (= 0.71.0)
|
- React-perflogger (= 0.71.0)
|
||||||
- rn-fetch-blob (0.12.0):
|
- rn-fetch-blob (0.12.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- RNBackgroundFetch (4.1.8):
|
||||||
|
- React-Core
|
||||||
- RNCAsyncStorage (1.17.11):
|
- RNCAsyncStorage (1.17.11):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNCClipboard (1.11.1):
|
- RNCClipboard (1.11.1):
|
||||||
|
@ -359,6 +361,11 @@ PODS:
|
||||||
- TOCropViewController
|
- TOCropViewController
|
||||||
- RNInAppBrowser (3.7.0):
|
- RNInAppBrowser (3.7.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- RNNotifee (7.4.0):
|
||||||
|
- React-Core
|
||||||
|
- RNNotifee/NotifeeCore (= 7.4.0)
|
||||||
|
- RNNotifee/NotifeeCore (7.4.0):
|
||||||
|
- React-Core
|
||||||
- RNReactNativeHapticFeedback (1.14.0):
|
- RNReactNativeHapticFeedback (1.14.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNReanimated (2.13.0):
|
- RNReanimated (2.13.0):
|
||||||
|
@ -448,12 +455,14 @@ DEPENDENCIES:
|
||||||
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
||||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||||
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
|
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
|
||||||
|
- RNBackgroundFetch (from `../node_modules/react-native-background-fetch`)
|
||||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||||
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
|
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
|
||||||
- RNFS (from `../node_modules/react-native-fs`)
|
- RNFS (from `../node_modules/react-native-fs`)
|
||||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||||
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
|
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
|
||||||
- RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`)
|
- RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`)
|
||||||
|
- "RNNotifee (from `../node_modules/@notifee/react-native`)"
|
||||||
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
|
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
|
||||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||||
- RNScreens (from `../node_modules/react-native-screens`)
|
- RNScreens (from `../node_modules/react-native-screens`)
|
||||||
|
@ -556,6 +565,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../node_modules/react-native/ReactCommon"
|
||||||
rn-fetch-blob:
|
rn-fetch-blob:
|
||||||
:path: "../node_modules/rn-fetch-blob"
|
:path: "../node_modules/rn-fetch-blob"
|
||||||
|
RNBackgroundFetch:
|
||||||
|
:path: "../node_modules/react-native-background-fetch"
|
||||||
RNCAsyncStorage:
|
RNCAsyncStorage:
|
||||||
:path: "../node_modules/@react-native-async-storage/async-storage"
|
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||||
RNCClipboard:
|
RNCClipboard:
|
||||||
|
@ -568,6 +579,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/react-native-image-crop-picker"
|
:path: "../node_modules/react-native-image-crop-picker"
|
||||||
RNInAppBrowser:
|
RNInAppBrowser:
|
||||||
:path: "../node_modules/react-native-inappbrowser-reborn"
|
:path: "../node_modules/react-native-inappbrowser-reborn"
|
||||||
|
RNNotifee:
|
||||||
|
:path: "../node_modules/@notifee/react-native"
|
||||||
RNReactNativeHapticFeedback:
|
RNReactNativeHapticFeedback:
|
||||||
:path: "../node_modules/react-native-haptic-feedback"
|
:path: "../node_modules/react-native-haptic-feedback"
|
||||||
RNReanimated:
|
RNReanimated:
|
||||||
|
@ -629,12 +642,14 @@ SPEC CHECKSUMS:
|
||||||
React-runtimeexecutor: ac80782d9d76ba2b0f709f4de0c427fe33c352dc
|
React-runtimeexecutor: ac80782d9d76ba2b0f709f4de0c427fe33c352dc
|
||||||
ReactCommon: 20e38a9be5fe1341b5e422220877cc94034776ba
|
ReactCommon: 20e38a9be5fe1341b5e422220877cc94034776ba
|
||||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||||
|
RNBackgroundFetch: 8e16176ff415daac743a6eb57afc8e9e14dbe623
|
||||||
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
|
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
|
||||||
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
||||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||||
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
||||||
RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda
|
RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda
|
||||||
RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364
|
RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364
|
||||||
|
RNNotifee: da8dcf09f079ea22f46e239d7c406e10d4525a5f
|
||||||
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
|
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
|
||||||
RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e
|
RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e
|
||||||
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"@fortawesome/react-native-fontawesome": "^0.3.0",
|
"@fortawesome/react-native-fontawesome": "^0.3.0",
|
||||||
"@gorhom/bottom-sheet": "^4",
|
"@gorhom/bottom-sheet": "^4",
|
||||||
"@mattermost/react-native-paste-input": "^0.6.0",
|
"@mattermost/react-native-paste-input": "^0.6.0",
|
||||||
|
"@notifee/react-native": "^7.4.0",
|
||||||
"@react-native-async-storage/async-storage": "^1.17.6",
|
"@react-native-async-storage/async-storage": "^1.17.6",
|
||||||
"@react-native-camera-roll/camera-roll": "^5.1.0",
|
"@react-native-camera-roll/camera-roll": "^5.1.0",
|
||||||
"@react-native-clipboard/clipboard": "^1.10.0",
|
"@react-native-clipboard/clipboard": "^1.10.0",
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-native": "0.71.0",
|
"react-native": "0.71.0",
|
||||||
"react-native-appstate-hook": "^1.0.6",
|
"react-native-appstate-hook": "^1.0.6",
|
||||||
|
"react-native-background-fetch": "^4.1.8",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-gesture-handler": "^2.5.0",
|
"react-native-gesture-handler": "^2.5.0",
|
||||||
"react-native-haptic-feedback": "^1.14.0",
|
"react-native-haptic-feedback": "^1.14.0",
|
||||||
|
|
|
@ -16,6 +16,7 @@ import * as view from './view/index'
|
||||||
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
||||||
import {MobileShell} from './view/shell/mobile'
|
import {MobileShell} from './view/shell/mobile'
|
||||||
import {s} from './view/lib/styles'
|
import {s} from './view/lib/styles'
|
||||||
|
import notifee, {EventType} from '@notifee/react-native'
|
||||||
|
|
||||||
const App = observer(() => {
|
const App = observer(() => {
|
||||||
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
||||||
|
@ -43,6 +44,13 @@ const App = observer(() => {
|
||||||
Linking.addEventListener('url', ({url}) => {
|
Linking.addEventListener('url', ({url}) => {
|
||||||
store.nav.handleLink(url)
|
store.nav.handleLink(url)
|
||||||
})
|
})
|
||||||
|
notifee.onForegroundEvent(async ({type}: {type: EventType}) => {
|
||||||
|
store.log.debug('Notifee foreground event', {type})
|
||||||
|
if (type === EventType.PRESS) {
|
||||||
|
store.log.debug('User pressed a notifee, opening notifications')
|
||||||
|
store.nav.switchTo(1, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
|
import notifee from '@notifee/react-native'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {FeedModel} from './feed-view'
|
import {FeedModel} from './feed-view'
|
||||||
import {NotificationsViewModel} from './notifications-view'
|
import {NotificationsViewModel} from './notifications-view'
|
||||||
|
@ -104,6 +105,9 @@ export class MeModel {
|
||||||
this.rootStore.log.error('Failed to setup notifications model', e)
|
this.rootStore.log.error('Failed to setup notifications model', e)
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// request notifications permission once the user has logged in
|
||||||
|
notifee.requestPermission()
|
||||||
} else {
|
} else {
|
||||||
this.clear()
|
this.clear()
|
||||||
}
|
}
|
||||||
|
@ -111,16 +115,28 @@ export class MeModel {
|
||||||
|
|
||||||
clearNotificationCount() {
|
clearNotificationCount() {
|
||||||
this.notificationCount = 0
|
this.notificationCount = 0
|
||||||
|
notifee.setBadgeCount(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchStateUpdate() {
|
async fetchNotifications() {
|
||||||
const res = await this.rootStore.api.app.bsky.notification.getCount()
|
const res = await this.rootStore.api.app.bsky.notification.getCount()
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
const newNotifications = this.notificationCount !== res.data.count
|
const newNotifications = this.notificationCount !== res.data.count
|
||||||
this.notificationCount = res.data.count
|
this.notificationCount = res.data.count
|
||||||
|
notifee.setBadgeCount(this.notificationCount)
|
||||||
if (newNotifications) {
|
if (newNotifications) {
|
||||||
// trigger pre-emptive fetch on new notifications
|
// trigger pre-emptive fetch on new notifications
|
||||||
this.notifications.refresh()
|
let oldMostRecent = this.notifications.mostRecentNotification
|
||||||
|
this.notifications.refresh().then(() => {
|
||||||
|
// if a new most recent notification is found, trigger a notification card
|
||||||
|
const mostRecent = this.notifications.mostRecentNotification
|
||||||
|
if (mostRecent && oldMostRecent?.uri !== mostRecent?.uri) {
|
||||||
|
const notifeeOpts = mostRecent.toNotifeeOpts()
|
||||||
|
if (notifeeOpts) {
|
||||||
|
notifee.displayNotification(notifeeOpts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
AppBskyFeedVote,
|
AppBskyFeedVote,
|
||||||
AppBskyGraphAssertion,
|
AppBskyGraphAssertion,
|
||||||
AppBskyGraphFollow,
|
AppBskyGraphFollow,
|
||||||
|
AppBskyEmbedImages,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {PostThreadViewModel} from './post-thread-view'
|
import {PostThreadViewModel} from './post-thread-view'
|
||||||
|
@ -179,6 +180,42 @@ export class NotificationsViewItemModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toNotifeeOpts() {
|
||||||
|
let author = this.author.displayName || this.author.handle
|
||||||
|
let title: string
|
||||||
|
let body: string = ''
|
||||||
|
if (this.isUpvote) {
|
||||||
|
title = `${author} liked your post`
|
||||||
|
body = this.additionalPost?.thread?.postRecord?.text || ''
|
||||||
|
} else if (this.isRepost) {
|
||||||
|
title = `${author} reposted your post`
|
||||||
|
body = this.additionalPost?.thread?.postRecord?.text || ''
|
||||||
|
} else if (this.isReply) {
|
||||||
|
title = `${author} replied to your post`
|
||||||
|
body = this.additionalPost?.thread?.postRecord?.text || ''
|
||||||
|
} else if (this.isFollow) {
|
||||||
|
title = `${author} followed you`
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
let ios
|
||||||
|
if (
|
||||||
|
AppBskyEmbedImages.isPresented(this.additionalPost?.thread?.post.embed) &&
|
||||||
|
this.additionalPost?.thread?.post.embed.images[0]?.thumb
|
||||||
|
) {
|
||||||
|
ios = {
|
||||||
|
attachments: [
|
||||||
|
{url: this.additionalPost.thread.post.embed.images[0].thumb},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
ios,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotificationsViewModel {
|
export class NotificationsViewModel {
|
||||||
|
@ -197,6 +234,9 @@ export class NotificationsViewModel {
|
||||||
// data
|
// data
|
||||||
notifications: NotificationsViewItemModel[] = []
|
notifications: NotificationsViewItemModel[] = []
|
||||||
|
|
||||||
|
// this is used to trigger push notifications
|
||||||
|
mostRecentNotification: NotificationsViewItemModel | undefined
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public rootStore: RootStoreModel,
|
public rootStore: RootStoreModel,
|
||||||
params: ListNotifications.QueryParams,
|
params: ListNotifications.QueryParams,
|
||||||
|
@ -388,6 +428,16 @@ export class NotificationsViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _replaceAll(res: ListNotifications.Response) {
|
private async _replaceAll(res: ListNotifications.Response) {
|
||||||
|
if (res.data.notifications[0]) {
|
||||||
|
this.mostRecentNotification = new NotificationsViewItemModel(
|
||||||
|
this.rootStore,
|
||||||
|
'mostRecent',
|
||||||
|
res.data.notifications[0],
|
||||||
|
)
|
||||||
|
await this.mostRecentNotification.fetchAdditionalData()
|
||||||
|
} else {
|
||||||
|
this.mostRecentNotification = undefined
|
||||||
|
}
|
||||||
return this._appendAll(res, true)
|
return this._appendAll(res, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {makeAutoObservable} from 'mobx'
|
||||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
||||||
import {createContext, useContext} from 'react'
|
import {createContext, useContext} from 'react'
|
||||||
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
|
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
|
||||||
|
import BackgroundFetch from 'react-native-background-fetch'
|
||||||
import {isObj, hasProp} from '../lib/type-guards'
|
import {isObj, hasProp} from '../lib/type-guards'
|
||||||
import {LogModel} from './log'
|
import {LogModel} from './log'
|
||||||
import {SessionModel} from './session'
|
import {SessionModel} from './session'
|
||||||
|
@ -34,6 +35,7 @@ export class RootStoreModel {
|
||||||
serialize: false,
|
serialize: false,
|
||||||
hydrate: false,
|
hydrate: false,
|
||||||
})
|
})
|
||||||
|
this.initBgFetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveName(didOrHandle: string) {
|
async resolveName(didOrHandle: string) {
|
||||||
|
@ -55,7 +57,7 @@ export class RootStoreModel {
|
||||||
if (!this.session.online) {
|
if (!this.session.online) {
|
||||||
await this.session.connect()
|
await this.session.connect()
|
||||||
}
|
}
|
||||||
await this.me.fetchStateUpdate()
|
await this.me.fetchNotifications()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (isNetworkError(e)) {
|
if (isNetworkError(e)) {
|
||||||
this.session.setOnline(false) // connection lost
|
this.session.setOnline(false) // connection lost
|
||||||
|
@ -109,9 +111,41 @@ export class RootStoreModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
emitPostDeleted(uri: string) {
|
emitPostDeleted(uri: string) {
|
||||||
console.log('emit')
|
|
||||||
DeviceEventEmitter.emit('post-deleted', uri)
|
DeviceEventEmitter.emit('post-deleted', uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// background fetch
|
||||||
|
// =
|
||||||
|
// - we use this to poll for unread notifications, which is not "ideal" behavior but
|
||||||
|
// gives us a solution for push-notifications that work against any pds
|
||||||
|
|
||||||
|
initBgFetch() {
|
||||||
|
// NOTE
|
||||||
|
// background fetch runs every 15 minutes *at most* and will get slowed down
|
||||||
|
// based on some heuristics run by iOS, meaning it is not a reliable form of delivery
|
||||||
|
// -prf
|
||||||
|
BackgroundFetch.configure(
|
||||||
|
{minimumFetchInterval: 15},
|
||||||
|
this.onBgFetch.bind(this),
|
||||||
|
this.onBgFetchTimeout.bind(this),
|
||||||
|
).then(status => {
|
||||||
|
this.log.debug(`Background fetch initiated, status: ${status}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBgFetch(taskId: string) {
|
||||||
|
this.log.debug(`Background fetch fired for task ${taskId}`)
|
||||||
|
if (this.session.hasSession) {
|
||||||
|
// grab notifications
|
||||||
|
await this.me.fetchNotifications()
|
||||||
|
}
|
||||||
|
BackgroundFetch.finish(taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBgFetchTimeout(taskId: string) {
|
||||||
|
this.log.debug(`Background fetch timed out for task ${taskId}`)
|
||||||
|
BackgroundFetch.finish(taskId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const throwawayInst = new RootStoreModel(AtpApi.service('http://localhost')) // this will be replaced by the loader, we just need to supply a value at init
|
const throwawayInst = new RootStoreModel(AtpApi.service('http://localhost')) // this will be replaced by the loader, we just need to supply a value at init
|
||||||
|
|
|
@ -286,17 +286,33 @@ export class SessionModel {
|
||||||
* Attempt to resume a session that we still have access tokens for.
|
* Attempt to resume a session that we still have access tokens for.
|
||||||
*/
|
*/
|
||||||
async resumeSession(account: AccountData): Promise<boolean> {
|
async resumeSession(account: AccountData): Promise<boolean> {
|
||||||
if (account.accessJwt && account.refreshJwt) {
|
if (!(account.accessJwt && account.refreshJwt && account.service)) {
|
||||||
this.setState({
|
|
||||||
service: account.service,
|
|
||||||
accessJwt: account.accessJwt,
|
|
||||||
refreshJwt: account.refreshJwt,
|
|
||||||
handle: account.handle,
|
|
||||||
did: account.did,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test that the session is good
|
||||||
|
const api = AtpApi.service(account.service)
|
||||||
|
api.sessionManager.set({
|
||||||
|
refreshJwt: account.refreshJwt,
|
||||||
|
accessJwt: account.accessJwt,
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const sess = await api.com.atproto.session.get()
|
||||||
|
if (!sess.success || sess.data.did !== account.did) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// session is good, connect
|
||||||
|
this.setState({
|
||||||
|
service: account.service,
|
||||||
|
accessJwt: account.accessJwt,
|
||||||
|
refreshJwt: account.refreshJwt,
|
||||||
|
handle: account.handle,
|
||||||
|
did: account.did,
|
||||||
|
})
|
||||||
return this.connect()
|
return this.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,14 +361,14 @@ export class SessionModel {
|
||||||
* Close all sessions across all accounts.
|
* Close all sessions across all accounts.
|
||||||
*/
|
*/
|
||||||
async logout() {
|
async logout() {
|
||||||
if (this.hasSession) {
|
/*if (this.hasSession) {
|
||||||
this.rootStore.api.com.atproto.session.delete().catch((e: any) => {
|
this.rootStore.api.com.atproto.session.delete().catch((e: any) => {
|
||||||
this.rootStore.log.warn(
|
this.rootStore.log.warn(
|
||||||
'(Minor issue) Failed to delete session on the server',
|
'(Minor issue) Failed to delete session on the server',
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}*/
|
||||||
this.clearSessionTokensFromAccounts()
|
this.clearSessionTokensFromAccounts()
|
||||||
this.rootStore.clearAll()
|
this.rootStore.clearAll()
|
||||||
}
|
}
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -2109,6 +2109,11 @@
|
||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@notifee/react-native@^7.4.0":
|
||||||
|
version "7.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.4.0.tgz#0f20744307bf3b800f7b56eb2d0bbdd474748d09"
|
||||||
|
integrity sha512-c8pkxDQFRbw0JlUmTb07OTG/4LQHRj8MBodMLwEcO+SvqIxK8ya8zSUEzfdcdWsSVqdoym0v3zpSNroR3Quj/w==
|
||||||
|
|
||||||
"@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
|
"@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
|
||||||
version "0.5.10"
|
version "0.5.10"
|
||||||
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz#2eba163b8e7dbabb4ce3609ab5e32ab63dda3ef8"
|
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz#2eba163b8e7dbabb4ce3609ab5e32ab63dda3ef8"
|
||||||
|
@ -11164,6 +11169,11 @@ react-native-appstate-hook@^1.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/react-native-appstate-hook/-/react-native-appstate-hook-1.0.6.tgz#cbc16e7b89cfaea034cabd999f00e99053cabd06"
|
resolved "https://registry.yarnpkg.com/react-native-appstate-hook/-/react-native-appstate-hook-1.0.6.tgz#cbc16e7b89cfaea034cabd999f00e99053cabd06"
|
||||||
integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ==
|
integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ==
|
||||||
|
|
||||||
|
react-native-background-fetch@^4.1.8:
|
||||||
|
version "4.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-background-fetch/-/react-native-background-fetch-4.1.8.tgz#a21858e5d876de8d9d15a37f40714b244f73713c"
|
||||||
|
integrity sha512-/qe86laa0n4AbD6mrLL8SCGR+K5693URX95e02/bTJh3UkdS3+sU1Jyc/XTlz4MQwlquI929/lm5EZh8AOUqzQ==
|
||||||
|
|
||||||
react-native-codegen@^0.71.3:
|
react-native-codegen@^0.71.3:
|
||||||
version "0.71.3"
|
version "0.71.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.71.3.tgz#75fbc591819050791319ebdb9fe341ee4df5c288"
|
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.71.3.tgz#75fbc591819050791319ebdb9fe341ee4df5c288"
|
||||||
|
|
Loading…
Reference in New Issue