Sharepoint Online: Custom web part that display news only from followed sites

Sharepoint Online: Custom web part that display news only from followed sites

At work we rolling out our new Intranet based on Sharepoint Online. While the news web part shipped with the platform works pretty well, we found that we had some requirements that combined didn’t make is a good fit:

  1. All news should be available to logged in user
  2. News are aggregated from intranet related sites the users follow
  3. Users can not opt out from receiving news from their own department

With these requirements in mind, we decided to implement our own, simple news web part. In case this can be of help for anyone I’ve pasted in most of the source code below (just skipped some of the boilerplate code generated when setting up the project), even though it’s work-in-progress and will be improved and modified.

import * as React from 'react';
import styles from './NmbuNews.module.scss';
import { INmbuNewsProps } from './INmbuNewsProps';
import { sp } from "@pnp/sp";
import { SocialActorTypes } from "@pnp/sp/social";
import { Web } from "@pnp/sp/webs";
import "@pnp/sp/webs";
import "@pnp/sp/social";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/site-users/web";
import "@pnp/sp/site-groups/web";
import "@pnp/sp/sites";
import { graph } from "@pnp/graph";
import "@pnp/graph/users"

type NewsStory = {
  siteUrl: string;
  title: string;
  description: string;
  bannerImageUrl: string;
  created: Date;
  fileRef: string;
};

interface MainState {
  userEmail: string;
  news: any[];
  isLoading: boolean;
}

export default class NmbuNews extends React.Component<INmbuNewsProps, MainState> {

  constructor(props: INmbuNewsProps) {
    super(props);
    const initialState: MainState = { userEmail: "(unknown)", news: [], isLoading: true };
    this.state = initialState;
  }

  private async _getFollowedSites() {
    const followedSites: any[] = await sp.social.my.followed(SocialActorTypes.Site);

    let followedSiteUrls = [];
    followedSites.forEach(site => {
      let siteUrl: String = site.ContentUri

      if (siteUrl.indexOf("/intranet_") !== -1) {
        followedSiteUrls.push(siteUrl)        
      }      
    });

    return followedSiteUrls;
  }

  private async _getNewsByUrl(siteUrl): Promise<any[]> {
    let news = [];
    let web: any = Web(siteUrl);

    let pages: any = await web.lists.getByTitle("Site Pages").items.select("Title", "BannerImageUrl", "Description", "Created", "FileRef").filter("PromotedState eq '2'").getAll();

    for (let i = 0; i < pages.length; i++) {
      let page = pages[i];
      let story: NewsStory = {
        title: page["Title"],
        siteUrl: siteUrl,
        bannerImageUrl: page["BannerImageUrl"]["Url"],
        description: page["Description"],
        created: page["Created"],
        fileRef: page["FileRef"],
      };

      news.push(story);
    }

    return news;
  }

  private async getRequiredNewsSourceByEmailAddress(emailAddress: string): Promise<string[]> {
    console.debug(`Fetching groups for user ${emailAddress}`)

    let nmbuOrgGroupNameprefix: string;

    const memberOf: string[] = await graph.users.getById(emailAddress).memberOf();
    memberOf.forEach(element => {
      const groupDisplayName: string = element["displayName"]

      if (groupDisplayName.indexOf("SG-Users-") == 0) {
        nmbuOrgGroupNameprefix = groupDisplayName.substr(0, 17)
        return        
      }
    });

    let userHomeSite: string;

    switch (nmbuOrgGroupNameprefix) {
      case "SG-Users-Marketing":
        userHomeSite = "marketing"                
        break;
      case "SG-Users-Support":
        userHomeSite = "support"
        break;      
      default:
        userHomeSite = undefined
    }

    if (userHomeSite === undefined) {
      console.info(`No required sites defined for group ${nmbuOrgGroupNameprefix}`)
      return []  
    }

    return [`https://acme.sharepoint.com/sites/intranet_org_${userHomeSite}`]
  }

  public async componentDidMount() {
    let user: any;
    try {
      user = await sp.web.currentUser();
    } catch (err) {
      console.error(`Failed to fetch user. Error: ${err}`);
    }

    const followedSites: string[] = await this._getFollowedSites();
    const requiredNewsSites: string[] = await this.getRequiredNewsSourceByEmailAddress(user.Email)
    const allNewsSites = followedSites.concat(requiredNewsSites)

    let allNews: any[] = [];

    for (const siteUrl of allNewsSites) {
      let news: any[] = await this._getNewsByUrl(siteUrl);
      allNews = allNews.concat(news);
    }

    allNews.sort((itemA, itemB) => (itemA.created < itemB.created) ? 1 : -1)

    this.setState({
      news: allNews.slice(0, 4),
      userEmail: user.Email,
      isLoading: false,
    });
  }

  private createNews = () => {
    let newsList = this.state.news;
    let table = [];

    if (this.state.isLoading) {
      return (
        <div className={styles.notificationBox}>
          <div className={styles.text}>
            Loading news....
        </div>
        </div>
      )
    }

    if (this.state.news.length == 0) {
      return (
        <div className={styles.notificationBox}>
          <div className={styles.text}>
            News from sites you subscribe to will show up here. See <url> for more info.
        </div>
        </div>
      )
    }

    for (let i = 0; i < newsList.length; i++) {
      let newsStory: NewsStory = newsList[i];
      table.push(
        <div className={styles.newsStory}>
          <div className={styles.leftColumn}>
            <a href={newsStory.fileRef}>
              <img src={newsStory.bannerImageUrl} alt="Image" />
            </a>
          </div>
          <div className={styles.rightColumn}>
            <div className={styles.newsHeader}>
              <a href={newsStory.fileRef}>
                <div className={styles.newsTitle}>{newsStory.title}
                </div>
              </a>
              <div className={styles.newsUrl}>
                Kilde: <a href={newsStory.siteUrl}><i>{newsStory.siteUrl}</i></a>
              </div>
            </div>
            <div className={styles.newsContent}>
              {newsStory.description}
            </div>
          </div>
        </div>
      )
    }

    return table;
  }

  public render(): React.ReactElement<INmbuNewsProps> {
    return (
      <div className={styles.nmbuNews} >
        <div className={styles.container}>
          <span className={styles.title}>Mine Nyheter</span>
          {this.createNews()}
        </div>
      </div>
    );
  }
}

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: