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>
    );
  }
}

3 thoughts on “Sharepoint Online: Custom web part that display news only from followed sites

  1. Nice article…thanks for sharing..Can you post a working solution that we can deploy and test..thanks in advance

  2. Hi. I apologize for the delayed response, but I didn’t see your comment until today. Unfortunately I don’t have access to the source code anymore, as I’m currently employed in another company. I hope you’ll get your own solution up and running.

  3. The code is working fine in Workbench but throwing following error when using it after deploying in App catlog at the time of fetching news information from the followed sites:-

    HttpClient request in queryable [403] ::> {“odata.error”:{“code”:”-2147024891, System.UnauthorizedAccessException”,”message”:{“lang”:”en-US”,”value”:”Attempted to perform an unauthorized operation.”}}}

    Additionally if you can share following CSS classes will be helpful :-

    styles.notificationBox
    styles.description
    styles.subTitle
    styles.column
    styles.title

    Once everything is working at my end i will share the working solution with you.
    Thanks for all your help indeed.

Leave a Reply

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

%d bloggers like this: