mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-13 07:49:29 +00:00
Display Wrapstodon inline widget (#37106)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
import { importFetchedAccount } from './importer';
|
||||
@@ -29,6 +31,9 @@ export const fetchServer = () => (dispatch, getState) => {
|
||||
.get('/api/v2/instance').then(({ data }) => {
|
||||
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
||||
dispatch(fetchServerSuccess(data));
|
||||
if (data.wrapstodon) {
|
||||
void dispatch(checkAnnualReport());
|
||||
}
|
||||
}).catch(err => dispatch(fetchServerFail(err)));
|
||||
};
|
||||
|
||||
|
||||
38
app/javascript/mastodon/api/annual_report.ts
Normal file
38
app/javascript/mastodon/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 'mastodon/actions/timelines';
|
||||
import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator';
|
||||
import { InlineFollowSuggestions } from 'mastodon/features/home_timeline/components/inline_follow_suggestions';
|
||||
import { AnnualReportTimeline } from 'mastodon/features/annual_report/timeline';
|
||||
import { TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report';
|
||||
|
||||
import { StatusQuoteManager } from '../components/status_quoted';
|
||||
|
||||
@@ -63,10 +65,12 @@ export default class StatusList extends ImmutablePureComponent {
|
||||
switch(statusId) {
|
||||
case TIMELINE_SUGGESTIONS:
|
||||
return (
|
||||
<InlineFollowSuggestions
|
||||
key='inline-follow-suggestions'
|
||||
/>
|
||||
<InlineFollowSuggestions key={TIMELINE_SUGGESTIONS} />
|
||||
);
|
||||
case TIMELINE_WRAPSTODON:
|
||||
return (
|
||||
<AnnualReportTimeline key={TIMELINE_WRAPSTODON} />
|
||||
)
|
||||
case TIMELINE_GAP:
|
||||
return (
|
||||
<LoadGap
|
||||
|
||||
@@ -59,7 +59,7 @@ export const AnnualReport: React.FC<{
|
||||
|
||||
const report = response?.annual_reports[0];
|
||||
|
||||
if (!report) {
|
||||
if (!report || report.schema_version !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
44
app/javascript/mastodon/features/annual_report/timeline.tsx
Normal file
44
app/javascript/mastodon/features/annual_report/timeline.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { showAlert } from '@/mastodon/actions/alerts';
|
||||
import {
|
||||
generateReport,
|
||||
selectWrapstodonYear,
|
||||
} from '@/mastodon/reducers/slices/annual_report';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/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 { scrollTopTimeline, loadPending } from '../../../actions/timelines';
|
||||
import StatusList from '../../../components/status_list';
|
||||
import { me } from '../../../initial_state';
|
||||
import { scrollTopTimeline, loadPending, TIMELINE_SUGGESTIONS } from '@/mastodon/actions/timelines';
|
||||
import StatusList from '@/mastodon/components/status_list';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import { TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report';
|
||||
|
||||
const makeGetStatusIds = (pending = false) => createSelector([
|
||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
||||
@@ -14,7 +15,7 @@ const makeGetStatusIds = (pending = false) => createSelector([
|
||||
(state) => state.get('statuses'),
|
||||
], (columnSettings, statusIds, statuses) => {
|
||||
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);
|
||||
|
||||
|
||||
@@ -37,8 +37,30 @@ interface AnnualReportV1 {
|
||||
archetype: Archetype;
|
||||
}
|
||||
|
||||
export interface AnnualReport {
|
||||
year: number;
|
||||
schema_version: number;
|
||||
data: AnnualReportV1;
|
||||
interface AnnualReportV2 {
|
||||
archetype: Archetype;
|
||||
time_series: TimeSeriesMonth[];
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -33,6 +33,7 @@ import { relationshipsReducer } from './relationships';
|
||||
import { searchReducer } from './search';
|
||||
import server from './server';
|
||||
import settings from './settings';
|
||||
import { sliceReducers } from './slices';
|
||||
import status_lists from './status_lists';
|
||||
import statuses from './statuses';
|
||||
import { suggestionsReducer } from './suggestions';
|
||||
@@ -80,6 +81,7 @@ const reducers = {
|
||||
notificationPolicy: notificationPolicyReducer,
|
||||
notificationRequests: notificationRequestsReducer,
|
||||
navigation: navigationReducer,
|
||||
...sliceReducers,
|
||||
};
|
||||
|
||||
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
|
||||
|
||||
118
app/javascript/mastodon/reducers/slices/annual_report.ts
Normal file
118
app/javascript/mastodon/reducers/slices/annual_report.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { insertIntoTimeline } from '@/mastodon/actions/timelines';
|
||||
import type { ApiAnnualReportState } from '@/mastodon/api/annual_report';
|
||||
import {
|
||||
apiGetAnnualReport,
|
||||
apiGetAnnualReportState,
|
||||
apiRequestGenerateAnnualReport,
|
||||
} from '@/mastodon/api/annual_report';
|
||||
import type { AnnualReport } from '@/mastodon/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/mastodon/reducers/slices/index.ts
Normal file
5
app/javascript/mastodon/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>,
|
||||
): 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>(
|
||||
name: string,
|
||||
loadData: LoadData<Args, LoadDataResult>,
|
||||
|
||||
Reference in New Issue
Block a user