@@ -14,27 +14,41 @@ See the License for the specific language governing permissions and
1414limitations under the License.
1515*/
1616
17+ import { debounce } from "lodash" ;
1718import {
1819 Beacon ,
1920 BeaconEvent ,
2021 MatrixEvent ,
2122 Room ,
2223} from "matrix-js-sdk/src/matrix" ;
2324import {
24- BeaconInfoState , makeBeaconInfoContent ,
25+ BeaconInfoState , makeBeaconContent , makeBeaconInfoContent ,
2526} from "matrix-js-sdk/src/content-helpers" ;
27+ import { M_BEACON } from "matrix-js-sdk/src/@types/beacon" ;
28+ import { logger } from "matrix-js-sdk/src/logger" ;
2629
2730import defaultDispatcher from "../dispatcher/dispatcher" ;
2831import { ActionPayload } from "../dispatcher/payloads" ;
2932import { AsyncStoreWithClient } from "./AsyncStoreWithClient" ;
30- import { arrayHasDiff } from "../utils/arrays" ;
33+ import { arrayDiff } from "../utils/arrays" ;
34+ import {
35+ ClearWatchCallback ,
36+ GeolocationError ,
37+ mapGeolocationPositionToTimedGeo ,
38+ TimedGeoUri ,
39+ watchPosition ,
40+ } from "../utils/beacon" ;
41+ import { getCurrentPosition } from "../utils/beacon/geolocation" ;
3142
3243const isOwnBeacon = ( beacon : Beacon , userId : string ) : boolean => beacon . beaconInfoOwner === userId ;
3344
3445export enum OwnBeaconStoreEvent {
3546 LivenessChange = 'OwnBeaconStore.LivenessChange' ,
3647}
3748
49+ const MOVING_UPDATE_INTERVAL = 2000 ;
50+ const STATIC_UPDATE_INTERVAL = 30000 ;
51+
3852type OwnBeaconStoreState = {
3953 beacons : Map < string , Beacon > ;
4054 beaconsByRoomId : Map < Room [ 'roomId' ] , Set < string > > ;
@@ -46,6 +60,15 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
4660 public readonly beacons = new Map < string , Beacon > ( ) ;
4761 public readonly beaconsByRoomId = new Map < Room [ 'roomId' ] , Set < string > > ( ) ;
4862 private liveBeaconIds = [ ] ;
63+ private locationInterval : number ;
64+ private geolocationError : GeolocationError | undefined ;
65+ private clearPositionWatch : ClearWatchCallback | undefined ;
66+ /**
67+ * Track when the last position was published
68+ * So we can manually get position on slow interval
69+ * when the target is stationary
70+ */
71+ private lastPublishedPositionTimestamp : number | undefined ;
4972
5073 public constructor ( ) {
5174 super ( defaultDispatcher ) ;
@@ -55,12 +78,21 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
5578 return OwnBeaconStore . internalInstance ;
5679 }
5780
81+ /**
82+ * True when we have live beacons
83+ * and geolocation.watchPosition is active
84+ */
85+ public get isMonitoringLiveLocation ( ) : boolean {
86+ return ! ! this . clearPositionWatch ;
87+ }
88+
5889 protected async onNotReady ( ) {
5990 this . matrixClient . removeListener ( BeaconEvent . LivenessChange , this . onBeaconLiveness ) ;
6091 this . matrixClient . removeListener ( BeaconEvent . New , this . onNewBeacon ) ;
6192
6293 this . beacons . forEach ( beacon => beacon . destroy ( ) ) ;
6394
95+ this . stopPollingLocation ( ) ;
6496 this . beacons . clear ( ) ;
6597 this . beaconsByRoomId . clear ( ) ;
6698 this . liveBeaconIds = [ ] ;
@@ -117,21 +149,12 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
117149 return ;
118150 }
119151
120- if ( ! isLive && this . liveBeaconIds . includes ( beacon . identifier ) ) {
121- this . liveBeaconIds =
122- this . liveBeaconIds . filter ( beaconId => beaconId !== beacon . identifier ) ;
123- }
124-
125- if ( isLive && ! this . liveBeaconIds . includes ( beacon . identifier ) ) {
126- this . liveBeaconIds . push ( beacon . identifier ) ;
127- }
128-
129152 // beacon expired, update beacon to un-alive state
130153 if ( ! isLive ) {
131154 this . stopBeacon ( beacon . identifier ) ;
132155 }
133156
134- // TODO start location polling here
157+ this . checkLiveness ( ) ;
135158
136159 this . emit ( OwnBeaconStoreEvent . LivenessChange , this . getLiveBeaconIds ( ) ) ;
137160 } ;
@@ -169,9 +192,29 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
169192 . filter ( beacon => beacon . isLive )
170193 . map ( beacon => beacon . identifier ) ;
171194
172- if ( arrayHasDiff ( prevLiveBeaconIds , this . liveBeaconIds ) ) {
195+ const diff = arrayDiff ( prevLiveBeaconIds , this . liveBeaconIds ) ;
196+
197+ if ( diff . added . length || diff . removed . length ) {
173198 this . emit ( OwnBeaconStoreEvent . LivenessChange , this . liveBeaconIds ) ;
174199 }
200+
201+ // publish current location immediately
202+ // when there are new live beacons
203+ // and we already have a live monitor
204+ // so first position is published quickly
205+ // even when target is stationary
206+ //
207+ // when there is no existing live monitor
208+ // it will be created below by togglePollingLocation
209+ // and publish first position quickly
210+ if ( diff . added . length && this . isMonitoringLiveLocation ) {
211+ this . publishCurrentLocationToBeacons ( ) ;
212+ }
213+
214+ // if overall liveness changed
215+ if ( ! ! prevLiveBeaconIds ?. length !== ! ! this . liveBeaconIds . length ) {
216+ this . togglePollingLocation ( ) ;
217+ }
175218 } ;
176219
177220 private updateBeaconEvent = async ( beacon : Beacon , update : Partial < BeaconInfoState > ) : Promise < void > => {
@@ -188,4 +231,90 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
188231
189232 await this . matrixClient . unstable_setLiveBeacon ( beacon . roomId , beacon . beaconInfoEventType , updateContent ) ;
190233 } ;
234+
235+ private togglePollingLocation = async ( ) : Promise < void > => {
236+ if ( ! ! this . liveBeaconIds . length ) {
237+ return this . startPollingLocation ( ) ;
238+ }
239+ return this . stopPollingLocation ( ) ;
240+ } ;
241+
242+ private startPollingLocation = async ( ) => {
243+ // clear any existing interval
244+ this . stopPollingLocation ( ) ;
245+
246+ this . clearPositionWatch = await watchPosition ( this . onWatchedPosition , this . onWatchedPositionError ) ;
247+
248+ this . locationInterval = setInterval ( ( ) => {
249+ if ( ! this . lastPublishedPositionTimestamp ) {
250+ return ;
251+ }
252+ // if position was last updated STATIC_UPDATE_INTERVAL ms ago or more
253+ // get our position and publish it
254+ if ( this . lastPublishedPositionTimestamp <= Date . now ( ) - STATIC_UPDATE_INTERVAL ) {
255+ this . publishCurrentLocationToBeacons ( ) ;
256+ }
257+ } , STATIC_UPDATE_INTERVAL ) ;
258+ } ;
259+
260+ private onWatchedPosition = ( position : GeolocationPosition ) => {
261+ const timedGeoPosition = mapGeolocationPositionToTimedGeo ( position ) ;
262+
263+ // if this is our first position, publish immediateley
264+ if ( ! this . lastPublishedPositionTimestamp ) {
265+ this . publishLocationToBeacons ( timedGeoPosition ) ;
266+ } else {
267+ this . debouncedPublishLocationToBeacons ( timedGeoPosition ) ;
268+ }
269+ } ;
270+
271+ private onWatchedPositionError = ( error : GeolocationError ) => {
272+ this . geolocationError = error ;
273+ logger . error ( this . geolocationError ) ;
274+ } ;
275+
276+ private stopPollingLocation = ( ) => {
277+ clearInterval ( this . locationInterval ) ;
278+ this . locationInterval = undefined ;
279+ this . lastPublishedPositionTimestamp = undefined ;
280+ this . geolocationError = undefined ;
281+
282+ if ( this . clearPositionWatch ) {
283+ this . clearPositionWatch ( ) ;
284+ this . clearPositionWatch = undefined ;
285+ }
286+ } ;
287+
288+ /**
289+ * Sends m.location events to all live beacons
290+ * Sets last published beacon
291+ */
292+ private publishLocationToBeacons = async ( position : TimedGeoUri ) => {
293+ this . lastPublishedPositionTimestamp = Date . now ( ) ;
294+ // TODO handle failure in individual beacon without rejecting rest
295+ await Promise . all ( this . liveBeaconIds . map ( beaconId =>
296+ this . sendLocationToBeacon ( this . beacons . get ( beaconId ) , position ) ) ,
297+ ) ;
298+ } ;
299+
300+ private debouncedPublishLocationToBeacons = debounce ( this . publishLocationToBeacons , MOVING_UPDATE_INTERVAL ) ;
301+
302+ /**
303+ * Sends m.location event to referencing given beacon
304+ */
305+ private sendLocationToBeacon = async ( beacon : Beacon , { geoUri, timestamp } : TimedGeoUri ) => {
306+ const content = makeBeaconContent ( geoUri , timestamp , beacon . beaconInfoId ) ;
307+ await this . matrixClient . sendEvent ( beacon . roomId , M_BEACON . name , content ) ;
308+ } ;
309+
310+ /**
311+ * Gets the current location
312+ * (as opposed to using watched location)
313+ * and publishes it to all live beacons
314+ */
315+ private publishCurrentLocationToBeacons = async ( ) => {
316+ const position = await getCurrentPosition ( ) ;
317+ // TODO error handling
318+ this . publishLocationToBeacons ( mapGeolocationPositionToTimedGeo ( position ) ) ;
319+ } ;
191320}
0 commit comments