mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-15 16:59:41 +00:00
[Glitch] Display Wrapstodon inline widget
Port e5e3a64a9b to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import { checkAnnualReport } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { importFetchedAccount } from './importer';
|
import { importFetchedAccount } from './importer';
|
||||||
@@ -29,6 +31,9 @@ export const fetchServer = () => (dispatch, getState) => {
|
|||||||
.get('/api/v2/instance').then(({ data }) => {
|
.get('/api/v2/instance').then(({ data }) => {
|
||||||
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
||||||
dispatch(fetchServerSuccess(data));
|
dispatch(fetchServerSuccess(data));
|
||||||
|
if (data.wrapstodon) {
|
||||||
|
void dispatch(checkAnnualReport());
|
||||||
|
}
|
||||||
}).catch(err => dispatch(fetchServerFail(err)));
|
}).catch(err => dispatch(fetchServerFail(err)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
38
app/javascript/flavours/glitch/api/annual_report.ts
Normal file
38
app/javascript/flavours/glitch/api/annual_report.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import api, { apiRequestGet, getAsyncRefreshHeader } from '../api';
|
||||||
|
import type { ApiAccountJSON } from '../api_types/accounts';
|
||||||
|
import type { ApiStatusJSON } from '../api_types/statuses';
|
||||||
|
import type { AnnualReport } from '../models/annual_report';
|
||||||
|
|
||||||
|
export type ApiAnnualReportState =
|
||||||
|
| 'available'
|
||||||
|
| 'generating'
|
||||||
|
| 'eligible'
|
||||||
|
| 'ineligible';
|
||||||
|
|
||||||
|
export const apiGetAnnualReportState = async (year: number) => {
|
||||||
|
const response = await api().get<{ state: ApiAnnualReportState }>(
|
||||||
|
`/api/v1/annual_reports/${year}/state`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: response.data.state,
|
||||||
|
refresh: getAsyncRefreshHeader(response),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const apiRequestGenerateAnnualReport = async (year: number) => {
|
||||||
|
const response = await api().post(`/api/v1/annual_reports/${year}/generate`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
refresh: getAsyncRefreshHeader(response),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ApiAnnualReportResponse {
|
||||||
|
annual_reports: AnnualReport[];
|
||||||
|
accounts: ApiAccountJSON[];
|
||||||
|
statuses: ApiStatusJSON[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiGetAnnualReport = (year: number) =>
|
||||||
|
apiRequestGet<ApiAnnualReportResponse>(`v1/annual_reports/${year}`);
|
||||||
@@ -8,6 +8,8 @@ import { debounce } from 'lodash';
|
|||||||
import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'flavours/glitch/actions/timelines';
|
import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'flavours/glitch/actions/timelines';
|
||||||
import { RegenerationIndicator } from 'flavours/glitch/components/regeneration_indicator';
|
import { RegenerationIndicator } from 'flavours/glitch/components/regeneration_indicator';
|
||||||
import { InlineFollowSuggestions } from 'flavours/glitch/features/home_timeline/components/inline_follow_suggestions';
|
import { InlineFollowSuggestions } from 'flavours/glitch/features/home_timeline/components/inline_follow_suggestions';
|
||||||
|
import { AnnualReportTimeline } from 'flavours/glitch/features/annual_report/timeline';
|
||||||
|
import { TIMELINE_WRAPSTODON } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||||
|
|
||||||
import { StatusQuoteManager } from '../components/status_quoted';
|
import { StatusQuoteManager } from '../components/status_quoted';
|
||||||
|
|
||||||
@@ -64,10 +66,12 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
switch(statusId) {
|
switch(statusId) {
|
||||||
case TIMELINE_SUGGESTIONS:
|
case TIMELINE_SUGGESTIONS:
|
||||||
return (
|
return (
|
||||||
<InlineFollowSuggestions
|
<InlineFollowSuggestions key={TIMELINE_SUGGESTIONS} />
|
||||||
key='inline-follow-suggestions'
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
case TIMELINE_WRAPSTODON:
|
||||||
|
return (
|
||||||
|
<AnnualReportTimeline key={TIMELINE_WRAPSTODON} />
|
||||||
|
)
|
||||||
case TIMELINE_GAP:
|
case TIMELINE_GAP:
|
||||||
return (
|
return (
|
||||||
<LoadGap
|
<LoadGap
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const AnnualReport: React.FC<{
|
|||||||
|
|
||||||
const report = response?.annual_reports[0];
|
const report = response?.annual_reports[0];
|
||||||
|
|
||||||
if (!report) {
|
if (!report || report.schema_version !== 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { showAlert } from '@/flavours/glitch/actions/alerts';
|
||||||
|
import {
|
||||||
|
generateReport,
|
||||||
|
selectWrapstodonYear,
|
||||||
|
} from '@/flavours/glitch/reducers/slices/annual_report';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
||||||
|
|
||||||
|
import { AnnualReportAnnouncement } from './announcement';
|
||||||
|
|
||||||
|
export const AnnualReportTimeline: FC = () => {
|
||||||
|
const { state } = useAppSelector((state) => state.annualReport);
|
||||||
|
const year = useAppSelector(selectWrapstodonYear);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleBuildRequest = useCallback(() => {
|
||||||
|
void dispatch(generateReport());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleOpen = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
// TODO: Implement opening the annual report view when components are ready.
|
||||||
|
showAlert({
|
||||||
|
message: 'Not yet implemented.',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (!year || !state || state === 'ineligible') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnnualReportAnnouncement
|
||||||
|
year={year.toString()}
|
||||||
|
hasData={state === 'available'}
|
||||||
|
isLoading={state === 'generating'}
|
||||||
|
onRequestBuild={handleBuildRequest}
|
||||||
|
onOpen={handleOpen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,9 +4,10 @@ import { connect } from 'react-redux';
|
|||||||
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { scrollTopTimeline, loadPending } from '../../../actions/timelines';
|
import { scrollTopTimeline, loadPending, TIMELINE_SUGGESTIONS } from '@/flavours/glitch/actions/timelines';
|
||||||
import StatusList from '../../../components/status_list';
|
import StatusList from '@/flavours/glitch/components/status_list';
|
||||||
import { me } from '../../../initial_state';
|
import { me } from '@/flavours/glitch/initial_state';
|
||||||
|
import { TIMELINE_WRAPSTODON } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||||
|
|
||||||
const getRegex = createSelector([
|
const getRegex = createSelector([
|
||||||
(state, { regex }) => regex,
|
(state, { regex }) => regex,
|
||||||
@@ -28,7 +29,7 @@ const makeGetStatusIds = (pending = false) => createSelector([
|
|||||||
getRegex,
|
getRegex,
|
||||||
], (columnSettings, statusIds, statuses, regex) => {
|
], (columnSettings, statusIds, statuses, regex) => {
|
||||||
return statusIds.filter(id => {
|
return statusIds.filter(id => {
|
||||||
if (id === null || id === 'inline-follow-suggestions') return true;
|
if (id === null || id === TIMELINE_SUGGESTIONS || id === TIMELINE_WRAPSTODON) return true;
|
||||||
|
|
||||||
const statusForId = statuses.get(id);
|
const statusForId = statuses.get(id);
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,30 @@ interface AnnualReportV1 {
|
|||||||
archetype: Archetype;
|
archetype: Archetype;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnnualReport {
|
interface AnnualReportV2 {
|
||||||
year: number;
|
archetype: Archetype;
|
||||||
schema_version: number;
|
time_series: TimeSeriesMonth[];
|
||||||
data: AnnualReportV1;
|
top_hashtags: NameAndCount[];
|
||||||
|
top_statuses: TopStatuses;
|
||||||
|
most_used_apps: NameAndCount[];
|
||||||
|
type_distribution: {
|
||||||
|
total: number;
|
||||||
|
reblogs: number;
|
||||||
|
replies: number;
|
||||||
|
standalone: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AnnualReport = {
|
||||||
|
year: number;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
schema_version: 1;
|
||||||
|
data: AnnualReportV1;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
schema_version: 2;
|
||||||
|
data: AnnualReportV2;
|
||||||
|
share_url: string | null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { relationshipsReducer } from './relationships';
|
|||||||
import { searchReducer } from './search';
|
import { searchReducer } from './search';
|
||||||
import server from './server';
|
import server from './server';
|
||||||
import settings from './settings';
|
import settings from './settings';
|
||||||
|
import { sliceReducers } from './slices';
|
||||||
import status_lists from './status_lists';
|
import status_lists from './status_lists';
|
||||||
import statuses from './statuses';
|
import statuses from './statuses';
|
||||||
import { suggestionsReducer } from './suggestions';
|
import { suggestionsReducer } from './suggestions';
|
||||||
@@ -82,6 +83,7 @@ const reducers = {
|
|||||||
notificationPolicy: notificationPolicyReducer,
|
notificationPolicy: notificationPolicyReducer,
|
||||||
notificationRequests: notificationRequestsReducer,
|
notificationRequests: notificationRequestsReducer,
|
||||||
navigation: navigationReducer,
|
navigation: navigationReducer,
|
||||||
|
...sliceReducers,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
|
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
|
||||||
|
|||||||
118
app/javascript/flavours/glitch/reducers/slices/annual_report.ts
Normal file
118
app/javascript/flavours/glitch/reducers/slices/annual_report.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { insertIntoTimeline } from '@/flavours/glitch/actions/timelines';
|
||||||
|
import type { ApiAnnualReportState } from '@/flavours/glitch/api/annual_report';
|
||||||
|
import {
|
||||||
|
apiGetAnnualReport,
|
||||||
|
apiGetAnnualReportState,
|
||||||
|
apiRequestGenerateAnnualReport,
|
||||||
|
} from '@/flavours/glitch/api/annual_report';
|
||||||
|
import type { AnnualReport } from '@/flavours/glitch/models/annual_report';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createAppSelector,
|
||||||
|
createAppThunk,
|
||||||
|
createDataLoadingThunk,
|
||||||
|
} from '../../store/typed_functions';
|
||||||
|
|
||||||
|
export const TIMELINE_WRAPSTODON = 'inline-wrapstodon';
|
||||||
|
|
||||||
|
interface AnnualReportState {
|
||||||
|
state?: ApiAnnualReportState;
|
||||||
|
report?: AnnualReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
const annualReportSlice = createSlice({
|
||||||
|
name: 'annualReport',
|
||||||
|
initialState: {} as AnnualReportState,
|
||||||
|
reducers: {},
|
||||||
|
extraReducers(builder) {
|
||||||
|
builder
|
||||||
|
.addCase(fetchReportState.fulfilled, (state, action) => {
|
||||||
|
state.state = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(generateReport.pending, (state) => {
|
||||||
|
state.state = 'generating';
|
||||||
|
})
|
||||||
|
.addCase(getReport.fulfilled, (state, action) => {
|
||||||
|
if (action.payload) {
|
||||||
|
state.report = action.payload;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const annualReport = annualReportSlice.reducer;
|
||||||
|
|
||||||
|
export const selectWrapstodonYear = createAppSelector(
|
||||||
|
[(state) => state.server.getIn(['server', 'wrapstodon'])],
|
||||||
|
(year: unknown) => (typeof year === 'number' && year > 2000 ? year : null),
|
||||||
|
);
|
||||||
|
|
||||||
|
// This kicks everything off, and is called after fetching the server info.
|
||||||
|
export const checkAnnualReport = createAppThunk(
|
||||||
|
`${annualReportSlice.name}/checkAnnualReport`,
|
||||||
|
async (_arg: unknown, { dispatch, getState }) => {
|
||||||
|
const year = selectWrapstodonYear(getState());
|
||||||
|
if (!year) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const state = await dispatch(fetchReportState());
|
||||||
|
if (
|
||||||
|
state.meta.requestStatus === 'fulfilled' &&
|
||||||
|
state.payload !== 'ineligible'
|
||||||
|
) {
|
||||||
|
dispatch(insertIntoTimeline('home', TIMELINE_WRAPSTODON, 1));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchReportState = createDataLoadingThunk(
|
||||||
|
`${annualReportSlice.name}/fetchReportState`,
|
||||||
|
async (_arg: unknown, { getState }) => {
|
||||||
|
const year = selectWrapstodonYear(getState());
|
||||||
|
if (!year) {
|
||||||
|
throw new Error('Year is not set');
|
||||||
|
}
|
||||||
|
return apiGetAnnualReportState(year);
|
||||||
|
},
|
||||||
|
({ state, refresh }, { dispatch }) => {
|
||||||
|
if (state === 'generating' && refresh) {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
void dispatch(fetchReportState());
|
||||||
|
}, 1_000 * refresh.retry);
|
||||||
|
} else if (state === 'available') {
|
||||||
|
void dispatch(getReport());
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
{ useLoadingBar: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Triggers the generation of the annual report.
|
||||||
|
export const generateReport = createDataLoadingThunk(
|
||||||
|
`${annualReportSlice.name}/generateReport`,
|
||||||
|
async (_arg: unknown, { getState }) => {
|
||||||
|
const year = selectWrapstodonYear(getState());
|
||||||
|
if (!year) {
|
||||||
|
throw new Error('Year is not set');
|
||||||
|
}
|
||||||
|
return apiRequestGenerateAnnualReport(year);
|
||||||
|
},
|
||||||
|
(_arg: unknown, { dispatch }) => {
|
||||||
|
void dispatch(fetchReportState());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getReport = createDataLoadingThunk(
|
||||||
|
`${annualReportSlice.name}/getReport`,
|
||||||
|
async (_arg: unknown, { getState }) => {
|
||||||
|
const year = selectWrapstodonYear(getState());
|
||||||
|
if (!year) {
|
||||||
|
throw new Error('Year is not set');
|
||||||
|
}
|
||||||
|
return apiGetAnnualReport(year);
|
||||||
|
},
|
||||||
|
(data) => data.annual_reports[0],
|
||||||
|
);
|
||||||
5
app/javascript/flavours/glitch/reducers/slices/index.ts
Normal file
5
app/javascript/flavours/glitch/reducers/slices/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { annualReport } from './annual_report';
|
||||||
|
|
||||||
|
export const sliceReducers = {
|
||||||
|
annualReport,
|
||||||
|
};
|
||||||
@@ -205,7 +205,7 @@ export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
|
|||||||
thunkOptions?: AppThunkOptions<Args>,
|
thunkOptions?: AppThunkOptions<Args>,
|
||||||
): ReturnType<typeof createAsyncThunk<Args, void>>;
|
): ReturnType<typeof createAsyncThunk<Args, void>>;
|
||||||
|
|
||||||
// Overload when the `onData` method returns nothing, then the mayload is the `onData` result
|
// Overload when the `onData` method returns nothing, then the payload is the `onData` result
|
||||||
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
|
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
|
||||||
name: string,
|
name: string,
|
||||||
loadData: LoadData<Args, LoadDataResult>,
|
loadData: LoadData<Args, LoadDataResult>,
|
||||||
|
|||||||
Reference in New Issue
Block a user