From e25831d7a86db06b4a1d283980201f8c3a57763e Mon Sep 17 00:00:00 2001 From: shprink Date: Fri, 23 Jun 2017 17:57:27 +0200 Subject: [PATCH] more --- package.json | 2 +- server/index.js | 28 ++++++ src/actions/index.ts | 1 + src/actions/tweet.ts | 26 ++++++ src/app/app.component.ts | 2 +- src/app/app.module.ts | 2 + src/components/feed/feed.ts | 7 +- src/components/og/og.ts | 2 +- src/components/tweet/tweet.html | 19 +++- src/components/tweet/tweet.scss | 21 +++++ src/components/tweet/tweet.ts | 19 +++- src/pages/feed/feed.html | 1 - src/pages/feed/feed.ts | 1 + src/pages/home/home.html | 2 +- src/pages/mentions/mentions.ts | 2 +- src/providers/feed/feed.ts | 47 +++++----- src/providers/index.ts | 1 + src/providers/mentions/mentions.ts | 49 +++++----- src/providers/storage/storage.ts | 47 +++++----- src/providers/tweet/tweet.ts | 39 ++++++++ src/providers/twitter/twitter.ts | 13 +++ src/reducers/feed.ts | 86 +----------------- src/reducers/index.ts | 4 + src/reducers/mentions.ts | 30 +------ src/reducers/tweets.ts | 140 +++++++++++++++++++++++++++++ src/theme/ionicons-icons.scss | 8 +- src/utils/feed.ts | 12 --- tsconfig.json | 2 +- yarn.lock | 6 +- 29 files changed, 407 insertions(+), 212 deletions(-) create mode 100644 src/actions/tweet.ts create mode 100644 src/providers/tweet/tweet.ts create mode 100644 src/reducers/tweets.ts delete mode 100644 src/utils/feed.ts diff --git a/package.json b/package.json index 4aaac00..34ce8e1 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "angularfire2": "^4.0.0-rc.0", "date-fns": "^1.28.5", "firebase": "^4.0.0", - "ionic-angular": "^3.4.2", + "ionic-angular": "nightly", "ionicons": "3.0.0", "lodash": "^4.17.4", "rxjs": "5.1.1", diff --git a/server/index.js b/server/index.js index 18b04fc..d7a831b 100644 --- a/server/index.js +++ b/server/index.js @@ -103,6 +103,20 @@ app.post('/api/tweet', (req, res) => { }); }); +app.post('/api/retweet', (req, res) => { + var client = getTwitterClient(req); + client.post(`statuses/retweet/${req.body.id}`, Object.assign({ trim_user: false }, req.body || {}), function (error, body, response) { + (!error) ? res.status(200).json(body) : res.status(400).json(error); + }); +}); + +app.post('/api/unretweet', (req, res) => { + var client = getTwitterClient(req); + client.post(`statuses/unretweet/${req.body.id}`, Object.assign({ trim_user: false }, req.body || {}), function (error, body, response) { + (!error) ? res.status(200).json(body) : res.status(400).json(error); + }); +}); + app.post('/api/mentions', (req, res) => { var client = getTwitterClient(req); client.get('statuses/mentions_timeline', req.body, function (error, body, response) { @@ -110,6 +124,20 @@ app.post('/api/mentions', (req, res) => { }); }); +app.post('/api/favorite', (req, res) => { + var client = getTwitterClient(req); + client.post('favorites/create', Object.assign({ include_entities: true }, req.body || {}), function (error, body, response) { + (!error) ? res.status(200).json(body) : res.status(400).json(error); + }); +}); + +app.post('/api/unfavorite', (req, res) => { + var client = getTwitterClient(req); + client.post('favorites/destroy', Object.assign({ include_entities: true }, req.body || {}), function (error, body, response) { + (!error) ? res.status(200).json(body) : res.status(400).json(error); + }); +}); + app.post('/api/messages', (req, res) => { var client = getTwitterClient(req); client.get('direct_messages', req.body, function (error, body, response) { diff --git a/src/actions/index.ts b/src/actions/index.ts index 65f1953..d4ad05a 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -6,3 +6,4 @@ export * from './users'; export * from './feed'; export * from './trends'; export * from './mentions'; +export * from './tweet'; diff --git a/src/actions/tweet.ts b/src/actions/tweet.ts new file mode 100644 index 0000000..5240c93 --- /dev/null +++ b/src/actions/tweet.ts @@ -0,0 +1,26 @@ +import { Action } from '@ngrx/store'; + +export const TWEET_RETWEET = 'TWEET_RETWEET'; +export const TWEET_UNRETWEET = 'TWEET_UNRETWEET'; +export const TWEET_FAVORITE = 'TWEET_FAVORITE'; +export const TWEET_UNFAVORITE = 'TWEET_UNFAVORITE'; + +export const tweetRetweet = (tweet, id): Action => ({ + type: TWEET_RETWEET, + payload: { tweet, id } +}); + +export const tweetUnretweet = (tweet, id): Action => ({ + type: TWEET_UNRETWEET, + payload: { tweet, id } +}); + +export const tweetFavorite = (tweet): Action => ({ + type: TWEET_FAVORITE, + payload: { tweet } +}); + +export const tweetUnfavorite = (tweet): Action => ({ + type: TWEET_UNFAVORITE, + payload: { tweet } +}); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a29ec56..da9af1b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -29,7 +29,7 @@ export class MyApp { if (this.previousAuthState !== isAuthenticated) { console.log('isAuthenticated', isAuthenticated, ) - if (isAuthenticated && !location.hash.includes('home')) { + if (isAuthenticated && !location.hash.includes('login')) { this.nav.setRoot('HomePage'); } else if (!isAuthenticated && !location.hash.includes('login')) { this.nav.setRoot('LoginPage'); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 843a606..882ede6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -37,6 +37,7 @@ import { ServiceWorkerProvider, SearchProvider, MentionsProvider, + TweetProvider, } from '../providers'; import { MenuComponentModule } from '../components/menu/menu.module'; import { HttpWrapper } from './http.wrapper'; @@ -106,6 +107,7 @@ export function provideHttp( ServiceWorkerProvider, SearchProvider, MentionsProvider, + TweetProvider, ], }) export class AppModule {} diff --git a/src/components/feed/feed.ts b/src/components/feed/feed.ts index 5389a47..7bae932 100644 --- a/src/components/feed/feed.ts +++ b/src/components/feed/feed.ts @@ -1,7 +1,7 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; import { InfiniteScroll, Refresher } from 'ionic-angular'; -import { ITweet } from './../../reducers/feed'; +import { ITweet } from './../../reducers'; /** * Generated class for the FeedComponent component. * @@ -11,6 +11,7 @@ import { ITweet } from './../../reducers/feed'; @Component({ selector: 'feed', templateUrl: 'feed.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FeedComponent { @Input() content: ITweet[]; @@ -34,6 +35,6 @@ export class FeedComponent { } trackById(index, item) { - return item.id; + return item.id_str; } } diff --git a/src/components/og/og.ts b/src/components/og/og.ts index 00f982a..f559e24 100644 --- a/src/components/og/og.ts +++ b/src/components/og/og.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; -import { ITweetEntitiesUrl } from './../../reducers/feed'; +import { ITweetEntitiesUrl } from './../../reducers'; /** * Generated class for the OgComponent component. * diff --git a/src/components/tweet/tweet.html b/src/components/tweet/tweet.html index 5f0acbf..34b1a3c 100644 --- a/src/components/tweet/tweet.html +++ b/src/components/tweet/tweet.html @@ -2,8 +2,23 @@

{{data.user.name}} @{{data.user.screen_name}}

-

-

+

+

+ +

+
+ + + +
\ No newline at end of file diff --git a/src/components/tweet/tweet.scss b/src/components/tweet/tweet.scss index 2349184..1cb72e3 100644 --- a/src/components/tweet/tweet.scss +++ b/src/components/tweet/tweet.scss @@ -1,6 +1,10 @@ tweet { .item-inner { padding: 10px 0; + ion-label{ + overflow: hidden; + flex:1; + } } avatar{ margin: 0 !important; @@ -8,4 +12,21 @@ tweet { ion-item { align-items: baseline !important; } + .actions{ + margin-top: 10px; + display: flex; + flex-direction: row; + justify-content: space-between; + button { + margin: 0; + font-size: 1.7rem !important; + color: color($colors, gray); + &.retweet.active { + color: color($colors, secondary) + } + &.favorite.active { + color: color($colors, danger) + } + } + } } diff --git a/src/components/tweet/tweet.ts b/src/components/tweet/tweet.ts index 0d65d6f..9f929bc 100644 --- a/src/components/tweet/tweet.ts +++ b/src/components/tweet/tweet.ts @@ -1,8 +1,10 @@ import { Component, Input } from '@angular/core'; import { App } from 'ionic-angular'; +import { autoLinkWithJSON } from 'twitter-text'; import _get from 'lodash/get'; import { ITweet, ITweetEntitiesMedia } from './../../reducers'; +import { TweetProvider } from './../../providers'; /** * Generated class for the TweetComponent component. * @@ -19,12 +21,19 @@ export class TweetComponent { @Input() data: ITweet; - constructor(public appCtrl: App) { + constructor( + public appCtrl: App, + public tweetProvider: TweetProvider, + ) { console.log('Hello TweetComponent Component'); } ngOnInit() { this.media = _get(this.data, 'entities.media[0]'); + this.text = autoLinkWithJSON(this.data.text, _get(this.data, 'entities'), { + hashtagUrlBase: `#/nav/${this.appCtrl.getRootNav().id}/search/%23`, + usernameUrlBase: `#/nav/${this.appCtrl.getRootNav().id}/profile/`, + }); } goToProfile = (id, handle) => { @@ -33,4 +42,12 @@ export class TweetComponent { handle: this.data.user.screen_name, }); }; + + retweet() { + return this.tweetProvider.retweet$(!this.data.retweeted, this.data.id_str).subscribe() + } + + favorite() { + return this.tweetProvider.favorite$(!this.data.favorited, this.data.id_str).subscribe() + } } diff --git a/src/pages/feed/feed.html b/src/pages/feed/feed.html index 5f2ae0f..0f4feaa 100644 --- a/src/pages/feed/feed.html +++ b/src/pages/feed/feed.html @@ -9,7 +9,6 @@ - (currentLength = items.length)); + console.log('loadMore', this.feedProvider.feedLength(), currentLength) if (this.feedProvider.feedLength() > currentLength) { this.nextPage(); infiniteScroll.complete(); diff --git a/src/pages/home/home.html b/src/pages/home/home.html index a5951e8..25dce25 100644 --- a/src/pages/home/home.html +++ b/src/pages/home/home.html @@ -9,6 +9,6 @@ - + \ No newline at end of file diff --git a/src/pages/mentions/mentions.ts b/src/pages/mentions/mentions.ts index d2ddf8d..ae2f144 100644 --- a/src/pages/mentions/mentions.ts +++ b/src/pages/mentions/mentions.ts @@ -39,7 +39,7 @@ export class MentionsPage { init() { const hasFeed = this.mentionsProvider.hasFeed(); if (!hasFeed) { - console.log('init') + console.log('hasFeed', hasFeed) this.mentionsProvider .fetch$() .first() diff --git a/src/providers/feed/feed.ts b/src/providers/feed/feed.ts index 2a64420..39af8f4 100644 --- a/src/providers/feed/feed.ts +++ b/src/providers/feed/feed.ts @@ -4,6 +4,7 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import _get from 'lodash/get'; import _take from 'lodash/take'; +import _without from 'lodash/without'; import { AppState, ITweet, IUsersState } from '../../reducers'; import { fetchFeed, fetchedFeed, errorFeed } from '../../actions'; @@ -27,39 +28,31 @@ export class FeedProvider { return this.store.select(state => state.feed.fetching); } - getFeed$(): Observable { - return Observable.combineLatest( - this.store.select(state => state.feed.list), - this.store.select(state => state.users), - (feed: ITweet[], users: IUsersState) => - feed.map(feedItem => { - feedItem.user = _get(users, `[${feedItem.userHandle}]`); - return feedItem; - }), - ); + getFeed$(): Observable { + return this.store.select(state => state.feed.list); } - getFeedPaginated$( - pageBSubject: BehaviorSubject, - perPage: number = 10, - ): Observable { + getFeedPaginated$(pageBSubject: BehaviorSubject, perPage: number = 10, ): Observable { return Observable.combineLatest( this.store.select(state => state.feed.list), + this.store.select(state => state.tweets), this.store.select(state => state.users), pageBSubject, - (feed: ITweet[], users: IUsersState, page) => - _take(feed, page * perPage).map(feedItem => { - feedItem.user = _get(users, `[${feedItem.userHandle}]`); - return feedItem; - }), + (feed: ITweet[], tweets: ITweet[], users: IUsersState, page) => _without(_take(feed, page * perPage) + .map(tweetId => { + const tweet = tweets[tweetId]; + if (!tweet) return null; + tweet.user = _get(users, `[${tweet.userHandle}]`); + return tweet; + }), null) ); } - getLastFeedItem(): ITweet { - let lastItem: ITweet; + getLastTweetId(): string { + let lastItem: string; this.getFeed$() .first() - .subscribe((items: ITweet[]) => (lastItem = items[items.length - 1])); + .subscribe((items: string[]) => (lastItem = items[items.length - 1])); return lastItem; } @@ -67,7 +60,7 @@ export class FeedProvider { let hasFeed: boolean; this.getFeed$() .first() - .subscribe((items: ITweet[]) => (hasFeed = items.length !== 0)); + .subscribe((items: string[]) => (hasFeed = items.length !== 0)); return hasFeed; } @@ -75,7 +68,7 @@ export class FeedProvider { let feedLength: number; this.getFeed$() .first() - .subscribe((items: ITweet[]) => (feedLength = items.length)); + .subscribe((items: string[]) => (feedLength = items.length)); return feedLength; } @@ -92,11 +85,11 @@ export class FeedProvider { } fetchNextPage$() { - const lastItem = this.getLastFeedItem(); - if (!lastItem) return Observable.of(null); + const lastTweetId = this.getLastTweetId(); + if (!lastTweetId) return Observable.of(null); this.store.dispatch(fetchFeed()); return this.twitterProvider - .getFeed$({ max_id: lastItem.id, include_entities: true }) + .getFeed$({ max_id: lastTweetId, include_entities: true }) .debounceTime(500) .map(feed => this.store.dispatch(fetchedFeed(feed))) .catch(error => { diff --git a/src/providers/index.ts b/src/providers/index.ts index d73938c..0ebd67b 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -6,4 +6,5 @@ export * from './auth/auth'; export * from './trends/trends'; export * from './search/search'; export * from './mentions/mentions'; +export * from './tweet/tweet'; export * from './service-worker/service-worker'; \ No newline at end of file diff --git a/src/providers/mentions/mentions.ts b/src/providers/mentions/mentions.ts index b13a954..33ab37a 100644 --- a/src/providers/mentions/mentions.ts +++ b/src/providers/mentions/mentions.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs/Observable'; import { Store } from '@ngrx/store'; import _get from 'lodash/get'; import _take from 'lodash/take'; +import _without from 'lodash/without'; import { AppState, ITweet, IUsersState } from '../../reducers'; import { fetchMentions, fetchedMentions, errorMentions } from '../../actions'; @@ -28,39 +29,33 @@ export class MentionsProvider { return this.store.select(state => state.mentions.fetching); } - getFeed$(): Observable { - return Observable.combineLatest( - this.store.select(state => state.mentions.list), - this.store.select(state => state.users), - (feed: ITweet[], users: IUsersState) => - feed.map(feedItem => { - feedItem.user = _get(users, `[${feedItem.userHandle}]`); - return feedItem; - }), - ); + getFeed$(): Observable { + return this.store.select(state => state.mentions.list); } - getMentionsPaginated$( - pageBSubject: BehaviorSubject, - perPage: number = 10, - ): Observable { + getMentionsPaginated$(pageBSubject: BehaviorSubject, perPage: number = 10, ): Observable { return Observable.combineLatest( this.store.select(state => state.mentions.list), + this.store.select(state => state.tweets), this.store.select(state => state.users), pageBSubject, - (feed: ITweet[], users: IUsersState, page) => - _take(feed, page * perPage).map(feedItem => { - feedItem.user = _get(users, `[${feedItem.userHandle}]`); - return feedItem; - }), + (feed: ITweet[], tweets: ITweet[], users: IUsersState, page) => _without(_take(feed, page * perPage) + .map(tweetId => { + const tweet = tweets[tweetId]; + if (!tweet) return null; + return { + ...tweet, // avoid mutation + user: _get(users, `[${tweet.userHandle}]`) + }; + }), null) ); } - getLastFeedItem(): ITweet { - let lastItem: ITweet; + getLastTweetId(): string { + let lastItem: string; this.getFeed$() .first() - .subscribe((items: ITweet[]) => (lastItem = items[items.length - 1])); + .subscribe((items: string[]) => (lastItem = items[items.length - 1])); return lastItem; } @@ -68,7 +63,7 @@ export class MentionsProvider { let hasFeed: boolean; this.getFeed$() .first() - .subscribe((items: ITweet[]) => (hasFeed = items.length !== 0)); + .subscribe((items: string[]) => (hasFeed = items.length !== 0)); return hasFeed; } @@ -76,7 +71,7 @@ export class MentionsProvider { let feedLength: number; this.getFeed$() .first() - .subscribe((items: ITweet[]) => (feedLength = items.length)); + .subscribe((items: string[]) => (feedLength = items.length)); return feedLength; } @@ -93,11 +88,11 @@ export class MentionsProvider { } fetchNextPage$() { - const lastItem = this.getLastFeedItem(); - if (!lastItem) return Observable.of(null); + const lastTweetId = this.getLastTweetId(); + if (!lastTweetId) return Observable.of(null); this.store.dispatch(fetchMentions()); return this.twitterProvider - .getMentions$({ max_id: lastItem.id, include_entities: true }) + .getMentions$({ max_id: lastTweetId, include_entities: true }) .debounceTime(500) .map(feed => this.store.dispatch(fetchedMentions(feed))) .catch(error => { diff --git a/src/providers/storage/storage.ts b/src/providers/storage/storage.ts index 0fffc40..1411a56 100644 --- a/src/providers/storage/storage.ts +++ b/src/providers/storage/storage.ts @@ -1,8 +1,11 @@ import { Injectable } from '@angular/core'; import { Storage as IonicStorage } from '@ionic/storage'; import { Store } from '@ngrx/store'; +import _slice from 'lodash/slice'; +import _uniq from 'lodash/uniq'; +import _pickBy from 'lodash/pickBy'; -import { AppState, IAuthState, IFeed, IUsersState, ITrends, IMentions } from './../../reducers'; +import { AppState, IAuthState, IFeed, IUsersState, ITrends, IMentions, ITweet } from './../../reducers'; import { INIT, ON_BEFORE_UNLOAD } from './../../actions'; /* @@ -37,32 +40,36 @@ export class StorageProvider { this.storage.set('auth', auth); }); - this.store.select('feed').skip(1).debounceTime(500).subscribe((feed: IFeed) => { - console.log('saving feed'); - this.storage.set('feed', feed); - }); - - this.store.select('mentions').skip(1).debounceTime(500).subscribe((mentions: IMentions) => { - console.log('saving mentions'); - this.storage.set('mentions', mentions); - }); - - this.store.select('users').skip(1).debounceTime(500).subscribe((users: IUsersState) => { - console.log('saving users'); - this.storage.set('users', users); - }); - this.store.select('trends').skip(1).debounceTime(500).subscribe((trends: ITrends) => { console.log('saving trends'); this.storage.set('trends', trends); }); + this.store.select(state => state).skip(1).debounceTime(500).subscribe(this.cleanupAndSaveState); + window.onbeforeunload = () => { - // cleanup the local storage when closing the app - // to have a instant load on next bootstrap this.store.dispatch({ type: ON_BEFORE_UNLOAD }); - this.store.select('feed').first().subscribe((feed: IFeed) => this.storage.set('feed', feed)); - this.store.select('mentions').first().subscribe((mentions: IMentions) => this.storage.set('mentions', mentions)); }; } + + cleanupAndSaveState = (state: AppState) => { + console.log('saving state'); + // FEED + const first20Feed = _slice(state.feed.list, 0, 20); + this.storage.set('feed', { fetching: false, list: first20Feed }); + + // MENTION + const first20Mentions = _slice(state.mentions.list, 0, 20); + this.storage.set('mentions', { fetching: false, list: first20Mentions }); + + // TWEETS + const tweetIdsToKeep = _uniq([...first20Feed, ...first20Mentions]); + const tweetsToKeep = _pickBy(state.tweets, (v, k) => tweetIdsToKeep.includes(k)); + this.storage.set('tweets', tweetsToKeep); + + // USERS + const userHandlesToKeep = Object.values(tweetsToKeep).map(tweet => tweet.userHandle); + const usersToKeep = _pickBy(state.users, (v, k) => userHandlesToKeep.includes(k)); + this.storage.set('users', usersToKeep); + } } \ No newline at end of file diff --git a/src/providers/tweet/tweet.ts b/src/providers/tweet/tweet.ts new file mode 100644 index 0000000..7f83e77 --- /dev/null +++ b/src/providers/tweet/tweet.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { AppState } from '../../reducers'; +import { + tweetFavorite, tweetUnfavorite, + tweetRetweet, tweetUnretweet +} from '../../actions'; +import { TwitterProvider } from './../twitter/twitter'; + +/* + Generated class for the TweetProvider provider. + + See https://angular.io/docs/ts/latest/guide/dependency-injection.html + for more info on providers and Angular DI. +*/ +@Injectable() +export class TweetProvider { + + constructor( + public store: Store, + private twitterProvider: TwitterProvider, + ) { + console.log('Hello TweetProvider Provider'); + } + + favorite$(handleDo, id) { + return this.twitterProvider.favorite$(handleDo, id) + .debounceTime(500) + .map(tweet => this.store.dispatch(handleDo ? tweetFavorite(tweet) : tweetUnfavorite(tweet))); + } + + retweet$(handleDo, id) { + return this.twitterProvider.retweet$(handleDo, id) + .debounceTime(500) + .map(tweet => this.store.dispatch(handleDo ? tweetRetweet(tweet, id) : tweetUnretweet(tweet, id))); + } + +} diff --git a/src/providers/twitter/twitter.ts b/src/providers/twitter/twitter.ts index b5aedcb..ecf5a12 100644 --- a/src/providers/twitter/twitter.ts +++ b/src/providers/twitter/twitter.ts @@ -116,4 +116,17 @@ export class TwitterProvider { : Observable.throw(authRequiredError); } + favorite$(handleDo: boolean = false, id): Observable { + return this.authProvider.isAuthenticated() + ? this.http.post(`${__APIURI__}api/${handleDo ? 'favorite' : 'unfavorite'}`, { id }, + this.getRequestOptions()).map(res => res.json()) + : Observable.throw(authRequiredError); + } + + retweet$(handleDo: boolean = false, id): Observable { + return this.authProvider.isAuthenticated() + ? this.http.post(`${__APIURI__}api/${handleDo ? 'retweet' : 'unretweet'}`, { id }, + this.getRequestOptions()).map(res => res.json()) + : Observable.throw(authRequiredError); + } } diff --git a/src/reducers/feed.ts b/src/reducers/feed.ts index 0edc362..af2abf5 100644 --- a/src/reducers/feed.ts +++ b/src/reducers/feed.ts @@ -1,29 +1,12 @@ import { ActionReducer, Action } from '@ngrx/store'; -import _slice from 'lodash/slice'; -import { ON_BEFORE_UNLOAD, FEED_FETCH, FEED_FETCHED, FEED_ERROR, LOGOUT, INIT } from '../actions'; -import { ITwitterUser } from './users'; -import { filterFeedList } from '../utils/feed'; +import { FEED_FETCH, FEED_FETCHED, FEED_ERROR, LOGOUT, INIT } from '../actions'; const defaultState = { fetching: false, list: [], }; -const propertiesToKeep: string[] = [ - 'id', - 'id_str', - 'created_at', - 'text', - 'truncated', - 'user', - 'favorite_count', - 'favorited', - 'retweet_count', - 'retweeted', - 'entities', -]; - export const feedReducer: ActionReducer = ( state: IFeed = defaultState, action: Action, @@ -40,7 +23,7 @@ export const feedReducer: ActionReducer = ( } case FEED_FETCHED: { - const newItems = filterFeedList(payload.feed, propertiesToKeep); + const newItems = payload.feed.map(item => item.id_str); return { fetching: false, list: payload.reset ? newItems : [...state.list, ...newItems], @@ -54,78 +37,17 @@ export const feedReducer: ActionReducer = ( fetching: false }; } - + case LOGOUT: { return defaultState; } - case ON_BEFORE_UNLOAD: { - return { - fetching: false, - list: _slice(state.list, 0, 20), - }; - } - default: return state; } }; -// https://dev.twitter.com/overview/api/entities-in-twitter-objects -export interface ITweetEntities { - hashtags: ITweetEntitiesHashtag[]; - symbols: ITweetEntitiesSymbol[]; - url: ITweetEntitiesUrl[]; - media: ITweetEntitiesMedia[]; - user_mentions: ITweetEntitiesMention[]; -} - -export interface ITweetEntitiesHashtag { - text: string; - indices: number[]; -} - -export interface ITweetEntitiesSymbol { - text: string; - indices: number[]; -} - -export interface ITweetEntitiesUrl { - display_url: string; - expanded_url: string; - url: string; - indices: number[]; -} - -export interface ITweetEntitiesMedia { - type: string; - media_url_https: string; -} - -export interface ITweetEntitiesMention { - id: number; - id_str: string; - name: string; - screen_name: string; - indices: number[]; -} - -export interface ITweet { - id: number; - id_str: string; - created_at: string; - text: string; - truncated: boolean; - favorited: boolean; - favorite_count: number; - retweeted: boolean; - retweet_count: number; - userHandle?: number; - user?: ITwitterUser; - entities: ITweetEntities; -} - export interface IFeed { fetching: boolean; - list: ITweet[]; + list: string[]; } diff --git a/src/reducers/index.ts b/src/reducers/index.ts index a370147..2b6e081 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -3,12 +3,14 @@ import { usersReducer, IUsersState } from './users'; import { feedReducer, IFeed } from './feed'; import { trendsReducer, ITrends } from './trends'; import { mentionsReducer, IMentions } from './mentions'; +import { tweetsReducer, ITweets } from './tweets'; export * from './auth'; export * from './users'; export * from './feed'; export * from './trends'; export * from './mentions'; +export * from './tweets'; export interface AppState { auth: IAuthState; @@ -16,6 +18,7 @@ export interface AppState { feed: IFeed; trends: ITrends; mentions: IMentions; + tweets: ITweets } export const Reducers = { @@ -24,4 +27,5 @@ export const Reducers = { feed: feedReducer, trends: trendsReducer, mentions: mentionsReducer, + tweets: tweetsReducer, } \ No newline at end of file diff --git a/src/reducers/mentions.ts b/src/reducers/mentions.ts index b4f8e3e..f79b63b 100644 --- a/src/reducers/mentions.ts +++ b/src/reducers/mentions.ts @@ -1,29 +1,12 @@ import { ActionReducer, Action } from '@ngrx/store'; -import _slice from 'lodash/slice'; -import { ON_BEFORE_UNLOAD, MENTIONS_FETCH, MENTIONS_FETCHED, MENTIONS_ERROR, LOGOUT, INIT } from '../actions'; -import { filterFeedList } from '../utils/feed'; -import { ITweet } from './feed'; +import { MENTIONS_FETCH, MENTIONS_FETCHED, MENTIONS_ERROR, LOGOUT, INIT } from '../actions'; const defaultState = { fetching: false, list: [], }; -const propertiesToKeep: string[] = [ - 'id', - 'id_str', - 'created_at', - 'text', - 'truncated', - 'user', - 'favorite_count', - 'favorited', - 'retweet_count', - 'retweeted', - 'entities', -]; - export const mentionsReducer: ActionReducer = (state: IMentions = defaultState, action: Action) => { const payload = action.payload; @@ -37,7 +20,7 @@ export const mentionsReducer: ActionReducer = (state: IMentions = defaul } case MENTIONS_FETCHED: { - const newItems = filterFeedList(payload.feed, propertiesToKeep); + const newItems = payload.feed.map(item => item.id_str); return { fetching: false, list: payload.reset ? newItems : [...state.list, ...newItems], @@ -56,13 +39,6 @@ export const mentionsReducer: ActionReducer = (state: IMentions = defaul return defaultState; } - case ON_BEFORE_UNLOAD: { - return { - fetching: false, - list: _slice(state.list, 0, 20), - }; - } - default: return state; } @@ -70,5 +46,5 @@ export const mentionsReducer: ActionReducer = (state: IMentions = defaul export interface IMentions { fetching: boolean; - list: ITweet[]; + list: string[]; } diff --git a/src/reducers/tweets.ts b/src/reducers/tweets.ts new file mode 100644 index 0000000..2d54442 --- /dev/null +++ b/src/reducers/tweets.ts @@ -0,0 +1,140 @@ +import { ActionReducer, Action } from '@ngrx/store'; +import _pickBy from 'lodash/pickBy'; + +import { + MENTIONS_FETCHED, FEED_FETCHED, LOGOUT, INIT, + TWEET_RETWEET, TWEET_UNRETWEET, TWEET_FAVORITE, TWEET_UNFAVORITE +} from '../actions'; +import { ITwitterUser } from './users'; + +const defaultState = {}; + +const propertiesToKeep: string[] = [ + 'id', + 'id_str', + 'created_at', + 'text', + 'truncated', + 'user', + 'favorite_count', + 'favorited', + 'retweet_count', + 'retweeted', + 'entities', +]; + +export const tweetsReducer: ActionReducer = (state: ITweets = defaultState, action: Action, ) => { + const payload = action.payload; + + switch (action.type) { + case MENTIONS_FETCHED: + case FEED_FETCHED: { + return { ...state, ...filterTweetList(payload.feed, propertiesToKeep) }; + } + + case TWEET_RETWEET: { + return { + ...state, + [payload.id]: { + ...state[payload.id], + retweeted: true + } + }; + } + + case TWEET_UNRETWEET: { + return { + ...state, + [payload.id]: { + ...state[payload.id], + retweeted: false + } + }; + } + + case TWEET_FAVORITE: + case TWEET_UNFAVORITE: { + return { ...state, ...filterTweetList([payload.tweet], propertiesToKeep) }; + } + + case INIT: { + return payload.tweets || state; + } + + case LOGOUT: { + return defaultState; + } + + default: + return state; + } +}; + +export function filterTweetList(list = [], propertiesToKeep = []) { + const feedItems = {}; + list.forEach(item => { + let feedItem = _pickBy(item, (v, k) => propertiesToKeep.includes(k)); + feedItem.userHandle = feedItem.user.screen_name; + delete feedItem.user; + feedItems[feedItem.id_str] = feedItem; + }); + return feedItems; +} + +// https://dev.twitter.com/overview/api/entities-in-twitter-objects +export interface ITweetEntities { + hashtags: ITweetEntitiesHashtag[]; + symbols: ITweetEntitiesSymbol[]; + url: ITweetEntitiesUrl[]; + media: ITweetEntitiesMedia[]; + user_mentions: ITweetEntitiesMention[]; +} + +export interface ITweetEntitiesHashtag { + text: string; + indices: number[]; +} + +export interface ITweetEntitiesSymbol { + text: string; + indices: number[]; +} + +export interface ITweetEntitiesUrl { + display_url: string; + expanded_url: string; + url: string; + indices: number[]; +} + +export interface ITweetEntitiesMedia { + type: string; + media_url_https: string; +} + +export interface ITweetEntitiesMention { + id: number; + id_str: string; + name: string; + screen_name: string; + indices: number[]; +} + +export interface ITweet { + id: number; + id_str: string; + created_at: string; + text: string; + truncated: boolean; + favorited: boolean; + favorite_count: number; + retweeted: boolean; + retweet_count: number; + userHandle?: number; + user?: ITwitterUser; + entities: ITweetEntities; +} + +export interface ITweets { + [key: string]: ITweet +} \ No newline at end of file diff --git a/src/theme/ionicons-icons.scss b/src/theme/ionicons-icons.scss index f4667bd..6d06097 100644 --- a/src/theme/ionicons-icons.scss +++ b/src/theme/ionicons-icons.scss @@ -40,6 +40,9 @@ .ion-md-arrow-back:before { content: "\f27d"; } .ion-md-pin:before { content: "\f34a"; } .ion-md-link:before { content: "\f22e"; } +.ion-md-repeat:before { content: "\f36a"; } +.ion-md-heart:before { content: "\f308"; } +.ion-md-mail:before { content: "\f322"; } .ion-md-home:before, .ion-md-search:before, @@ -49,7 +52,10 @@ .ion-md-person:before, .ion-md-arrow-back:before, .ion-md-pin:before, -.ion-md-link:before +.ion-md-link:before, +.ion-md-repeat:before, +.ion-md-heart:before, +.ion-md-mail:before { @extend .ion } diff --git a/src/utils/feed.ts b/src/utils/feed.ts deleted file mode 100644 index 059209b..0000000 --- a/src/utils/feed.ts +++ /dev/null @@ -1,12 +0,0 @@ -import _pickBy from 'lodash/pickBy'; - -export function filterFeedList(list = [], propertiesToKeep = []) { - const feedItems = []; - list.forEach(item => { - let feedItem = _pickBy(item, (v, k) => propertiesToKeep.includes(k)); - feedItem.userHandle = feedItem.user.screen_name; - delete feedItem.user; - feedItems.push(feedItem); - }); - return feedItems; -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f845711..428811f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "experimentalDecorators": true, "lib": [ "dom", - "es2016" + "es2017" ], "module": "es2015", "moduleResolution": "node", diff --git a/yarn.lock b/yarn.lock index 0ad3859..8054bfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2094,9 +2094,9 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" -ionic-angular@^3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/ionic-angular/-/ionic-angular-3.4.2.tgz#762631f1af78a5ae1c0aa0f4d23b31435142abe1" +ionic-angular@nightly: + version "3.4.2-201706201953" + resolved "https://registry.yarnpkg.com/ionic-angular/-/ionic-angular-3.4.2-201706201953.tgz#65d46c2f4aa57c2578809f9b809f70b9bfff3fe2" ionic@^3.4.0: version "3.4.0"