Implementing news feature in loklak

The idea is to bring out an useful feature out of the enriched high quality of data provided by api.loklak.org. Through this blog post, I’ll be discussing about the implementation of a news feature similar to Google to provide latest news relevant to the query in loklak.

Creating news Component

First step in implementing news feature would be to create a news component which would be rendered on clicking of news tab.

ng g component feed/news

Creating an action & reducer for news status

This step involves creating an action & reducer function for tracking the status of news component on click of news tab. When the user will click on news tab, corresponding action would be dispatched to display the news component in feed.

import { Action } from ‘@ngrx/store’;
export const ActionTypes = {
    NEWS_STATUS: ‘[NEWS] NEWS STATUS’
};
export class NewsStatusAction implements Action {
    type = ActionTypes.NEWS_STATUS;
    constructor (public payload: boolean) { }
}
export type Actions
    = NewsStatusAction;

 

Corresponding reducer function for news status to store current news status would be:

export function reducer(state: State = initialState, 
   action: newsStatusAction.NewsStatusAction): State {
switch (action.type) {
    case newsStatusAction.ActionTypes.NEWS_STATUS: {
    const newsStatusPayload: boolean = action.payload;
    return Object.assign({}, state, {
                newsStatus: newsStatusPayload
            });
        }
        default: {
            return state;
        }
    }
}

 

I would not be getting into each line of code here, only the important portions would be discussed here.

Creating news-org to store the names of news organizations

A new file news-org would be created inside app/shared/ folder containing list of news organizations in an array newsOrgs from which we would extract and filter results.

export const newsOrgs = [
    ‘CNN’,
    ‘nytimes’
];

 

Note: This is a dynamic implementation of news service and hence, the list of news organizations can be easily modified/updated here.

We would also need to export the newsOrgs in the index file inside shared/ folder as:

export { newsOrgs } from ‘./news-org’;

Creating ngrx action, reducer & effect for news success

Now comes the main part. We would need an action to dispatch on successful response of news search and corresponding reducer to keep appending the async news results.

News action would be created following the simple ngrx composition as:

import { Action } from ‘@ngrx/store’;
import { ApiResponse } from ‘../models/api-response’;
export const ActionTypes = {
    NEWS_SEARCH_SUCCESS: ‘[NEWS] SEARCH SUCCESS’
};
export class NewsSearchSuccessAction implements Action {
    type = ActionTypes.NEWS_SEARCH_SUCCESS;
    constructor (public payload: ApiResponse) { }
}
export type Actions
    = NewsSearchSuccessAction;

 

One of the important parts of news implementation is the reducer function corresponding to the news success action. We would actually append the async results provided by news effect into the store.

import * as newsSuccessAction from ‘../actions/newsSuccess’;
import { ApiResponseResult } from ‘../models’;

export interface State {
newsResponse: ApiResponseResult[];
}
export const initialState: State = {
newsResponse: [],
};
export function reducer(state: State = initialState,
   action: newsSuccessAction.NewsSearchSuccessAction): State {
    switch (action.type) {
        case newsSuccessAction.ActionTypes.NEWS_SEARCH_SUCCESS: {
            // Appending the news result
            return Object.assign({}, state, {
                newsResponse: […action.payload.statuses]
            });
        }
        default: {
            return state;
        }
    }
}
export const getNewsResponse = (state: State) => state.newsResponse;

 

Now, we would create an effect which would actually use the search service to fetch results for the list of organizations one by one will keep dispatching a corresponding action to append the response into the store to be displayed in news feed. Instead of showing whole code for the effect, only the main logic is being displayed.

import { newsOrgs } from ‘../shared/news-org’;
...
@Injectable()
export class DisplayNewsEffects {
    @Effect({ dispatch: false })
        searchNews$: Observable<void> = this.actions$
        .pipe(
        ofType(
        newsAction.ActionTypes.NEWS_STATUS
        ),
        map((action) => {
            if (action[‘payload’]) {
                const orgs = newsOrgs;
                orgs.forEach((org) => {
                const searchServiceConfig:
                    SearchServiceConfig = 
                    new SearchServiceConfig();
                this.apiSearchService
                .fetchQuery(‘from:’
                  + org, searchServiceConfig)
                    .subscribe(response =>
                this.store$.dispatch(
                     new newsStatusAction
                     .NewsSearchSuccessAction(response)));
                });
            }
        })
    );

Filtering the news response inside news component

The basic idea is to filter the results from news response which contain the query and display them using news template on click of news tab on results page of loklak.

import { Query } from ‘../../models/query’;
import { Observable } from ‘rxjs’;
...
public query: string;
public query$: Observable<Query>;
...
ngOnInit() {
   const texts = [];
   this.store.select(fromRoot.getQuery).subscribe(res => 
       this.query = res.displayString);
   this.query$ = this.store.select(fromRoot.getQuery);
   this.store.select(fromRoot.getNewsResponse).subscribe(
       v => {
       for ( let i = 0; i < v.length; i++ ) {
           this.newsResponse.push(v[i]);
           if (v[i][‘text’].includes(this.query
               .replace(/\s/g, ”).toLowerCase())) {
               if (!texts.includes(v[i][‘text’]
                   .replace(/\s/g, ”).toLowerCase())) {
                   texts.push(v[i][‘text’]
                       .replace(/\s/g, ”).toLowerCase());
                   this.newsResponse.push(v[i]);
               }
           }
       }
   });
}

 

In above configuration, firstly we needed the current query which we extracted from ngrx store and then checking whether the text of news results contain the query string or not. If the query is present inside the text, we would add it into an array which would be used to display news feed using feed-card.

Displaying the news response

Displaying the news response is very easy, we just need to pass the news response items with index inside feed-card component which would display the news feed similar to the normal feed under the news section.

<div class=“wrapper feed-results”>
   <div *ngFor=“let item of newsResponse; let i = index “>
     <feed-card [feedItem]=“item” [feedIndex]=“i”></feed-card>
   </div>
</div>

Creating News tab to display news feed

To provide a tab to user to access news feed, we would need to create a News tab along with the other searching tools inside feed-advanced-search component as:

<button [class.selected]=“selectedTab === ‘news'” class=“tab” value=“news”
   (click)=“getFilterResults($event.currentTarget.value)”>
   News
</button>

 

Note: Complete code for news implementation can be found here and the news filter can be found here.

Testing news feature

Search a query on loklak, and then click on news tab to get the latest relevant news matching the query.

Resources

Continue Reading

Adding Unit Test for Reducer in loklak search

Ngrx/store components are an integral part of the loklak search. All the components are dependent on how the data is received from the reducers. Reducer is like a client-side database which stores up all the data received from the API response. It is responsible for changing the state of the application. Reducers also supplies data to the Angular components from the central Store. If correct data is not received by the components, the application would crash. Therefore, we need to test if the reducer is storing the data in a correct way and changes the state of the application as expected.

Reducer also stores the current state properties which are fetched from the APIs. We need to check if reducers store the data in a correct way and if the data is received from the reducer when called from the angular components.

In this blog, I would explain how to build up different components for unit testing reducers.

Reducer to test

This reducer is used for store the data from suggest.json API from the loklak server.The data received from the server is further classified into three properties which can be used by the components to show up auto- suggestions related to current search query.

  • metadata: – This property stores the metadata from API suggestion response.
  • entities: – This property stores the array of suggestions corresponding to the particular query received from the server.
  • valid: – This is a boolean which keeps a check if the suggestions are valid or not.

We also have two actions corresponding to this reducer. These actions, when called, changes the state properties which , further, supplies data to the components in a more classified manner. Moreover, state properties also causes change in the UI of the component according to the action dispatched.

  • SUGGEST_COMPLETE_SUCCESS: – This action is called when the data is received successfully from the server.
  • SUGGEST_COMPLETE_FAIL: – This action is called when the retrieving data from the server fails.

export interface State {
metadata: SuggestMetadata;
entities: SuggestResults[];
valid: boolean;
}export const initialState: State = {
metadata: null,
entities: [],
valid: true
};export function reducer(state: State = initialState, action: suggestAction.Actions): State {
switch (action.type) {
case suggestAction.ActionTypes.SUGGEST_COMPLETE_SUCCESS: {
const suggestResponse = action.payload;return {
metadata: suggestResponse.suggest_metadata,
entities: suggestResponse.queries,
valid: true
};
}case suggestAction.ActionTypes.SUGGEST_COMPLETE_FAIL: {
return Object.assign({}, state, {
valid: false
});
}default: {
return state;
}
}
}

Unit tests for reducers

  • Import all the actions, reducers and mocks

import * as fromSuggestionResponse from ‘./suggest-response’;
import * as suggestAction from ‘../actions/suggest’;
import { SuggestResponse } from ‘../models/api-suggest’;
import { MockSuggestResponse } from ‘../shared/mocks/suggestResponse.mock’;

 

  • Next, we are going to test if the undefined action doesn’t a cause change in the state and returns the initial state properties. We will be creating an action by const action = {} as any;  and call the reducer by const result = fromSuggestionResponse.reducer(undefined, action);. Now we will be making assertions with expect() block to check if the result is equal to initialState and all the initial state properties are returned

describe(‘SuggestReducer’, () => {
describe(‘undefined action’, () => {
it(‘should return the default state’, () => {
const action = {} as any;const result = fromSuggestionResponse.reducer(undefined, action);
expect(result).toEqual(fromSuggestionResponse.initialState);
});
});

 

  • Now, we are going to test SUGGEST_COMPLETE_SUCCESS and SUGGEST_COMPLETE_FAIL action and check if reducers change only the assigned state properties corresponding to the action in a correct way.  Here, we will be creating action as assigned to the const action variable in the code below. Our next step would be to create a new state object with expected new state properties as assigned to variable const expectedResult below. Now, we would be calling reducer and make an assertion if the individual state properties of the result returned from the reducer (by calling reducer) is equal to the state properties of the expectedResult (Mock state result created to test).

describe(‘SUGGEST_COMPLETE_SUCCESS’, () => {
it(‘should add suggest response to the state’, () => {
const ResponseAction = new suggestAction.SuggestCompleteSuccessAction(MockSuggestResponse);
const expectedResult: fromSuggestionResponse.State = {
metadata: MockSuggestResponse.suggest_metadata,
entities: MockSuggestResponse.queries,
valid: true
};const result = fromSuggestionResponse.reducer(fromSuggestionResponse.initialState, ResponseAction);
expect(result).toEqual(expectedResult);
});
});describe(‘SUGGEST_COMPLETE_FAIL’, () => {
it(‘should set valid to true’, () => {
const action = new suggestAction.SuggestCompleteFailAction();
const result = fromSuggestionResponse.reducer(fromSuggestionResponse.initialState, action);expect(result.valid).toBe(false);
});
});

Reference

Continue Reading
Close Menu