diff --git a/packages/mgt-chat/src/components/Chat/Chat.tsx b/packages/mgt-chat/src/components/Chat/Chat.tsx index f6918853ab..b9067b8c29 100644 --- a/packages/mgt-chat/src/components/Chat/Chat.tsx +++ b/packages/mgt-chat/src/components/Chat/Chat.tsx @@ -134,8 +134,8 @@ export const Chat = ({ chatId }: IMgtChatProps) => { ); }} diff --git a/packages/mgt-components/src/components/mgt-person/mgt-person.tests.ts b/packages/mgt-components/src/components/mgt-person/mgt-person.tests.ts index 35d807338e..1149fa539e 100644 --- a/packages/mgt-components/src/components/mgt-person/mgt-person.tests.ts +++ b/packages/mgt-components/src/components/mgt-person/mgt-person.tests.ts @@ -7,11 +7,13 @@ import { fixture, html, expect, waitUntil } from '@open-wc/testing'; import { MockProvider, Providers } from '@microsoft/mgt-element'; import { registerMgtPersonComponent } from './mgt-person'; +import { useConfig } from '../../graph/graph.presence.mock'; +import { PresenceService } from '../../utils/PresenceService'; describe('mgt-person - tests', () => { before(() => { registerMgtPersonComponent(); - Providers.globalProvider = new MockProvider(true); + Providers.globalProvider = new MockProvider(true, [{ id: '48d31887-5fad-4d73-a9f5-3c356e68a038' }]); }); it('should render', async () => { @@ -31,6 +33,19 @@ describe('mgt-person - tests', () => { ); }); + it('unknown user should render with a default icon', async () => { + // purposely throws browser error "Error: Invalid userId" + const person = await fixture( + html`` + ); + await waitUntil(() => person.shadowRoot.querySelector('svg'), 'no svg was populated'); + await expect(person).shadowDom.to.equal( + ` + + ` + ); + }); + it('should pop up a flyout on click', async () => { const person = await fixture(html``); await waitUntil(() => person.shadowRoot.querySelector('img'), 'mgt-person did not update'); @@ -176,4 +191,97 @@ describe('mgt-person - tests', () => { })}' view="twoLines">`); await expect(person.shadowRoot.querySelector('span.initials')).lightDom.to.equal('FV'); }); + + it('should support a change in presence', async () => { + useConfig({ + default: 'Available', + 3: 'Busy', + 4: 'Busy', + 5: 'Busy' + }); + + PresenceService.config.initial = 100; + PresenceService.config.refresh = 200; + + const person = await fixture( + html`` + ); + + const match = (status: string) => `
+
+ Photo for Megan Bowen + + +
+
+ + +
+
`; + + // starts as Offline + await waitUntil(() => person.shadowRoot.querySelector('span.presence-wrapper'), 'no presence span'); + await expect(person).shadowDom.to.equal(match('Offline'), { ignoreAttributes: ['src'] }); + + // changes to Available on initial + await waitUntil(() => person.shadowRoot.querySelector('span[title="Available"]'), 'did not update to available', { + timeout: 4000 + }); + await expect(person).shadowDom.to.equal(match('Available'), { ignoreAttributes: ['src'] }); + + // changes to Busy on refresh + await waitUntil(() => person.shadowRoot.querySelector('span[title="Busy"]'), 'did not update to busy', { + timeout: 4000 + }); + await expect(person).shadowDom.to.equal(match('Busy'), { ignoreAttributes: ['src'] }); + }); + + it('should not update presence on error', async () => { + useConfig({ + default: 'Available', + 0: new Error('purposeful error'), + 1: new Error('purposeful error'), + 2: new Error('purposeful error') + }); + + PresenceService.config.initial = 1000; + PresenceService.config.refresh = 2000; + + const person = await fixture( + html`` + ); + await waitUntil(() => person.shadowRoot.querySelector('img'), 'mgt-person did not update'); + + const match = (status: string) => `
+
+ Photo for Megan Bowen + + +
+
+ + +
+
`; + + // starts Offline during errors + await waitUntil(() => person.shadowRoot.querySelector('span.presence-wrapper'), 'no presence span'); + await expect(person).shadowDom.to.equal(match('Offline'), { ignoreAttributes: ['src'] }); + + // changes to Available eventually + await waitUntil(() => person.shadowRoot.querySelector('span[title="Available"]'), 'did not update to available', { + timeout: 4000 + }); + await expect(person).shadowDom.to.equal(match('Available'), { ignoreAttributes: ['src'] }); + }); }); diff --git a/packages/mgt-components/src/components/mgt-person/mgt-person.ts b/packages/mgt-components/src/components/mgt-person/mgt-person.ts index 5f87c166c8..d5c2f55f65 100644 --- a/packages/mgt-components/src/components/mgt-person/mgt-person.ts +++ b/packages/mgt-components/src/components/mgt-person/mgt-person.ts @@ -28,6 +28,7 @@ import { isUser, isContact } from '../../graph/entityType'; import { ifDefined } from 'lit/directives/if-defined.js'; import { buildComponentName, registerComponent } from '@microsoft/mgt-element'; import { IExpandable, IHistoryClearer } from '../mgt-person-card/types'; +import { PresenceService, PresenceAwareComponent } from '../../utils/PresenceService'; export { PersonCardInteraction } from '../PersonCardInteraction'; @@ -107,7 +108,7 @@ export const registerMgtPersonComponent = () => { * * @cssprop --person-details-wrapper-width - {Length} the minimum width of the details section. Default is 168px. */ -export class MgtPerson extends MgtTemplatedComponent { +export class MgtPerson extends MgtTemplatedComponent implements PresenceAwareComponent { /** * Array of styles to apply to the element. The styles should be defined * using the `css` tag function. @@ -240,6 +241,17 @@ export class MgtPerson extends MgtTemplatedComponent { }) public showPresence: boolean; + /** + * determines if person component refreshes presence + * + * @type {boolean} + */ + @property({ + attribute: 'disable-presence-refresh', + type: Boolean + }) + public disablePresenceRefresh; + /** * determines person component avatar size and apply presence badge accordingly. * Default is "auto". When you set the view > 1, it will default to "auto". @@ -563,6 +575,19 @@ export class MgtPerson extends MgtTemplatedComponent { this.verticalLayout = false; } + /** + * Unregisters from presence service if necessary. Note, it does not cause an error + * if the component was not registered. + * + * @memberof MgtAgenda + */ + public disconnectedCallback() { + if (this.showPresence && !this.disablePresenceRefresh) { + PresenceService.unregister(this); + } + super.disconnectedCallback(); + } + /** * Invoked on each update to perform rendering tasks. This method must return * a lit-html TemplateResult. Setting properties inside this method will *not* @@ -639,7 +664,22 @@ export class MgtPerson extends MgtTemplatedComponent { * @memberof MgtPerson */ protected renderLoading(): TemplateResult { - return this.renderTemplate('loading', null) || html``; + const loadingTemplate = this.renderTemplate('loading', null); + if (loadingTemplate) { + return loadingTemplate; + } + + const avatarClasses = { + 'avatar-icon': true, + vertical: this.isVertical(), + small: !this.isLargeAvatar(), + threeLines: this.isThreeLines(), + fourLines: this.isFourLines() + }; + + return html` + ${this.renderLoadingIcon()} + `; } /** @@ -677,8 +717,19 @@ export class MgtPerson extends MgtTemplatedComponent { }; return html` - - `; + ${this.renderPersonIcon()} + `; + } + + /** + * Render a loading icon. + * + * @protected + * @returns + * @memberof MgtPerson + */ + protected renderLoadingIcon() { + return getSvg(SvgIcon.Loading); } /** @@ -1167,14 +1218,16 @@ export class MgtPerson extends MgtTemplatedComponent { details = this.personDetailsInternal || this.personDetails || this.fallbackDetails; - // populate presence const defaultPresence: Presence = { activity: 'Offline', availability: 'Offline', id: null }; - if (this.showPresence && !this.personPresence && !this._fetchedPresence) { + if (this.showPresence && !this.disablePresenceRefresh) { + this._fetchedPresence = defaultPresence; + PresenceService.register(this); + } else if (this.showPresence && !this.personPresence && !this._fetchedPresence) { try { if (details) { // setting userId to 'me' ensures only the presence.read permission is required @@ -1375,4 +1428,26 @@ export class MgtPerson extends MgtTemplatedComponent { flyout.open(); } }; + + /** + * gets the id of the person that presence updates are needed for + * + * @memberof MgtPerson + * @implements {PresenceAwareComponent} + * @returns {string | undefined} + **/ + public get presenceId(): string | undefined { + return this.personDetailsInternal?.id || this.personDetails?.id || this.fallbackDetails?.id; + } + + /** + * fires when the presence for the user is changed + * + * @memberof MgtPerson + * @implements {PresenceAwareComponent} + * @param {Presence} [presence] + **/ + public onPresenceChange(presence: Presence): void { + this.personPresence = presence; + } } diff --git a/packages/mgt-components/src/exports.ts b/packages/mgt-components/src/exports.ts index fe9e325d0c..3b96f13624 100644 --- a/packages/mgt-components/src/exports.ts +++ b/packages/mgt-components/src/exports.ts @@ -10,6 +10,7 @@ export * from './components/preview'; export * from './graph/types'; export * from './graph/graph.userWithPhoto'; export * from './graph/graph.photos'; +export * from './graph/graph.presence'; export * from './graph/cacheStores'; export * from './styles/theme-manager'; export * from './graph/entityType'; diff --git a/packages/mgt-components/src/graph/graph.presence.mock.ts b/packages/mgt-components/src/graph/graph.presence.mock.ts new file mode 100644 index 0000000000..a753fe97a7 --- /dev/null +++ b/packages/mgt-components/src/graph/graph.presence.mock.ts @@ -0,0 +1,55 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +import { IGraph } from '@microsoft/mgt-element'; +import { Presence } from '@microsoft/microsoft-graph-types'; +import { IDynamicPerson } from './types'; + +const globalObject = typeof window !== 'undefined' ? window : global; + +type status = 'Available' | 'Busy' | 'Away' | 'DoNotDisturb' | Error; + +type CallConfig = { + default: status; + [key: string]: status; +} & { + calls?: number; +}; + +export const useConfig = (config: CallConfig | status = 'Available') => { + if (typeof config === 'object') { + globalObject['unit-test:presence-config'] = config; + } else if (typeof config === 'string') { + globalObject['unit-test:presence-config'] = { default: config as status }; + } else { + throw new Error('Invalid config'); + } +}; + +export const getUserPresence = async (_graph: IGraph, _userId?: string): Promise => { + return Promise.reject(new Error()); +}; + +export const getUsersPresenceByPeople = async (graph: IGraph, people?: IDynamicPerson[], _ = true) => { + const config = (globalObject['unit-test:presence-config'] as CallConfig) || { default: 'Available' }; + const index = config.calls || 0; + const value = config[index] || config.default; + config.calls = index + 1; + if (value instanceof Error) { + return Promise.reject(value); + } else { + const peoplePresence: Record = {}; + for (const person of people || []) { + peoplePresence[person.id] = { + id: person.id, + availability: value, + activity: value + }; + } + return Promise.resolve(peoplePresence); + } +}; diff --git a/packages/mgt-components/src/graph/graph.presence.ts b/packages/mgt-components/src/graph/graph.presence.ts index 0feb78aa07..c428f6f07c 100644 --- a/packages/mgt-components/src/graph/graph.presence.ts +++ b/packages/mgt-components/src/graph/graph.presence.ts @@ -66,12 +66,13 @@ export const getUserPresence = async (graph: IGraph, userId?: string): Promise

{ +export const getUsersPresenceByPeople = async (graph: IGraph, people?: IDynamicPerson[], bypassCacheRead = false) => { if (!people || people.length === 0) { return {}; } @@ -90,10 +91,15 @@ export const getUsersPresenceByPeople = async (graph: IGraph, people?: IDynamicP const id = person.id; peoplePresence[id] = null; let presence: CachePresence; - if (getIsPresenceCacheEnabled()) { + if (!bypassCacheRead && getIsPresenceCacheEnabled()) { presence = await cache.getValue(id); } - if (getIsPresenceCacheEnabled() && presence && getPresenceInvalidationTime() > Date.now() - presence.timeCached) { + if ( + !bypassCacheRead && + getIsPresenceCacheEnabled() && + presence && + getPresenceInvalidationTime() > Date.now() - presence.timeCached + ) { peoplePresence[id] = JSON.parse(presence.presence) as Presence; } else { peoplePresenceToQuery.push(id); diff --git a/packages/mgt-components/src/graph/graph.userWithPhoto.mock.ts b/packages/mgt-components/src/graph/graph.userWithPhoto.mock.ts index 5c3f3d6127..f552989a62 100644 --- a/packages/mgt-components/src/graph/graph.userWithPhoto.mock.ts +++ b/packages/mgt-components/src/graph/graph.userWithPhoto.mock.ts @@ -21,23 +21,30 @@ export const getUserWithPhoto = async ( _userId?: string, _requestedProps?: string[] ): Promise => { - return Promise.resolve({ - '@odata.context': - 'https://graph.microsoft.com/v1.0/$metadata#users(businessPhones,displayName,givenName,jobTitle,department,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,userType)/$entity', - businessPhones: ['+1 412 555 0109'], - displayName: 'Megan Bowen', - givenName: 'Megan', - jobTitle: 'Auditor', - department: 'Finance', - mail: 'MeganB@M365x214355.onmicrosoft.com', - mobilePhone: null, - officeLocation: '12/1110', - preferredLanguage: 'en-US', - surname: 'Bowen', - userPrincipalName: 'MeganB@M365x214355.onmicrosoft.com', - id: '48d31887-5fad-4d73-a9f5-3c356e68a038', - userType: 'Member', - personImage: - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCADwAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDeMsdrZPO+AqJk8e1eU3l7K2sXE8ibftHKrjp6V33iG4KabHADjzGG73Arz67dpJXuXwWLk4/lXmYe3Lc9XEt3sUL2XCpADkL1+tUozjfIfw+g/wDr0XDsZGJPOfzpsrgW20YHIWuuKOOTFtd0skj91HB9zT7lhLIlsh+QkBvZRyfzp1iypYNIcfMSR/Sq0LZaaTrzt/M1RIXs5ddwGATtX6CrNkuwxDGPm/pVYxGRYlzwpJP41ehUjHbPT8qUmVFa3NazC7EJXOeuR3qrrmftBhA+YMqD8a1LKP8AdRk4+U9PpWbqpzrUYIJUupA/KsI/EbyXumjJ8tlJ1GyLpWfAd2owIANkaOwHv90H8gfzrSuExZXO7nEYwPQVmWJAvdzKOIAAfrmlF6NlSWqRDqrbg6nuOaqadGbjUo1AyAOlWNWOJwB0PWpdGAgluJGHzJEx/lVx+EzeszUtz9o1JinCx4RPc+v86miU6pfrCgHlI21eOvq1V4ZDb6eNi4ll+VSPU9T+tX4LiPTdPadMByNkdZM6F2NG7dXuIrGHHlRj5iO57mk1C+hs4tz4JxiNBWRYXo3Oqgy3J5PoPTPsKdP5QnbOLm66sW+6p/r9KSj3HfsQ+Y0m67vCctwiKOp9B7VHawtLciWcbR1xjgCtK2s2I+0XA3TdRnoPf/61Q6hMYwiRcSM24jHNUtdETayuzNEgjvGVRuYE7eO1XI5ndgXwXKnjFV4IPs8zMxzIfvE9TntQQynGMnpx0FXdmdktWasMzMm1AoxyeOAT0HvW8skNtZrbqAZFwSexbufr/WuXsUKqskjFI4/mZs9P/r1u6Y6yRG8KBY0BdFP6E1tCRlOOpJJaBJAj4AZCWwOc4JOK5K4g8mKaTG1TCTGPZSvP6muiku2ecMzZyxxn9az54ftNtqHyZVLJ9o9DuH9BVaNaE2aZFaoUtlXsuVPtg8foRUIV4ZmABwH3Aex61qWEDXNu5C/ejSQfiuD+opLqFfscs74XA6jtjmseXUty0O68NXIutPxxuUA1sSoe2OnpXEeCL8GOLLc42EV3q4IOfSsmro87EU7T0PMPFFwfNt1DYXk/kK4y8nLARrwBxXR+MZ9rRr3UZFcfLIGcHPWowqvTR7OKfvsrSc3C+vU1HP8A6tFzz1P41akCfao2ONpXk+lU2VsMvUg/0rticEiSC4BtjEAcqmDnuc5/pTLYt5TnuGBqIFVKlW/1jFT7D/Jq/BCBGwY48xePqOtN6AtSWOIFQo67sVZnHlR55+VcjHeotPHmSMpYbs9D3rTurfOwr1Bxg9xisJP3jojH3S7oh860dWB3K/8AOqfiFRDeW8+PuSbG98Y5pPD8rPJJCWA4wMn72PWtDV7F7u1ulG4yIiyKpHOR1Hvkf0qVpIt6wLW1ZUyfuyoUP5VzURaC4VW6ou1vfB/wrd0eQ3OkLk7pF79eRWLrC/Z71bnHEnOB69CPyqYrVoqWykR6sokkVvU02KXy2lJ58xCMfUioLqUYQ7sgOuB7VWkmEdxAN2FYDBJ966FH3TncrSN2e7/e7EPywrtUAfxHjP61Ulml1C6jtYSSsQwSOi//AF6rO8m7BGxXldjzyQK3tC065exiht0CS3ZaV5SPuJ2rFxsrm8XzOxFDEFmaKPdsXqu7l29WPYD/AOtWxa20VvGWYglsdsBR6AfzNTtbWulQHgMFGSWH3/8AJrnr25utQu/stusjvJgMiffbPb/ZH9Kz1k7I2soq5NqWtPLN9k08GRn4LAdD6D/GtKytTaWRkuSZLl+DzwvsKv6P4ah0iyN3clWlx97suPT/ABqte3UccUtzL8kaD5QO3v8AWh6e6gSv7zMC4u/LnZesxJ6dvp79qv2ds83lqxGSPm44QHHf15rJ09Jb7WGvAoRZGIAxxGOw+uK6iR0sLdIkZQ6s0ajoSCOvvz39hWq00Md9Sjqdwknl2Fqn7pCGcjv6Z989q1oE2aY2FxGMAZ6HkfzpE01YrGEeWTJKcknvnj+Wa0dUiWHSkEfLMy59x3/QVLZcV1MZQFmDt/8ArNV/tQt7HUpSoLSR7BxwBkf5/CtGZUFvgnlSOPwFc5fylLWQdywLD8Olbxdkc81c6bRSog0nDZE1ookGeCQMj+dLfoJtJv49vCEnp1DcYrJ027ZEtE5AhBUHHYIBWkZ/tEVxCWUm4jJ/Ecj6VorNGTuZvhC5JjiYNgDg/hXrNu/mwI47r1rxvwwyvGylfL2FhtHTNeu+H3E+nKR1UfpXM1Zsiv8ACmeM+JLv7ZeNtPyqMVy80mDg9RitN3Z1kdup5rHvW+Xd6rToQ5VynTiKnNJyLM7D7KJD3UKT+NV1mAdQ2Oyk+lNtJ1m02WFuSADz9aoyFkbbg5BwfeuhI5WyzIrRXYDKSgbPHXrXQ+XvWMJyQd6nHXNZMMJuYi0jCNlHzF+APqa1NJmiuHWGOZGEYyxHYd/wqZ6q6NIaMlWOFCGzsk6Er2PtV2PUB5ZR2Q8g5PHP0rY+y6HZeWbi+tvMlOFCuGz+VW7jw/YylGa4jVn4QBh81YNdzdPschE5ttQ81ABn+JRjae34V0FtrCB2eRwmSCCw44/yar3nhKaTMlvPE4BI49fQ4rHl0G8tw2RwfQ8Ghq4JyRpmSPTbw3Nmwe2mPzRg/dNLqbQ31mChBGcjjkVgm3nhGGLhQO3OK0LUjb5bncp4DKehpuPUFLozn7nMT+U4KsPfOfcVVlLvEueGjYj8/wDP611F9o73NuGGC6/dasb+zLmWGUmL95CBvAzlx6/l/KtYTVtTGcHfQn011v8AULMPuIBO4eh7fqBXqmhWsceiRXACgvGqZA54Hf8AX8q8btGmtpxIMqysCf6H8a9O0XXjdWMNqCCV6YAOaiojak7li/0+S6kfa33Thcjt9PWr2i6FDay7kQGSQ/vZD94Dvz6mtCKJSu9ly54JIq4l0kcbRlRvyOG6MKxjZHRNtlbWPLe2aNFzt6jt9K4bXbSS9ubPTomKJjzJGHUn3/U49q6zUdetopDHsMjdMD1rnm1i2jvriQxsvmlWXGC2RmrS1uRze7YctlDa6ha2EDbFJYO3fgA8+5rL1CZr3VWljG2KNiiKh+7GuRu/PJpbvVFwswlKTb9xzx1BH8sVYsPIiskT5DM0G7OMcFvf/ZHT3osxXT2Ops4vPDTKhVAY448noMg//r+tS6zFsghXbgbj/wCgnik0G4VNJjhBXerK6jr3HINal7B9pss8FkUMAPr/APrpNDUjj9RPlSCNiMEhjxnIAHSuavXVpVVR8uSW/AV0Ws5+8QQV8sHjpkHn9K466uG8tmGCxZu3Xp/hVRZnNG3BKiyx/wAIdm2jPtmnC9Ed5GCBle2OuQawmvC8toFwF38/lRJfN9sfJz+8C/TpWtzE1PChUzTqSNxc7Sa9X8GuWSWFyCecYrybwywGoSKmADJxn0zXpPhK526zLD0IOfqKma0M6ivE8ddQLZiTyw4rn7xmEJB7cVtXrgyLGpwFrBv5PNkIXnnFXSWlwqMr2s5hlySdpG1h7GrTzyQGNUKRuCWWfncR2+lRS6bNbD/SR5OPvK/XPpirWliO91SCG4gBhJ4xxgDt71q2lqZxTbsWEttS163ZlWQyKoOCcLL2/OtLSvBOutdxO9qLeNVO9jIp3D0xnv0rvLS0RbgzlQsaKFRR7VZkuwkTOCfzrldfsdao6ps80n8F60Gkxa26hmyMTDK+w5p934evltbPIQmMESxLMAc/3sk962tZ1pomZUcbj71hW6ahqtxtiyzE8FvSqjKctRTjCF0dJoVzfJpkcBuLWzkiAVg8ykTdg3GcHAA96vyzamq5eKCdf70Tg/1qrZ+Cry4iJlvWHGCFGM1R1exuNBVG+2u8h+7Fgc+9TKi5S5rlQxEYrlsXWvYgCLi3eJj3YYBpFNoTlQozWNYeIPMYpe7kyMBlbGD/ACP0rVNrbSbfNICy/wCruIvlDexHY0nCUUXGpGRqRvGV2rjHatW10yBpBKqZkI6+3pXH3Fre6c/mRuLiIc8cNiug8O6/ZT4inmEUg6B/lrN3RqrWLmp+E7O/BxGYnHAdOP0pNP8ABk9jdL5siSop+VgmGx7111sqPtcEMvtz+tav2ZfLORkdeuCPxpxldEuyZgBPJTDZAHqc1WvDuj3c7V5wOpP+FX9Qa2iVjJMqZ5+Yiubn1mzjbCTGRh2jUms+azNlFyWhl3dlIWYq22Q5PHYelZfk+SrNFCZJOjO/OD/jWvLrB+bZp9w+e+zGaqNrd2AFj0ebA7Y/+tWiqEOizAlsZLiZF2EjnIVK1bHTL+e4UJbsQkYGRjge9Tf27fCXedGfpjHNQah48vtP+zrbWccEzv8AMknO9fT2FXGV3YynT5VzM6S30C/igdtz+YMH5W7VrabBqNqpkjkM4xhon7L/AJ/lXLWfxUK6Y13dWAkmWQrIsT7Qn93/AHga0NH+I9heBBqMMtpcO2U8hCVK+uc1bjYy50xuvBvs5HUYCvz6HKn6dq4G9OYnC9mJI/HtXa3PibRtdna0iSWC8MpjRpHHlsp7k9vpXDakDaXFxb3DBXidkbHYg/yoUXuEprYijbzJYJMn5WOR9RUbyZupTnPzg06KFjEF81flGQcn5hzxUIRWm3I2/wAxsYXtV2Mrm/4dLLeswJzwf1r0bw0wj8Tht33owD+FedaIpjnbd1+7XoWmARavaMv3mRST9aia90HseMXU5MhA6msljifkkjPNXmOW3Hr0qg/+v/Gt4qyMZO7Lkxa51S4CSbw7NglvvenWrOhxSrqsG7lVBxg5Aqpa2ZublIh/E2K6CfSZfDWqWcmd1ldkAsw5U9x9ef1pSa2Kgnuei7HECoQ2SfSql6myDo20dcc1vRwEorKHwygg78DmqdxpZlkUuVKk/dyTmvO0ud6vY4B9LmvrrKoQp9eeK2NFgj0e5LzZAc45GcfSuwWxiB2hVXtxVe40tHdn2DJGMjrW8amljN07s1rG5t5YgUkTkdM81xPjOJo9Y3PGNjxDy2Pt1FbCaZtbcm9W9M1K+lpcQqk6+dg/8tOcE1sqiaM/YWeh5pq9xbzWttBFYGGZUP2iUvkO2eMegxxUmj6htiNndZMbnCknuOhHvXfXPhqwuIsfZgrkY3Hp+lc7qHhZIgpWSMKgwo/qKJTVghTaYksrlQv93vXLatfvaarJE6rInDAgYIBHSuwigjMaiSQbY1y7+wFcnpujz+LNfuZI9ywBslgO3QCs6bTu5F1OZWUdyWx8Svb48m+ng/4ERWhc+M70xbW1m4Ze6rIea6iH4VafJCA3m7+53Vxvi3wLdeHYvtKAyWucEkcrTXs5OyG5VYq50XhywfXLQ6hJ5xi3bUMrffx1/Ct3ynhby4tqKOyjFXfBvlyeCdKdfuiEqceoJzVm5tXiZmUAqeQR0rOcIrobQqzktWZFwJY4i5kYemOp9hWLcW+pXDZMzRoegVuRW7LJmYAKZpegCjp/hVq18PS3pZ7y5eJcjakJ5P1P+FEIXegpzsrs4yaxuI/u3dwGHIO+qcq3rD96YbpOwlAzXpo8D6OY2kkjkYoC2WkOa8xvdLvZTe3tjBIbG1IMrowcQoTgHGc81t7JI5vb3GhtPuIltLuwWNA2Qi/IM+uR1re0fwlolxMksN3dwuOqFg6kenPP61xL3VzGQkq7lOSuR94A/pXQeHtWdJ1GCT2PqKipGUVdM0pyhN6o0r34Ua1bt5ml3VrdRbiyoxMcgHpk8H86xNTuJbTV1sdWsTLcxL5c7vGEeaMe/fjo3XivbdEuvPskYnJI55rkPiva2lx4churglHgmGx1UFuR936H+lVCpe1yJ07N2PK7mOxj3NZXUskZTKiVNpXB4HHB+tNslYuA/deSPWpIBBd2IEZf9ySh3kE7TyDgds5/Opja+SECjfv4BxWjMkjX0ENJdHfjPnAZ7jiu10+dvtdm/GwNj9a5Lw7FtnP/AD0RwxFb2lymSOPH3hIcZ7cmolsWloeTJhevJbtVKU7pWI9atgh4nk6EDgVSX7wzXQczOp8J2gn1GLj7pzXo/iDRW1Xw7PDCubqHE1uMc7l5x+IyK43wLF/xMMjn5a9XiARR/e61w1p2nc7qMLwscf4R8YWWp2sdlcuLa8j+UI5wGHsTXXvHuwQoAPfNYGr+BND1q5e5khktrlzlpLdtoY+pHIrHPg/W9IYDSPFEoQHhJ1OP6iol7OTunYuMZx0audVJbONxRGwT2AA/xqqr3MfO0jHQ1jJcePrUYf8Asy9UHruAJ/lTT4g8TI2bnwwJMfxRTihR80VzeTNs3cw5IPP+zTlvJn+URn8Rwa59vGNzE4Fx4YvlPTC/Nn9KRvHMQPPh7VVPoExmqUJCdSJ0xnnVCzFUz15rm9Ud7y6EUZL89ulUrjxndzArb+Gr0k/3wR/Ssi7ufFd+rKloNPhI5PQkfU81fI+pHOuiDxDe7I10Ww/eXU5xKVP3R6fjXofgnQYdI0uOHGXI3StjkmuQ8I+GhaN9sny87cbjXqOnRrtH8qxqTVuWJpCm/ikatvGg27SeehzUWv6JDq2g3dnMAVkjIBPY44NWYUOV746cdKvTjNsFyM/zq4bES3PIfhZdMLG/0G4/19jOWVT/AHTwf1H611mqRRXNs1qJjbnPJA61w+uibwb43/tyKJmtLhcXKIOQPWuvstX0vxFbedYXKuQPmTPzL+HpWkveXMhRXLLlZSuI5rWBY7eSPapBLKvXFPtNSnjfL8jOeamntp4skIHUdlHNVoZVLFHTHPTuPqKy5mbJJqzNpNajmidJLd1yME7hXnd14N1KYMttIjQyOflSbaWX3+ma72C6g3FXT5cYBHSra2lpdfvFAHOQTxWyqJo55UlFnk134L1a3Ut9lB2DgeYDnPbr9awtMWeC8Ab92yvtKsMkfhX0Fb6fAFLPgluo61w+ueF1bU57q1TCs2Xz0+tU5LlJUbSN7w3J/oKgyHJGSCuK5P4vahnS7CxT5jJKZCvqB0/rXQ6UZIbVIhMAwODvB/SvPPFFxqPiXxk2maSnmyxYTcp+7jqc9h6ms6W5dXa7OQs7x7CVryMFHyQEX7vPVSD1GO1dQl1BdadbTwQGOSTcZAz5A2nHy8dOtb0/ww1G7tBLfXKCSNMkQRBVY+p9T+Fc9NpMllFFEk0hMIJCk4GCefxrdtPY50mtDU0vfHNJuxudVJwO9aQZo2zH90HcPas3RivnSZ+bahPuT61qpILi3WQdx82B0P8AkfrWbsy9keSPlYNx/iqEVNc8FV9BUIrpOU9G+Ho8y6yeoGK9O5LYBrzP4aLmaZv7or06MZfPXHNebiPjPTw/wE+wCLB79Kge3Dk85A7etWiQV2881AHw2Qxx06VjJHTBsjW0QHaSDmkfTojnJAz7VOJMgE8f0p27KDA49KSRbKTaXB1J6dOahewXOFLfjWkXXO1sVG0qrknoOlVYky2timV3YP1qsbBC4Zuc9c1LeXLG4CRNk9/pShpWdV6np0quhJdtrYKpVFAPr6Vu2NuUGB1PcUyytlSEFuWPGKvxgx9Oo5qEtRNl2CI7ueeM1LMmBtx0pbZskEH5qtXIzFnHWuyCXKck21I4bWbBL26TcikA9xnPsfauI1L4dR29493ot7LpswOQnLJ+HcfrXqklvmUN156GmtZo6lWXdg5APb2qad1exrNxkkmeURaj450oASQWmqxKfvI43frg1K3jpVP/ABNvDt/av3dIyR+td3LpFuGchcMetV105Tna7LnoAeDScu6BU+sWcZD420Eni9eNs/dmhK4/Gtey8Y6EW+bU7YD2kx/OtVtEQhhNbwS89XjVj+oqjP4Z0mb/AFukWjY7+SB/Ko5odiuWb6o0P+Es0XZmHWrHcR0kcf0NU38VaLh2utYsSOoCPkfTFQReB9CnlVH0a1C9d6gj+tbcHw68KmMFtDt2x1J3c/rVx5ZdzOSnHseea54+sUjay8OxST3c52BwnAJ4+UdSa7LwJ4PPhzSTLeqG1O7IkuCTynomfbv710+m+EdA0eYXGn6TaQTgcSqmW/AnODV6dcA4H4VbtFWiZayd2PaMPauPVTXiPiy3eyuVmTOFcgj2Ne5Q4Nu3fArx3xeGuNQkhUDDHkH2q76EJamboyxq80gBz5eDz61NZf8AEvuJLeVwVkjBzngE1EALVljHB2qx7Z7f1o1PeqTTHG3AVTUp62NJR0ueaXpBuGIHHSq/pU1zhZNoOcdahrrOE9L+F2Ql2x6E8V6OZNgGO5rzj4YsTFcqegPFd5LJ0HNeZiP4h6mG+A0fPJA9PSomfAJ7etVI5isfPAHrSmY5A7Gs2dEUWhIDjB6jpT0nLRk8r2qoJCOg4z1pztjrj6U0i2SNLgZY/hWRqmqpbwkBsseFB65qS7udiNt6d65K7keW4MjEnB456VVrGbd2dtptiY7cSS8ysAT61NaWjpePLI5IZsqNoG3/ABqlbeIbY26F5EXIH3jjFWl1ZHZXicMoOcg5FTqNnSRSBSMYxVkMCCc8/wA6woL5ZWDL0xyM1bScBxg9/Wkhcuhv2zFCKuvOpix3rLS8j8lUJ68Z9KUSE49Ca6YuysjmlG7uxr7w7MOcHj3qsdQCOY2yuDzmtHymCk9VPSuc1Tci5bcOeMUPQuCUtGaTssvzggjsc1Rf5CzdOcYrItL3EuGc8HHNbCSJOu1icn8aiWpry8ogYuRuzlcd6sRDK5API5NNhtwC2M1oLCAny8AUowuKU0thLa3UHdjGecYrXhA8vbjp+lUEwoVcYqdG2jBPsAa3jZHPO7LEoG04HvVCYbuelWi3JGc5/Kq78q1KWpKViFZhHaucHAFeUajuv9dkMYHyHAB716TqdwLLRppScHacV55oqtcrNPj94HJLYqW9EOEbsxr5N+pK38QYBval1OTbZurAKzSnjHbGK0v7Pdr1p3zjd+ZrA1m5L3EjkfuwRs/PmnFXdyqnuxsedSndIx96ZjgGnScMRSDpiu0809G+GJAjuh3Nd2z5yB+NeffDOT99dJ6Cu/k43cjmvNxH8Q9PDP3CGSQ9APoPWnrN7fWoZF3fh+tMB24POR2rNHSmXFm52k5HpUc0/ljap6+/SqU935Z2g/MarPMS+M5rSMRORYLlwcfnWfNbBnJTBA61diUlsDgdhUyW+eqjFDBGDPZiUH2qG2tHt5g0UjRMD95DwfqK6KawyoYKTjrVGS0k/hXHeknYlmpZXZUglsdmHvWtFdgc7sjrXIxvPE5/ds6Hqo/pWpaXCYHmCQemUzStdlxlodCNXRB+9cADuTgVbtPEVjI4X7VCWHbeK5z7NBcNhkZlP98U2Xw7ZSqSttEPQ4rRRJfKz0GLU45FG1gazNbkSW3VUxvY5NcxYaRJYEql5Ls/uZ+UVqDJjBJJPRiad31EoJaowrmRrW5Vm7nHSteyuiUBB+UjqD0qnrVuZbdtv3wMjFZ2k3nmRqCee496iW5qtUd7bToVRieT+pq59qULhWBHoK5qC4yMKOlXUnYcgAmmp2MnTVzXEqsdwOTjpUsUwJx69qzY5SDljjP3uKuRuSOT9KakJxsi+rcZ9KZyQee1M8z5D0x3p0ZxGzEdqpaswlojj/GlzttILJT80rc89qoaND9jsmJAwRg0aqj6r4mUqpMUHy57ZrXuLeJEjiAGAMsamT10N6UbR1OX1u9Sxs1ix80p5x1ArjtQhBjaSBt4cHOe1R+MNSN5rciW8hAgOBg1HpL/AGpSpJJI5FdMFaJxVZc02cO/LE0g6ipfLbfgDPGaRlyu5Rgdx6Vuch2Hw4mKa1LHnhkr0ufKN614/wCDbk2viO3OcB/lNeyTAMg47V5+KXv3PQwsvdsVlOenXtUyWyufr1qCPO84HGK0rRVJ+Y496xR1mBPa+W0khAJ3Hj2rJW6jW4wXx32k12t3bIOSuVzXGeIdAguzJNCuJRzwetaRd3Zmd30NazngwCXX6ZrTie3JH71TxxzXj9/b3dhD5sc0qbTg4Y1saFc3N1ApeaTPTJPetJU7K4ozvLlZ6erwlOGBPoKWO2icnjdz0HauO+03dsAXBdexWrtvrRU43svqKysb8iZ1KWiKxxGMntjrUi2AY8IFYHJ4rMs9cZWy4V0IrYh1WzKK2SMnoapGcoNAluok2Ffxz1qb7IQcc7c/lUq3dmcMHHrn0qWO6t5F4dfQAmrsRaRVaIqvHPbBpjbVXbtyPYVblSMhgD1OeDVWX0IwOlR1KTdjNulY7gcda5Fi1jqrIeA5yPSu3uEDJjdjvya5zXbDz4gYh+9XkUWui1I0LW5JKliCTwa1Y7hVP3u3WuN0nUN6BX++pxg9a34pgRk8VmaG5bTCSRSQOmBWvAQ0fORzXPWzh2BBHritxZMW28cMeBkdaqJlULWTsx696fdSeTYsP4m4FCLhVHYcUy8UyzQRDoPmNax7nNJ30MxrMwAeWo3OPmPvXPeK9SGiaPLlszSDC13FzsS3Z26KM5rwbxlrp1XVJBu/cQkqPc1UYXkOdW0LHLsS9x5hJLPyT6mrmmTm2vkcHvyKpscgbfTNSWMg+0gN3roexxdSizvG4HBBHSqwJA5zu/umrpTKo5HKnFU71PKuSvPrVkMW2l+z3sM33drhq9utLkXVlFKpyGUHNeEqecHvXqPgfUjc6SYJGy8Jx+Fc+JjeNzow0rSsdQo2vnqDWjZx7idx47YqgCCuas2cpjYc1xI7rl2/H+ilh0rEMe49OCMc1vzHzLdkxwRWMiDb05B4qnuETm9V0aOaOWIp8jjt2rP8NWymKbS5jiSORVBHXk9RXYzx7uBg45rGu9NIv7e9t5PKlSRS2BxIoOcGtVK6sxyp8y5o7o6/X9EiNmklqgEilV2gfeHT86xrrRfsLiK5jUuw4I6Gt6HxBZXGt2tis6tKVMgTHp61rahaxXs1pvUEK+T9MVTp3V0YQrODtI4caFE6fuZHjbHQHj8qkg0GdpG3XLBBxkr1rt7zSIXiLwqEdRxjvSaFp6XH76cZTPC+tJRknZm/1iPJzI5H+xrlPu3QI91qBrK+jbEc6Hn+7Xe6rpima3SBQhdsEjsKi1HRYv8ARo4TtAYh27kU3BiWJi0jhmfVrdso8bnuM4qpN4rms+b62ZVB+8Dmu91jTFeGCC3KxKrZZiOQMfzrzXx5pMCLbWtnO0s8h3TEtwo/xoVO+4vbqWiWpo2virTdQXEV1Hz1Vjg1JLeW7kgzBc8A+9ctpfhNbxVXygsa4+Yjk1vx6JbaUskYyybsjcc4qZqMdilzdTKlgMOtjyyGWVc/iK6S1jcgZz9BVa0smubtrpx/sxj0FbMEeHUE4J4rFu5a0RcsLUmRdxOAc1ruczxQqAcZc57VHartQZIwOant0ZnklPVuB9Kqxi5XZeh5AJ6daqpODO8pPU4H0qtrmqx6NotxdyHAVTgV5V/wseY2zGKBgT3Nbxi2tDHmin7zO78a+JI9N0SYK48112qAa8GcyGAySdZDWxdanca1M0t5Jx1UdhWNc3PnXCpGMKvAFbwjZanPVmpPQkciJo93BK1W8zy58j7uagu5G83k52jGaWNhJF71djK5pNtSTYehFQ6rAHSOVSMhcEVY1FQcMvUVAmZbYjOWXoPWkHkYwHPNb3hXVW07V0Vj+7kO01jSR5JK/iKiG5SGBwR0NU0pKwotxdz3lHDgFeRUsZKSAiuX8JawNQ0tUd/30Qw1dKHyoPXvivMlHldj1Iy5o3RppKTEDnnvVZ0O/jgGnWzhs4OMjPSpnQuuR1xSKKjLuQgY3LUIKdGAGelWiNpwe3Wq88IkG5fTpVXLpysyBbRfPjmX78ZzG68MprVtry+TVBc3c5liEWxUCgBOc596x4ppIZCGBwtXob1W+/wWNOLa2N5whNao6ca7ZGNlNzGGxyGOD+taNhe2i26eXPFjHGHFceXtp5FDbWBUnkZq5a21uPuCPB6DA4rZVG3scssJC250VxrVp/advamVfMkUlTnjPoT2JrL1TxJFbavbWgV5A4Yu6DIT0z9aT+zYlG544/qMVAyWMLsRGGPXA9ap1H2M1hY33M3V9Xv72dktUMcRGwOc59z7VnWujK0nmTbmz1djkmuhx9ozsjGfboKeItvB+Zu5rKUmzZctNWSK0MMcS7UXGzoPWsm8QzXGwjK55rbKCKNyvLHkk+tVYLfu3J9aykJb3FhtwkXB7YqeGFVAkbO4dKCdnGantuvmMMD0pJEuTLJ/dRLGDlj1q3bZEfzfWqS5Mu9upOaoeINbj0jTHct+8Iwo960jG7MpNKJxfxL1ltQvE0qB/wBzHzLjufSuCli+VUUYUdavXEzyzyTSNl5DuNQGN1iZ279q7UrI43qyixGwoowoqi0qxhvKGCfvMa0JmSK3ZyD7VhyNJK2Npx6Yq0ZyHO3ntgdRSxI6yAHI5psKYf5gc56Cuo0/TkZVuLnARORmhuwJXKl+nlzsjVnW03kz4PK5wa2tagZJyT6VgFCJQexNSim+pNe2wWTehxnkVSO08dG71szEPYj5csP5VjzLk7hTRLL+hao2k6iku4+UThx7V65azpc2yyxNuVhkEV4bmu38Ea95b/2dcPgH/VE/yrGvTuuZG9CrZ8rPS7QgHqfpWkq8Z9qyoCN/B4rSQkr06HrXCd25FKoXOR3qpnZIR7Zq/MTgnrVVkIAIA96LlJEBWJxzgHuKje3IO5Kn8ncwPAzT9jrgdapFczRR8lww78YzT4/OTacuOccGrgJzkjr0yKsJjaDhfYVV2Uqj6kcCXEm0NIxGPWtOCwACmU5z2zUUTkfcAB9auR75G5POc01qRKo+hLtUKFjGB/Ok2Y4FTBD6Zz3qRk2Djr70zC5nyxAgioiAnBxV2RRjPFUJAcnjvUNdS+boM2h39c1OWAIQCmBBGAR+VEkkcCGSUgKBnNOMWyJSCW5jtYHmlbCqMkmvKfEOtyaxqDFSfJU4RRV7xR4kl1KZrSzJ8lT8zdq5Ka6jsUwp3SH+L0rqhCxzTndmhCkcLB5SC3oadNdW6BixyOpxXNS39xMvXAPrVu2iUWbvMSx+tbWMuYty6lpixAsrMT2Aqu+q2LqQtk3scVjzS4YhFEa9geTUbzEgAFuO5NOxHMa322GM7kt0X68mhtRkmBLOQo6LWdGgJBAy3pmtKGBZlCMhX0xUuyKV2aviqTZMqoeWrCjAaMK33jVu/nN1dM7HJ6CqnSVeeOlMSVkWG5s5FJ5XpWQZMjI4PcVrlSm/PQrWIw5YjsaEJikBuR19KEd4pFdCVZTkEdqZ3yKerbuDVCPWPCmvrqtiodgLiMASD+tdlBN3HOO1eA6Xqc+kaglxCTwfmX+8PSvY9K1SK9tIbqNso4yMdvauGtT5XdHfQqcys9zojgqeMg1CEJOME461Ikm4fLyCOaXYwyAawOtDCqMnPbj3qSOEFe9NLEN90+hq3GVxxx9KBtaEQtAxPy49D608WTKenNX7fYc5/HNXgiEZIwOua0jG5jKTTMqO2CnDAVbijbgbQBUu1WYgLx9KlRQi49KdiWxFAzxTZTxx170uctweRUbMQeTTsRsVpc9OBVXHzZJzUs8uJOR096yb7VIbSNmJGevWjluwuWri5S3jLyMAq981534m8SS3xMED7IBwT61V8QeI7i6cpEdqHvXKSPJM2Mkk9K3hBLUwlO+hYluPKj2oQCxwBWBcymWZjuJwcVozRvDHJJIMtt2oPTNZCbQcNW0UrGEmx4coRgc1cjdvKkMzMPl+UD1ohit2ALS/hirCpbIxYK0hIwMnii4JGRtZgWPT1NLHE0n3RnmrJs55ZD6ds8Veg011iJCkNindCSKYBifGOccmtWwaInDS4PvWSYp45DuBH1FXrcptAnt2x/eAqZK6Ki7CRQSuNx79WNVpH3Xaov3V61r6xMkaiGAcjrisiCMrlj941MHdXZU42djQkIFmzHrWOyfuycda1JTtslj/AInOapzR+XEoPeqRLRR8s7c0gjYtgDmrUaGQhFGSelaiWqQRgYBfuTTcrCjDmMhoSEBI5rqvCGpvahrZs+UxyP8AZNYF0R3rZ8MoHY+1Z1NY6mtNcs9D06yvg6hScfjWtHLkBgdw74rjURlUMpwRWjbamUwHfBFcTR6EZXR1KFX7/XNJ0Jx+NZC6oGXhh+dSLfBsfMKXKO5sW82AQSc5rWhnV0xnB71zUVwmfb61aju1U5DZx79KuLsTJXOh3Dnuahe4wMZB5xWYb4AcOMepqpPq8UYzkE+lWZ2Ng3CqOvPr6VRutQSNSd2fasSXUp5+IxgVAQ20ySt06k0XJauTXuqHYzfdX1rltXlbyPPuCQpGVjz1+tawhN5N5koxChyq56+5rkvFl4ZLryg3SriQ1ZHPXE7Ty9eWNb+kaWJB50q/Ko+UetYekQfaLxVOCSe9eiLFHY2Q37QAua1k7Iygru7OD8QKttGxI5PQVz+n6bPqEuIwQo6tW9c/8VDrwhLbLdDyfWuyjg0rS7LZDIm4DsapaIlpSlc8/vNKaxCkMfo1UxM6NtyAfeuh1i9t7nAiwWB9a5y6jIkyy4J6URu9xTsti1DdvE2XUkVd/tBCB+5Rl6jnBrHEsqKA3zKamKJgMu5Gx07U2kSpM2zdxzJsliZQBkFTToJLcxMsbtuBz81ZEH2poyQAyj3p8c8iSKzblzwwI6ip5exVz//Z' - }); + switch (_userId) { + case null: + case undefined: + case '48d31887-5fad-4d73-a9f5-3c356e68a038': + return Promise.resolve({ + '@odata.context': + 'https://graph.microsoft.com/v1.0/$metadata#users(businessPhones,displayName,givenName,jobTitle,department,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,userType)/$entity', + businessPhones: ['+1 412 555 0109'], + displayName: 'Megan Bowen', + givenName: 'Megan', + jobTitle: 'Auditor', + department: 'Finance', + mail: 'MeganB@M365x214355.onmicrosoft.com', + mobilePhone: null, + officeLocation: '12/1110', + preferredLanguage: 'en-US', + surname: 'Bowen', + userPrincipalName: 'MeganB@M365x214355.onmicrosoft.com', + id: '48d31887-5fad-4d73-a9f5-3c356e68a038', + userType: 'Member', + personImage: + 'data:image/jpeg;base64,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' + }); + default: + return Promise.reject(new Error('Invalid userId')); + } }; diff --git a/packages/mgt-components/src/utils/PresenceService.ts b/packages/mgt-components/src/utils/PresenceService.ts new file mode 100644 index 0000000000..2fb2bbf4f8 --- /dev/null +++ b/packages/mgt-components/src/utils/PresenceService.ts @@ -0,0 +1,191 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +import { ProviderState, Providers, log, error } from '@microsoft/mgt-element'; +import { getUsersPresenceByPeople } from '../graph/graph.presence'; +import { Presence } from '@microsoft/microsoft-graph-types'; + +/** + * Holds the presence options. + * + * @export + * @interface PresenceConfig + */ +export interface PresenceConfig { + /** + * The maximum time limit before the presence is polled again. + * + * @type {number} + * @memberof PresenceConfig + */ + refresh: number; + + /** + * The maximum time limit after a component is registered for it to be queried. + * + * @type {number} + * @memberof PresenceConfig + */ + initial: number; +} + +export interface PresenceAwareComponent { + get presenceId(): string | undefined; + onPresenceChange(presence: Presence): void; +} + +/** + * Class in charge of managing presence across all users and components. + * + * @export + * @class PresenceService + */ +export class PresenceService { + private static readonly components = new Set(); + private static initPromise: Promise | null = null; + private static registerPromise: (value: unknown) => void; + private static isStopped = false; + + private static readonly presenceConfig: PresenceConfig = { + initial: 1000, + refresh: 30000 + }; + + /** + * Returns the presenceConfig object. + * + * @readonly + * @static + * @type {PresenceConfig} + * @memberof PresenceService + */ + public static get config(): PresenceConfig { + return this.presenceConfig; + } + + /** + * Registers a component with the presence service so that it can receive updates. + * + * @static + * @param {MgtPerson} component + * @memberof PresenceService + */ + public static register(component: PresenceAwareComponent) { + if (this.initPromise === null || this.initPromise === undefined) { + this.initPromise = this.init(); + } + + // after init, add component + this.initPromise.then( + () => { + setTimeout(() => { + if (this.registerPromise) { + this.registerPromise(null); + } + }, this.config.initial); + this.components.add(component); + }, + e => error('the PresenceService could not be initialized; presence will not be updated', e) + ); + } + + /** + * Unregisters a component with the presence service so that it no longer receives updates. + * There is no error if the component was not registered. + * + * @static + * @param {MgtPerson} component + * @memberof PresenceService + */ + public static unregister(component: PresenceAwareComponent) { + this.components.delete(component); + } + + /* + * Stops the presence service. + * + * @static + * @memberof PresenceService + */ + public static stop() { + this.isStopped = true; + } + + /** + * Starts a timer for notifying the components of presence updates. + * + * @private + * @static + * @memberof PresenceService + */ + private static async init(): Promise { + const loop = async () => { + while (!this.isStopped) { + // wait for next interval + const registerPromise = new Promise(resolve => (this.registerPromise = resolve)); + const timeoutPromise = new Promise(resolve => setTimeout(resolve, this.config.refresh)); + await Promise.race([registerPromise, timeoutPromise]); + + // get a valid graph provider + const provider = Providers.globalProvider; + if (!provider || provider.state === ProviderState.Loading || provider.state === ProviderState.SignedOut) { + continue; + } + + // log attempt + log(`updating presence for ${this.components.size} components.`); + const presenceByUserId = new Map(); + + // get full list of users + for (const component of this.components) { + const id = component.presenceId; + if (id && !presenceByUserId.has(id)) { + presenceByUserId.set(id, undefined); + } + } + + // get updated presence for all users + const listOfIds: string[] = []; + try { + const presences = await getUsersPresenceByPeople( + provider.graph, + Array.from(presenceByUserId.keys()).map(userId => ({ id: userId })), + true // bypassCacheRead + ); + if (presences) { + for (const presence of Object.values(presences)) { + if (presence.id) { + presenceByUserId.set(presence.id, presence); + listOfIds.push(`${presence.id}=${presence.availability}/${presence.activity}`); + } + } + } + } catch (e) { + error(`could not update presence`, e); + continue; + } + + // log results + log(`updated presence for ${listOfIds.length} users.`, listOfIds); + + // update components + for (const component of this.components) { + const id = component.presenceId; + if (id) { + const presence = presenceByUserId.get(id); + if (presence) { + component.onPresenceChange(presence); + } + } + } + } + }; + + void loop(); // start loop without waiting for end + return Promise.resolve(); + } +} diff --git a/packages/mgt-components/src/utils/SvgHelper.ts b/packages/mgt-components/src/utils/SvgHelper.ts index 67be44d881..693ad10831 100644 --- a/packages/mgt-components/src/utils/SvgHelper.ts +++ b/packages/mgt-components/src/utils/SvgHelper.ts @@ -299,7 +299,12 @@ export enum SvgIcon { PresenceAway, PresenceOofAway, PresenceOffline, - PresenceStatusUnknown + PresenceStatusUnknown, + + /** + * Loading icon + */ + Loading } import { html } from 'lit'; @@ -772,5 +777,11 @@ export const getSvg = (svgIcon: SvgIcon, color?: string) => { `; + + case SvgIcon.Loading: + return html` + `; } }; diff --git a/packages/mgt-react/src/generated/person.ts b/packages/mgt-react/src/generated/person.ts index 7537ab1c62..cae3e7bae1 100644 --- a/packages/mgt-react/src/generated/person.ts +++ b/packages/mgt-react/src/generated/person.ts @@ -15,6 +15,7 @@ export type PersonProps = { userId?: string; usage?: string; showPresence?: boolean; + disablePresenceRefresh?: boolean; avatarSize?: AvatarSize; personDetails?: IDynamicPerson; personImage?: string; @@ -29,6 +30,7 @@ export type PersonProps = { line3Property?: string; line4Property?: string; view?: ViewType | PersonViewType; + presenceId?: string | undefined; templateContext?: TemplateContext; mediaQuery?: ComponentMediaQuery; line1clicked?: (e: CustomEvent) => void; diff --git a/stories/components/person/person.properties.stories.js b/stories/components/person/person.properties.stories.js index b150e19a25..19da739347 100644 --- a/stories/components/person/person.properties.stories.js +++ b/stories/components/person/person.properties.stories.js @@ -171,6 +171,11 @@ export const personPresence = () => html` > `; +export const myPresence = () => html` +

Change your presence in Teams to see the presence reflected here...
+ +`; + export const personPresenceDisplayAll = () => html` +
Refreshing presence
+ +
+
Refreshing presence (fake)
+ +
+
Refreshing presence disabled
+ + `; + export const moreExamples = () => html`