adding js sample to demonstrate how to use UCWA SDK and subscribe to people status (#378)

* adding js sample to demonstrate how to use UCWA SDK and subscribe to skype status

* js-skype-status: fixing extension casing for images links

* removing files that are not supposed to be here

* js-skype-status: adding missing import for jquery in service

* js-skype-status: aligning services with react version

* adding missing telemetry code

* js-skype-status: updating sample's readme to match standard

* updating readme to latest version of the standard
This commit is contained in:
Vincent Biret 2017-12-08 06:58:58 -05:00 committed by Vesa Juvonen
parent 137526e5d4
commit d16af92f89
37 changed files with 624 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

32
samples/js-skype-status/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.3.4",
"libraryName": "skype-presence-spfx",
"libraryId": "fcfeb3e4-bf84-4405-81c1-9c8f5dbaabe6",
"environment": "spo"
}
}

View File

@ -0,0 +1,69 @@
# JavaScript Skype Status WebPart
## Summary
This sample demonstrates how to use the UCWA JS Sdk for skype in the SharePoint Framework. It shows how to subscribe to status change of the different people of the organization but you can get much more information, checkout the [documentation](https://msdn.microsoft.com/en-us/skype/websdk/docs/generalreference?f=255&MSPPError=-2147217396)
The goal is to demonstrate how you can leverage the SDK and to have the simplest approach, hence the usage of JQuery. No Framework (React, Angular, Knockout...) is used here but you can use that SDK in conjuction with any framwork.
No branding has been applied to keep it simple but you could perfectly leverage Office Ui Fabric to display persona cards.
Allows you to properly display and subscribe to change of users' availibility.
See the demo:
![demo](./images/demo.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-GA-green.svg)
## Applies to
* [SharePoint Framework Developer](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Solution
Solution|Author(s)
--------|---------
js-skype-status|[Vincent Biret](https://github.com/baywet)
## Version history
Version|Date|Comments
-------|----|--------
1.0|December 1, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
### Authentication
Because the Skype API's are secured, you first need to register an Azure Active Directory application.
To do that go to `portal.azure.com` and sign in as your tenant adminstrator.
![azure active directory](./images/1.PNG)
Click on `Azure Active Directory`.
![app registrations](./images/2.PNG)
Click on on `App Registrations` and then `New Application Registration`
![app details entry](./images/3.PNG)
Enter any name, select `Web app / API` in `Application Type` and in `Sign-In URL` enter `https://*.sharepoint.com/*`, then click on `Create`
![app details display](./images/4.PNG)
Take note of the `application ID`, we'll need it later. Click on `required permissions`.
![skype permission](./images/5.PNG)
Click on `add` then `select an API` and select `Skype for Business`.
![skype scopes](./images/6.PNG)
Select under `delegated permissions` both `Read/Write Skype user contacts and groups` and `Read/Write Skype user information (preview)`, then click `select`. Finish by clicking `Done`.
![grant permission](./images/7.PNG)
Don't forget to click on `Grant permissions` and `yes`.
### Updating the app
After copying that sample to your local machine and running `npm install` to install the depenencies, `/src/webparts/skypePresence/services/Constants.ts` open your favorite editor. Make sure you replace the value of `ApplicationIdKey` to the value application id we kept earlier.
### Deploying the application
Start by running `gulp package-solution` and deploy the application to the app catalog. More information on how to do it [here](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/serve-your-web-part-in-a-sharepoint-page)
**At the end select SkypePresence webpart instead**
Don't forget to run `gulp serve --nobrowser` to start the debugging server.
### Addtional page required
Because Skype needs to silently redirect the user to a page for the authentication flow, you need to create an additional page called `skypepresence` in your site.
You don't need to add anything on that page.
![tracking image](https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/js-skype-status)

View File

@ -0,0 +1,20 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"skype-presence-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/skypePresence/SkypePresenceWebPart.js",
"manifest": "./src/webparts/skypePresence/SkypePresenceWebPart.manifest.json"
}
]
}
},
"externals": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"
},
"localizedResources": {
"SkypePresenceWebPartStrings": "lib/webparts/skypePresence/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "skype-presence-spfx",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,12 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "skype-presence-spfx-client-side-solution",
"id": "fcfeb3e4-bf84-4405-81c1-9c8f5dbaabe6",
"version": "1.0.0.0",
"skipFeatureDeployment": true
},
"paths": {
"zippedPackage": "solution/skype-presence-spfx.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,45 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

View File

@ -0,0 +1,31 @@
{
"name": "skype-presence-spfx",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.3.4",
"@microsoft/sp-lodash-subset": "~1.3.4",
"@microsoft/sp-office-ui-fabric-core": "~1.3.4",
"@microsoft/sp-webpart-base": "~1.3.4",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"jquery": "^2.2.4"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.3.4",
"@microsoft/sp-module-interfaces": "~1.3.4",
"@microsoft/sp-webpart-workbench": "~1.3.4",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/jquery": "^2.0.48",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2",
"gulp": "~3.9.1"
}
}

View File

@ -0,0 +1,26 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "cd41bf93-187a-47db-8ef1-ee84b37706ce",
"alias": "SkypePresenceWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "SkypePresence" },
"description": { "default": "Demonstration webpart leveraging UCWA and presence subscription" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "SkypePresence"
}
}]
}

View File

@ -0,0 +1,74 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.skypePresence {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,68 @@
import { Version } from "@microsoft/sp-core-library";
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from "@microsoft/sp-webpart-base";
import { escape } from "@microsoft/sp-lodash-subset";
import * as jquery from "jquery";
import styles from "./SkypePresenceWebPart.module.scss";
import * as strings from "SkypePresenceWebPartStrings";
import { SkypeForBusinessCommunicationService } from "./services";
export interface ISkypePresenceWebPartProps {
description: string;
}
export default class SkypePresenceWebPartWebPart extends BaseClientSideWebPart<ISkypePresenceWebPartProps> {
public render(): void {
const skypeService: SkypeForBusinessCommunicationService = new SkypeForBusinessCommunicationService(() => this.context);
this.domElement.innerHTML = `
<div class="${ styles.skypePresence }">
<div class="${ styles.container }">
<div class="${ styles.row }">
<div class="${ styles.column }">
<span class="${ styles.title }">Skype presence with UCWA JS SDK sample for SPFX</span>
<p class="${ styles.subTitle }">Enter the email address of the user you want to see the status change</p>
<input type="email" id="emailaddress" /><input type="button" value="subscribe" id="subbutton" /><br />
Status <span id="status">Loading...</span>
</div>
</div>
</div>
</div>`;
jquery("#subbutton").click(async () => {
const emailAddress: string = jquery("#emailaddress").val() as string;
await skypeService.SubscribeToStatusChangeForUser(emailAddress, "Name", (newStatus, oldStatus, displayName) => {
jquery("#status").text(newStatus);
});
});
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("description", {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface ISkypePresenceWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'SkypePresenceWebPartStrings' {
const strings: ISkypePresenceWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,21 @@
import { IWebPartContext} from "@microsoft/sp-webpart-base";
import { CommunicationServiceConfiguration, Constants, ICommunicationConfigurationService } from "./";
export class CommunicationConfigurationService implements ICommunicationConfigurationService {
private _currentConfiguration: CommunicationServiceConfiguration;
private _webPartContext: IWebPartContext;
public constructor(WebPartContext: IWebPartContext) {
this._webPartContext = WebPartContext;
}
public async getCurrentConfiguration(): Promise<CommunicationServiceConfiguration> {
if (!this._currentConfiguration) {
this._currentConfiguration = new CommunicationServiceConfiguration();
this._currentConfiguration.ClientId = Constants.ApplicationIdKey;
this._currentConfiguration.RedirectUri =
`${this._webPartContext.pageContext.web.absoluteUrl}${Constants.ApplicationRedirectUrl}`;
return this._currentConfiguration;
} else {
return Promise.resolve(this._currentConfiguration);
}
}
}

View File

@ -0,0 +1,4 @@
export class CommunicationServiceConfiguration {
public RedirectUri: string;
public ClientId: string;
}

View File

@ -0,0 +1,5 @@
export class Constants {
public static ApplicationIdKey = "application id";
public static ApplicationRedirectUrl = "/SitePages/skypepresence.aspx";
public static ErrorCategory = "communication service";
}

View File

@ -0,0 +1,5 @@
import {CommunicationServiceConfiguration } from "./";
export interface ICommunicationConfigurationService {
getCurrentConfiguration(): Promise<CommunicationServiceConfiguration>;
}

View File

@ -0,0 +1,4 @@
export interface ICommunicationService {
SubscribeToStatusChangeForUser(userEmail: string, userDisplayName: string,
handler: (newStatus: string, oldStatus: string, displayName: string) => void): Promise<boolean>;
}

View File

@ -0,0 +1,84 @@
import { Log } from "@microsoft/sp-core-library";
import { IWebPartContext} from "@microsoft/sp-webpart-base";
import { Constants, CommunicationConfigurationService, ICommunicationConfigurationService, ICommunicationService } from "./";
import * as jQuery from "jquery";
declare var Skype: any;
export class SkypeForBusinessCommunicationService implements ICommunicationService {
private static initializePromise: any;
private static configurationService: ICommunicationConfigurationService;
private static webPartContext: () => IWebPartContext;
public constructor(WebPartContext: () => IWebPartContext) {
SkypeForBusinessCommunicationService.configurationService = new CommunicationConfigurationService(WebPartContext());
SkypeForBusinessCommunicationService.webPartContext = WebPartContext;
}
public async SubscribeToStatusChangeForUser(userEmail: string, userDisplayName: string,
handler: (newStatus: string, oldStatus: string, displayName: string) => void): Promise<boolean> {
if (!userEmail || !handler) {
return false;
}
userDisplayName = userDisplayName.replace("(...)", "");
const skypeApp: any = await this.Initialize();
const personsAndGroupsManager: any = skypeApp.personsAndGroupsManager;
const mePerson: any = personsAndGroupsManager.mePerson;
if (SkypeForBusinessCommunicationService.webPartContext().pageContext.user.email === userEmail) {
Log.info(Constants.ErrorCategory, `Bypassed skype subscription for current user ${userEmail}`);
handler("Online", undefined, mePerson.displayName());
} else {
const query: any = personsAndGroupsManager.createPersonSearchQuery();
query.text(userEmail);
query.limit(1);
await query.getMore();
query.results().forEach((result) => {
const person: any = result.result;
if (person.id().indexOf(userEmail) !== -1) {
handler("Offline", undefined, userDisplayName);
person.status.changed((newStatus, reason, oldStatus) => {
Log.info(Constants.ErrorCategory,
`${person.displayName()} status changed from ${oldStatus} to ${newStatus} because ${reason}`);
handler(newStatus, oldStatus, person.displayName());
});
person.status.subscribe();
}
});
}
return true;
}
private Initialize(): Promise<any> {
if (!SkypeForBusinessCommunicationService.initializePromise) {
SkypeForBusinessCommunicationService.initializePromise = new Promise<any>((resolve, reject) => {
return SkypeForBusinessCommunicationService.configurationService.getCurrentConfiguration().then((currentConfiguration) => {
return jQuery.getScript("https://swx.cdn.skype.com/shared/v/1.2.36/SkypeBootstrap.min.js").then(() => {
const config: {apiKey: string, apiKeyCC: string} = {
apiKey: "a42fcebd-5b43-4b89-a065-74450fb91255", // sdk
apiKeyCC: "9c967f6b-a846-4df2-b43d-5167e47d81e1", // sdk+ui
};
if (currentConfiguration && currentConfiguration.ClientId) {
Skype.initialize({ apiKey: config.apiKey }, (api) => {
const app: any = new api.application();
app.signInManager.signIn ({
client_id: currentConfiguration.ClientId,
cors: true,
origins: ["https://webdir.online.lync.com/autodiscover/autodiscoverservice.svc/root"],
redirect_uri: currentConfiguration.RedirectUri,
}).then(() => {
resolve(app);
}, (err: any) => {
Log.error(Constants.ErrorCategory, new Error(`cannot sign in ${err}`));
location.assign("https://login.microsoftonline.com/common/oauth2/authorize?response_type=token" +
"&client_id=" + currentConfiguration.ClientId +
"&redirect_uri=" + location.href +
"&resource=https://webdir.online.lync.com");
reject(err);
});
});
} else {
Log.error(Constants.ErrorCategory, new Error(`configuration missing for skype presence service`));
}
});
});
});
}
return SkypeForBusinessCommunicationService.initializePromise;
}
}

View File

@ -0,0 +1,6 @@
export * from "./ICommunicationService";
export * from "./SkypeForBusinessCommunicationService";
export * from "./ICommunicationConfigurationService";
export * from "./CommunicationConfigurationService";
export * from "./Constants";
export * from "./CommunicationServiceConfiguration";

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('SkypePresenceWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}

View File

@ -0,0 +1,11 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;
/* Global defintion for SPO builds */
declare const DATACENTER: boolean;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />