Skip to content

Commit 0b2645f

Browse files
committed
Added RaidPal Integration - Easily view your lineup and channel status when participating in raid trains!
Notable Changes - Added RaidPal tab with event selection and lineup information - Added follow status to the lineup, so you never need to be _that_ person who follows after raiding - Change Log - AppReducer.js: Added showAmPm preference - API.js: Also accept text/javascript as a json mimetype - Loading.js: Added children prop to display errors while loading (if a user has no followers, Twitch API will report 500 Internal Server Error) - Loading.scss: Fix alerts to the top of the UI - FollowerActions.js: Added actions for managing follower state - FollowerReducer.js: Added follower state - NotFollowingBadgeOfShame.js: Added for indicate that you are not following the given channel - RaidPalActions.js: Added actions for managing raidpal user and event state, and event streams - RaidPalReducer.js: Added raidpal user, event, and stream state - RaidPalView.js: Added RaidPal event / lineup view - RaidPalView.scss: Added RaidPal event page styling - StreamInfoModal.js: - Refactored to reusable component (was in StreamList) - Added fallbacks to show channel info when offline - StreamList.js: - Refactored selected stream modal to own component - Exported REFRESH_INTERVAL for use in the RP view - Added selectedStreamUserId for modal channel fallback - Added fallback to update selectedStream if the stream crashes and a new stream starts - Added error to loading view in the event the user has no followed channels (Twitch returns 500 ISE) - Added sad message when the user has no followed channels online - Added RaidPal view - StreamList.scss: Added styling for alert links, nav tweaks, and channel modal - reducers.js: Added raidpal and followers reducers to app state - package.json: Bumped to 2.0.0 and added react-show-more-text dependency
1 parent a33d876 commit 0b2645f

17 files changed

+1386
-197
lines changed

package-lock.json

Lines changed: 57 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dirty-raid",
3-
"version": "1.4.0",
3+
"version": "2.0.0",
44
"repository": {
55
"type": "git",
66
"url": "https://github.com/kfitzgerald/dirty-raid"
@@ -22,6 +22,7 @@
2222
"react-dom": "^18.2.0",
2323
"react-redux": "^8.0.5",
2424
"react-scripts": "5.0.1",
25+
"react-show-more-text": "^1.6.2",
2526
"redux-localstorage": "^0.4.1",
2627
"redux-thunk": "^2.4.2",
2728
"sass": "^1.60.0",

src/app/AppReducer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export const initialState = {
66
preferences: {
77
showTitles: true,
88
showTags: true,
9-
showProfileImg: true
9+
showProfileImg: true,
10+
showAmPm: true
1011
}
1112
};
1213

src/common/API.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function doApiCall(method, path, { payload, query, oauth, bearer, clientI
4444

4545
// Build a fetch request using the given params and options
4646
const headers = new Headers({
47-
'Accept': 'application/json' // we always want JSON
47+
'Accept': 'application/json,text/javascript' // we always want JSON
4848
});
4949

5050
const fetchArgs = {
@@ -90,7 +90,7 @@ export function doApiCall(method, path, { payload, query, oauth, bearer, clientI
9090
.then(res => {
9191
// Some API calls do not return JSON or a body at all - only JSON-decode when necessary
9292
const contentType = res.headers.get("Content-Type") || '';
93-
if (contentType.includes('json')) {
93+
if (contentType.includes('json') || contentType.includes('javascript')) {
9494
return res.json();
9595
} else {
9696
// no content - mock a response

src/common/Loading.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import './Loading.scss';
66
* @return {JSX.Element}
77
* @constructor
88
*/
9-
function Loading() {
9+
function Loading({ children }) {
1010
return (
1111
<div className="Loading">
1212
<header className="Loading-header">
1313
<img src={logo} className="Loading-logo" alt="logo" />
1414
</header>
15+
{children}
1516
</div>
1617
);
1718
}

src/common/Loading.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
.Loading {
22
text-align: center;
3+
4+
.alert {
5+
position: absolute;
6+
top: 0;
7+
width: 96vw;
8+
margin: 2vw;
9+
}
310
}
411

512
.Loading-logo {

src/followers/FollowerActions.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import {apiGet} from "../common/API";
2+
import {revokeToken} from "../session/SessionActions";
3+
4+
//region Twitch Get Followed Channels
5+
6+
export const REQUEST_FOLLOWED_CHANNELS = 'REQUEST_FOLLOWED_CHANNELS';
7+
export function requestFollowedChannels() {
8+
return {
9+
type: REQUEST_FOLLOWED_CHANNELS
10+
};
11+
}
12+
13+
export const RECEIVE_FOLLOWED_CHANNELS_SUCCESS = 'RECEIVE_FOLLOWED_CHANNELS_SUCCESS';
14+
export function requestFollowedChannelsSuccess(data) {
15+
return {
16+
type: RECEIVE_FOLLOWED_CHANNELS_SUCCESS,
17+
lastUpdated: Date.now(),
18+
data
19+
};
20+
}
21+
22+
export const RECEIVE_FOLLOWED_CHANNELS_ERROR = 'RECEIVE_FOLLOWED_CHANNELS_ERROR';
23+
export function receiveFollowedChannelsError(error) {
24+
return {
25+
type: RECEIVE_FOLLOWED_CHANNELS_ERROR,
26+
error
27+
};
28+
}
29+
30+
export const ENQUEUE_FOLLOWED_CHANNELS = 'ENQUEUE_FOLLOWED_CHANNELS';
31+
export function enqueueFollowedChannels(user_ids) {
32+
return {
33+
type: ENQUEUE_FOLLOWED_CHANNELS,
34+
user_ids
35+
};
36+
}
37+
38+
export function fetchFollowedChannels(broadcaster_ids) {
39+
return async (dispatch, getState) => {
40+
const { followers } = getState();
41+
const { cache, queue } = followers;
42+
43+
// only queue if user is not following this broadcaster and not cached
44+
const filteredIds = broadcaster_ids.filter(broadcaster_id => {
45+
return (!broadcaster_id || // fetch all
46+
!cache[broadcaster_id] || // not cached?
47+
!cache[broadcaster_id].followed_at) && // not followed
48+
queue.indexOf(broadcaster_id) < 0 // not queued
49+
;
50+
});
51+
52+
if (!filteredIds.length) return;
53+
54+
// Add to the queue
55+
dispatch(enqueueFollowedChannels(filteredIds));
56+
dispatch(_fetchFollowedChannels());
57+
};
58+
}
59+
60+
61+
function _fetchFollowedChannels(callback=() => {}) {
62+
return async (dispatch, getState) => {
63+
const { followers, session } = getState();
64+
const { queue, isFetching } = followers;
65+
66+
if (isFetching) return; // no dup fetching
67+
if (!session.data) return; // no auth, no request
68+
if (!queue.length) return; // no users to fetch
69+
70+
const { access_token } = session.token;
71+
const { user_id } = session.data;
72+
73+
// lock out dup fetch requests
74+
dispatch(requestFollowedChannels());
75+
76+
const pageSize = 100;
77+
let followList = [];
78+
let broadcaster_id;
79+
while (queue.length > 0) {
80+
81+
broadcaster_id = queue.shift();
82+
83+
let done = false, body, error, after, query;
84+
while (!done) { // ooo scary!
85+
86+
// Build query
87+
query = { user_id, first: pageSize, /*after: ...*/ };
88+
if (broadcaster_id) query.broadcaster_id = broadcaster_id;
89+
if (after) query.after = after;
90+
91+
// Execute request
92+
({ error, body } = await apiGet('https://api.twitch.tv/helix/channels/followed', { query, bearer: access_token})
93+
.then(body => {
94+
return { body, error: null };
95+
}, error => {
96+
return { error };
97+
})
98+
);
99+
100+
101+
// If the request failed, handle it
102+
if (error) {
103+
// If twitch auth fails - it's likely due to an expired token
104+
// Clear the session!
105+
if (error.status === 401) {
106+
dispatch(revokeToken());
107+
}
108+
109+
dispatch(receiveFollowedChannelsError(error));
110+
callback(error);
111+
dispatch(_fetchFollowedChannels());
112+
return;
113+
114+
} else {
115+
// Append the page to the results
116+
if (broadcaster_id && body.data.length === 0) {
117+
// create a stub record for users that are not following
118+
body.data = [
119+
{
120+
broadcaster_id,
121+
}
122+
];
123+
}
124+
followList = followList.concat(body.data);
125+
126+
// Check if there is another page to fetch
127+
after = body.pagination.cursor;
128+
done = !body.pagination.cursor;
129+
}
130+
}
131+
132+
}
133+
134+
// success - update the cache and release the fetch lock
135+
dispatch(requestFollowedChannelsSuccess(followList));
136+
callback(null, followList);
137+
138+
// do another follow up just in case the queue is still long
139+
dispatch(_fetchFollowedChannels());
140+
};
141+
}
142+
143+
//endregion

0 commit comments

Comments
 (0)