kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Porównaj commity
14 Commity
e44d4da0d6
...
d9933105ae
Autor | SHA1 | Data |
---|---|---|
Soapbox Bot | d9933105ae | |
marcin mikołajczak | b8093ace04 | |
marcin mikołajczak | 1b73a1fbc3 | |
Alex Gleason | 87d04e6a0c | |
Alex Gleason | b86da5f426 | |
marcin mikołajczak | 1518615767 | |
marcin mikołajczak | fa34fa3652 | |
marcin mikołajczak | 5f515524f8 | |
marcin mikołajczak | 6918768f55 | |
marcin mikołajczak | f90a1618d2 | |
marcin mikołajczak | 7a8b473406 | |
marcin mikołajczak | 10db5c264d | |
marcin mikołajczak | 161db37ba0 | |
marcin mikołajczak | e3a87a0326 |
|
@ -111,9 +111,9 @@ pages:
|
|||
|
||||
docker:
|
||||
stage: deploy
|
||||
image: docker:25.0.3
|
||||
image: docker:26.0.2
|
||||
services:
|
||||
- docker:25.0.3-dind
|
||||
- docker:26.0.2-dind
|
||||
tags:
|
||||
- dind
|
||||
# https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
||||
import { accountIdsToAccts } from 'soapbox/selectors';
|
||||
import toast from 'soapbox/toast';
|
||||
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { openModal } from './modals';
|
||||
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Announcement } from 'soapbox/types/entities';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
|
||||
const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
|
||||
|
@ -81,35 +77,6 @@ const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
|
|||
|
||||
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
||||
|
||||
const ADMIN_ANNOUNCEMENTS_FETCH_FAIL = 'ADMIN_ANNOUNCEMENTS_FETCH_FAILS';
|
||||
const ADMIN_ANNOUNCEMENTS_FETCH_REQUEST = 'ADMIN_ANNOUNCEMENTS_FETCH_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS = 'ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS';
|
||||
|
||||
const ADMIN_ANNOUNCEMENTS_EXPAND_FAIL = 'ADMIN_ANNOUNCEMENTS_EXPAND_FAILS';
|
||||
const ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST = 'ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS = 'ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_CONTENT = 'ADMIN_ANNOUNCEMENT_CHANGE_CONTENT';
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_START_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_START_TIME';
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_END_TIME = 'ADMIN_ANNOUNCEMENT_CHANGE_END_TIME';
|
||||
const ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY = 'ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_CREATE_REQUEST = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_CREATE_SUCCESS = 'ADMIN_ANNOUNCEMENT_CREATE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_CREATE_FAIL = 'ADMIN_ANNOUNCEMENT_CREATE_FAIL';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_DELETE_REQUEST = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_DELETE_SUCCESS = 'ADMIN_ANNOUNCEMENT_DELETE_REQUEST';
|
||||
const ADMIN_ANNOUNCEMENT_DELETE_FAIL = 'ADMIN_ANNOUNCEMENT_DELETE_FAIL';
|
||||
|
||||
const ADMIN_ANNOUNCEMENT_MODAL_INIT = 'ADMIN_ANNOUNCEMENT_MODAL_INIT';
|
||||
|
||||
const messages = defineMessages({
|
||||
announcementCreateSuccess: { id: 'admin.edit_announcement.created', defaultMessage: 'Announcement created' },
|
||||
announcementDeleteSuccess: { id: 'admin.edit_announcement.deleted', defaultMessage: 'Announcement deleted' },
|
||||
announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' },
|
||||
});
|
||||
|
||||
const fetchConfig = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
|
||||
|
@ -572,92 +539,6 @@ const expandUserIndex = () =>
|
|||
});
|
||||
};
|
||||
|
||||
const fetchAdminAnnouncements = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_REQUEST });
|
||||
return api(getState)
|
||||
.get('/api/v1/pleroma/admin/announcements', { params: { limit: 50 } })
|
||||
.then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS, announcements: data });
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_FETCH_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const expandAdminAnnouncements = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const page = getState().admin_announcements.page;
|
||||
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST });
|
||||
return api(getState)
|
||||
.get('/api/v1/pleroma/admin/announcements', { params: { limit: 50, offset: page * 50 } })
|
||||
.then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS, announcements: data });
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENTS_EXPAND_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const changeAnnouncementContent = (content: string) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||
value: content,
|
||||
});
|
||||
|
||||
const changeAnnouncementStartTime = (time: Date | null) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||
value: time,
|
||||
});
|
||||
|
||||
const changeAnnouncementEndTime = (time: Date | null) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||
value: time,
|
||||
});
|
||||
|
||||
const changeAnnouncementAllDay = (allDay: boolean) => ({
|
||||
type: ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||
value: allDay,
|
||||
});
|
||||
|
||||
const handleCreateAnnouncement = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_REQUEST });
|
||||
|
||||
const { id, content, starts_at, ends_at, all_day } = getState().admin_announcements.form;
|
||||
|
||||
return api(getState)[id ? 'patch' : 'post'](
|
||||
id ? `/api/v1/pleroma/admin/announcements/${id}` : '/api/v1/pleroma/admin/announcements',
|
||||
{ content, starts_at, ends_at, all_day },
|
||||
).then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_SUCCESS, announcement: data });
|
||||
toast.success(id ? messages.announcementUpdateSuccess : messages.announcementCreateSuccess);
|
||||
dispatch(fetchAdminAnnouncements());
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_CREATE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAnnouncement = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_REQUEST, id });
|
||||
|
||||
return api(getState).delete(`/api/v1/pleroma/admin/announcements/${id}`).then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_SUCCESS, id });
|
||||
toast.success(messages.announcementDeleteSuccess);
|
||||
dispatch(fetchAdminAnnouncements());
|
||||
return data;
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_DELETE_FAIL, id, error });
|
||||
});
|
||||
};
|
||||
|
||||
const initAnnouncementModal = (announcement?: Announcement) =>
|
||||
(dispatch: AppDispatch) => {
|
||||
dispatch({ type: ADMIN_ANNOUNCEMENT_MODAL_INIT, announcement });
|
||||
dispatch(openModal('EDIT_ANNOUNCEMENT'));
|
||||
};
|
||||
|
||||
export {
|
||||
ADMIN_CONFIG_FETCH_REQUEST,
|
||||
|
@ -709,23 +590,6 @@ export {
|
|||
ADMIN_USER_INDEX_FETCH_REQUEST,
|
||||
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
||||
ADMIN_USER_INDEX_QUERY_SET,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_FAIL,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_REQUEST,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENTS_EXPAND_FAIL,
|
||||
ADMIN_ANNOUNCEMENTS_EXPAND_REQUEST,
|
||||
ADMIN_ANNOUNCEMENTS_EXPAND_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_FAIL,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_REQUEST,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_DELETE_FAIL,
|
||||
ADMIN_ANNOUNCEMENT_DELETE_REQUEST,
|
||||
ADMIN_ANNOUNCEMENT_DELETE_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_MODAL_INIT,
|
||||
fetchConfig,
|
||||
updateConfig,
|
||||
updateSoapboxConfig,
|
||||
|
@ -750,13 +614,4 @@ export {
|
|||
setUserIndexQuery,
|
||||
fetchUserIndex,
|
||||
expandUserIndex,
|
||||
fetchAdminAnnouncements,
|
||||
expandAdminAnnouncements,
|
||||
changeAnnouncementContent,
|
||||
changeAnnouncementStartTime,
|
||||
changeAnnouncementEndTime,
|
||||
changeAnnouncementAllDay,
|
||||
handleCreateAnnouncement,
|
||||
deleteAnnouncement,
|
||||
initAnnouncementModal,
|
||||
};
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import announcements from 'soapbox/__fixtures__/announcements.json';
|
||||
import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'soapbox/actions/announcements';
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildInstance } from 'soapbox/jest/factory';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { normalizeAnnouncement } from 'soapbox/normalizers';
|
||||
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
describe('fetchAnnouncements()', () => {
|
||||
describe('with a successful API request', () => {
|
||||
it('should fetch announcements from the API', async() => {
|
||||
const state = rootState
|
||||
.set('instance', buildInstance({ version: '3.5.3' }));
|
||||
const store = mockStore(state);
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/announcements').reply(200, announcements);
|
||||
});
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'ANNOUNCEMENTS_FETCH_REQUEST', skipLoading: true },
|
||||
{ type: 'ANNOUNCEMENTS_FETCH_SUCCESS', announcements, skipLoading: true },
|
||||
{ type: 'POLLS_IMPORT', polls: [] },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [] },
|
||||
{ type: 'STATUSES_IMPORT', statuses: [], expandSpoilers: false },
|
||||
];
|
||||
await store.dispatch(fetchAnnouncements());
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissAnnouncement', () => {
|
||||
describe('with a successful API request', () => {
|
||||
it('should mark announcement as dismissed', async() => {
|
||||
const store = mockStore(rootState);
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onPost('/api/v1/announcements/1/dismiss').reply(200);
|
||||
});
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'ANNOUNCEMENTS_DISMISS_REQUEST', id: '1' },
|
||||
{ type: 'ANNOUNCEMENTS_DISMISS_SUCCESS', id: '1' },
|
||||
];
|
||||
await store.dispatch(dismissAnnouncement('1'));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addReaction', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.setIn(['announcements', 'items'], ImmutableList((announcements).map((announcement: APIEntity) => normalizeAnnouncement(announcement))))
|
||||
.setIn(['announcements', 'isLoading'], false);
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
it('should add reaction to a post', async() => {
|
||||
__stub((mock) => {
|
||||
mock.onPut('/api/v1/announcements/2/reactions/📉').reply(200);
|
||||
});
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'ANNOUNCEMENTS_REACTION_ADD_REQUEST', id: '2', name: '📉', skipLoading: true },
|
||||
{ type: 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS', id: '2', name: '📉', skipLoading: true },
|
||||
];
|
||||
await store.dispatch(addReaction('2', '📉'));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeReaction', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.setIn(['announcements', 'items'], ImmutableList((announcements).map((announcement: APIEntity) => normalizeAnnouncement(announcement))))
|
||||
.setIn(['announcements', 'isLoading'], false);
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
it('should remove reaction from a post', async() => {
|
||||
__stub((mock) => {
|
||||
mock.onDelete('/api/v1/announcements/2/reactions/📉').reply(200);
|
||||
});
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST', id: '2', name: '📉', skipLoading: true },
|
||||
{ type: 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS', id: '2', name: '📉', skipLoading: true },
|
||||
];
|
||||
await store.dispatch(removeReaction('2', '📉'));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,196 +0,0 @@
|
|||
import api from 'soapbox/api';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import { importFetchedStatuses } from './importer';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST';
|
||||
export const ANNOUNCEMENTS_FETCH_SUCCESS = 'ANNOUNCEMENTS_FETCH_SUCCESS';
|
||||
export const ANNOUNCEMENTS_FETCH_FAIL = 'ANNOUNCEMENTS_FETCH_FAIL';
|
||||
export const ANNOUNCEMENTS_UPDATE = 'ANNOUNCEMENTS_UPDATE';
|
||||
export const ANNOUNCEMENTS_DELETE = 'ANNOUNCEMENTS_DELETE';
|
||||
|
||||
export const ANNOUNCEMENTS_DISMISS_REQUEST = 'ANNOUNCEMENTS_DISMISS_REQUEST';
|
||||
export const ANNOUNCEMENTS_DISMISS_SUCCESS = 'ANNOUNCEMENTS_DISMISS_SUCCESS';
|
||||
export const ANNOUNCEMENTS_DISMISS_FAIL = 'ANNOUNCEMENTS_DISMISS_FAIL';
|
||||
|
||||
export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST';
|
||||
export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS';
|
||||
export const ANNOUNCEMENTS_REACTION_ADD_FAIL = 'ANNOUNCEMENTS_REACTION_ADD_FAIL';
|
||||
|
||||
export const ANNOUNCEMENTS_REACTION_REMOVE_REQUEST = 'ANNOUNCEMENTS_REACTION_REMOVE_REQUEST';
|
||||
export const ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS = 'ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS';
|
||||
export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = 'ANNOUNCEMENTS_REACTION_REMOVE_FAIL';
|
||||
|
||||
export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE';
|
||||
|
||||
export const ANNOUNCEMENTS_TOGGLE_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW';
|
||||
|
||||
const noOp = () => {};
|
||||
|
||||
export const fetchAnnouncements = (done = noOp) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { instance } = getState();
|
||||
const features = getFeatures(instance);
|
||||
|
||||
if (!features.announcements) return null;
|
||||
|
||||
dispatch(fetchAnnouncementsRequest());
|
||||
|
||||
return api(getState).get('/api/v1/announcements').then(response => {
|
||||
dispatch(fetchAnnouncementsSuccess(response.data));
|
||||
dispatch(importFetchedStatuses(response.data.map(({ statuses }: APIEntity) => statuses)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchAnnouncementsFail(error));
|
||||
}).finally(() => {
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchAnnouncementsRequest = () => ({
|
||||
type: ANNOUNCEMENTS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const fetchAnnouncementsSuccess = (announcements: APIEntity) => ({
|
||||
type: ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||
announcements,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const fetchAnnouncementsFail = (error: unknown) => ({
|
||||
type: ANNOUNCEMENTS_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
});
|
||||
|
||||
export const updateAnnouncements = (announcement: APIEntity) => ({
|
||||
type: ANNOUNCEMENTS_UPDATE,
|
||||
announcement: announcement,
|
||||
});
|
||||
|
||||
export const dismissAnnouncement = (announcementId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(dismissAnnouncementRequest(announcementId));
|
||||
|
||||
return api(getState).post(`/api/v1/announcements/${announcementId}/dismiss`).then(() => {
|
||||
dispatch(dismissAnnouncementSuccess(announcementId));
|
||||
}).catch(error => {
|
||||
dispatch(dismissAnnouncementFail(announcementId, error));
|
||||
});
|
||||
};
|
||||
|
||||
export const dismissAnnouncementRequest = (announcementId: string) => ({
|
||||
type: ANNOUNCEMENTS_DISMISS_REQUEST,
|
||||
id: announcementId,
|
||||
});
|
||||
|
||||
export const dismissAnnouncementSuccess = (announcementId: string) => ({
|
||||
type: ANNOUNCEMENTS_DISMISS_SUCCESS,
|
||||
id: announcementId,
|
||||
});
|
||||
|
||||
export const dismissAnnouncementFail = (announcementId: string, error: unknown) => ({
|
||||
type: ANNOUNCEMENTS_DISMISS_FAIL,
|
||||
id: announcementId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const addReaction = (announcementId: string, name: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const announcement = getState().announcements.items.find(x => x.get('id') === announcementId);
|
||||
|
||||
let alreadyAdded = false;
|
||||
|
||||
if (announcement) {
|
||||
const reaction = announcement.reactions.find(x => x.name === name);
|
||||
|
||||
if (reaction && reaction.me) {
|
||||
alreadyAdded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyAdded) {
|
||||
dispatch(addReactionRequest(announcementId, name, alreadyAdded));
|
||||
}
|
||||
|
||||
return api(getState).put(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
|
||||
dispatch(addReactionSuccess(announcementId, name, alreadyAdded));
|
||||
}).catch(err => {
|
||||
if (!alreadyAdded) {
|
||||
dispatch(addReactionFail(announcementId, name, err));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const addReactionRequest = (announcementId: string, name: string, alreadyAdded?: boolean) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_ADD_REQUEST,
|
||||
id: announcementId,
|
||||
name,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const addReactionSuccess = (announcementId: string, name: string, alreadyAdded?: boolean) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_ADD_SUCCESS,
|
||||
id: announcementId,
|
||||
name,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const addReactionFail = (announcementId: string, name: string, error: unknown) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_ADD_FAIL,
|
||||
id: announcementId,
|
||||
name,
|
||||
error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const removeReaction = (announcementId: string, name: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(removeReactionRequest(announcementId, name));
|
||||
|
||||
return api(getState).delete(`/api/v1/announcements/${announcementId}/reactions/${name}`).then(() => {
|
||||
dispatch(removeReactionSuccess(announcementId, name));
|
||||
}).catch(err => {
|
||||
dispatch(removeReactionFail(announcementId, name, err));
|
||||
});
|
||||
};
|
||||
|
||||
export const removeReactionRequest = (announcementId: string, name: string) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_REMOVE_REQUEST,
|
||||
id: announcementId,
|
||||
name,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const removeReactionSuccess = (announcementId: string, name: string) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_REMOVE_SUCCESS,
|
||||
id: announcementId,
|
||||
name,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const removeReactionFail = (announcementId: string, name: string, error: unknown) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_REMOVE_FAIL,
|
||||
id: announcementId,
|
||||
name,
|
||||
error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const updateReaction = (reaction: APIEntity) => ({
|
||||
type: ANNOUNCEMENTS_REACTION_UPDATE,
|
||||
reaction,
|
||||
});
|
||||
|
||||
export const toggleShowAnnouncements = () => ({
|
||||
type: ANNOUNCEMENTS_TOGGLE_SHOW,
|
||||
});
|
||||
|
||||
export const deleteAnnouncement = (id: string) => ({
|
||||
type: ANNOUNCEMENTS_DELETE,
|
||||
id,
|
||||
});
|
|
@ -1,21 +1,18 @@
|
|||
import { getLocale, getSettings } from 'soapbox/actions/settings';
|
||||
import { updateReactions } from 'soapbox/api/hooks/announcements/useAnnouncements';
|
||||
import { importEntities } from 'soapbox/entity-store/actions';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { selectEntity } from 'soapbox/entity-store/selectors';
|
||||
import messages from 'soapbox/messages';
|
||||
import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { announcementSchema, type Announcement, type Relationship } from 'soapbox/schemas';
|
||||
import { getUnreadChatsCount, updateChatListItem, updateChatMessage } from 'soapbox/utils/chats';
|
||||
import { removePageItem } from 'soapbox/utils/queries';
|
||||
import { play, soundCache } from 'soapbox/utils/sounds';
|
||||
|
||||
import { connectStream } from '../stream';
|
||||
|
||||
import {
|
||||
deleteAnnouncement,
|
||||
updateAnnouncements,
|
||||
updateReaction as updateAnnouncementsReaction,
|
||||
} from './announcements';
|
||||
import { updateConversations } from './conversations';
|
||||
import { fetchFilters } from './filters';
|
||||
import { MARKER_FETCH_SUCCESS } from './markers';
|
||||
|
@ -29,7 +26,6 @@ import {
|
|||
} from './timelines';
|
||||
|
||||
import type { IStatContext } from 'soapbox/contexts/stat-context';
|
||||
import type { Relationship } from 'soapbox/schemas';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity, Chat } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -66,6 +62,35 @@ const updateChatQuery = (chat: IChat) => {
|
|||
queryClient.setQueryData<Chat>(ChatKeys.chat(chat.id), newChat as any);
|
||||
};
|
||||
|
||||
const updateAnnouncementReactions = ({ announcement_id: id, name, count }: APIEntity) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => {
|
||||
if (value.id !== id) return value;
|
||||
|
||||
return announcementSchema.parse({
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, -1, true),
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const updateAnnouncement = (announcement: APIEntity) =>
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => {
|
||||
let updated = false;
|
||||
|
||||
const result = prevResult.map(value => value.id === announcement.id
|
||||
? (updated = true, announcementSchema.parse(announcement))
|
||||
: value);
|
||||
|
||||
if (!updated) return [announcementSchema.parse(announcement), ...result];
|
||||
});
|
||||
|
||||
const deleteAnnouncement = (id: string) =>
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.filter(value => value.id !== id),
|
||||
);
|
||||
|
||||
interface TimelineStreamOpts {
|
||||
statContext?: IStatContext;
|
||||
enabled?: boolean;
|
||||
|
@ -164,13 +189,13 @@ const connectTimelineStream = (
|
|||
dispatch(updateFollowRelationships(JSON.parse(data.payload)));
|
||||
break;
|
||||
case 'announcement':
|
||||
dispatch(updateAnnouncements(JSON.parse(data.payload)));
|
||||
updateAnnouncement(JSON.parse(data.payload));
|
||||
break;
|
||||
case 'announcement.reaction':
|
||||
dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
|
||||
updateAnnouncementReactions(JSON.parse(data.payload));
|
||||
break;
|
||||
case 'announcement.delete':
|
||||
dispatch(deleteAnnouncement(data.payload));
|
||||
deleteAnnouncement(data.payload);
|
||||
break;
|
||||
case 'marker':
|
||||
dispatch({ type: MARKER_FETCH_SUCCESS, marker: JSON.parse(data.payload) });
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { adminAnnouncementSchema, type AdminAnnouncement } from 'soapbox/schemas';
|
||||
|
||||
import { useAnnouncements as useUserAnnouncements } from '../announcements';
|
||||
|
||||
import type { AxiosResponse } from 'axios';
|
||||
|
||||
interface CreateAnnouncementParams {
|
||||
content: string;
|
||||
starts_at?: string | null;
|
||||
ends_at?: string | null;
|
||||
all_day?: boolean;
|
||||
}
|
||||
|
||||
interface UpdateAnnouncementParams extends CreateAnnouncementParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const useAnnouncements = () => {
|
||||
const api = useApi();
|
||||
const userAnnouncements = useUserAnnouncements();
|
||||
|
||||
const getAnnouncements = async () => {
|
||||
const { data } = await api.get<AdminAnnouncement[]>('/api/v1/pleroma/admin/announcements');
|
||||
|
||||
const normalizedData = data.map((announcement) => adminAnnouncementSchema.parse(announcement));
|
||||
return normalizedData;
|
||||
};
|
||||
|
||||
const result = useQuery<ReadonlyArray<AdminAnnouncement>>({
|
||||
queryKey: ['admin', 'announcements'],
|
||||
queryFn: getAnnouncements,
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: createAnnouncement,
|
||||
isPending: isCreating,
|
||||
} = useMutation({
|
||||
mutationFn: (params: CreateAnnouncementParams) => api.post('/api/v1/pleroma/admin/announcements', params),
|
||||
retry: false,
|
||||
onSuccess: ({ data }: AxiosResponse) =>
|
||||
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
|
||||
[...prevResult, adminAnnouncementSchema.parse(data)],
|
||||
),
|
||||
onSettled: () => userAnnouncements.refetch(),
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: updateAnnouncement,
|
||||
isPending: isUpdating,
|
||||
} = useMutation({
|
||||
mutationFn: ({ id, ...params }: UpdateAnnouncementParams) => api.patch(`/api/v1/pleroma/admin/announcements/${id}`, params),
|
||||
retry: false,
|
||||
onSuccess: ({ data }: AxiosResponse) =>
|
||||
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
|
||||
prevResult.map((announcement) => announcement.id === data.id ? adminAnnouncementSchema.parse(data) : announcement),
|
||||
),
|
||||
onSettled: () => userAnnouncements.refetch(),
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: deleteAnnouncement,
|
||||
isPending: isDeleting,
|
||||
} = useMutation({
|
||||
mutationFn: (id: string) => api.delete(`/api/v1/pleroma/admin/announcements/${id}`),
|
||||
retry: false,
|
||||
onSuccess: (_, id) =>
|
||||
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
|
||||
prevResult.filter(({ id: announcementId }) => announcementId !== id),
|
||||
),
|
||||
onSettled: () => userAnnouncements.refetch(),
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
createAnnouncement,
|
||||
isCreating,
|
||||
updateAnnouncement,
|
||||
isUpdating,
|
||||
deleteAnnouncement,
|
||||
isDeleting,
|
||||
};
|
||||
};
|
||||
|
||||
export { useAnnouncements };
|
|
@ -0,0 +1 @@
|
|||
export { useAnnouncements } from './useAnnouncements';
|
|
@ -0,0 +1,95 @@
|
|||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { announcementReactionSchema, announcementSchema, type Announcement, type AnnouncementReaction } from 'soapbox/schemas';
|
||||
|
||||
const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => announcementReactionSchema.parse({
|
||||
...reaction,
|
||||
me: typeof me === 'boolean' ? me : reaction.me,
|
||||
count: overwrite ? count : (reaction.count + count),
|
||||
});
|
||||
|
||||
export const updateReactions = (reactions: AnnouncementReaction[], name: string, count: number, me?: boolean, overwrite?: boolean) => {
|
||||
const idx = reactions.findIndex(reaction => reaction.name === name);
|
||||
|
||||
if (idx > -1) {
|
||||
reactions = reactions.map(reaction => reaction.name === name ? updateReaction(reaction, count, me, overwrite) : reaction);
|
||||
}
|
||||
|
||||
return [...reactions, updateReaction(announcementReactionSchema.parse({ name }), count, me, overwrite)];
|
||||
};
|
||||
|
||||
const useAnnouncements = () => {
|
||||
const api = useApi();
|
||||
|
||||
const getAnnouncements = async () => {
|
||||
const { data } = await api.get<Announcement[]>('/api/v1/announcements');
|
||||
|
||||
const normalizedData = data?.map((announcement) => announcementSchema.parse(announcement));
|
||||
return normalizedData;
|
||||
};
|
||||
|
||||
const { data, ...result } = useQuery<ReadonlyArray<Announcement>>({
|
||||
queryKey: ['announcements'],
|
||||
queryFn: getAnnouncements,
|
||||
placeholderData: [],
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: addReaction,
|
||||
} = useMutation({
|
||||
mutationFn: ({ announcementId, name }: { announcementId: string; name: string }) =>
|
||||
api.put<Announcement>(`/api/v1/announcements/${announcementId}/reactions/${name}`),
|
||||
retry: false,
|
||||
onMutate: ({ announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : announcementSchema.parse({
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, 1, true),
|
||||
})),
|
||||
);
|
||||
},
|
||||
onError: (_, { announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : announcementSchema.parse({
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, -1, false),
|
||||
})),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutate: removeReaction,
|
||||
} = useMutation({
|
||||
mutationFn: ({ announcementId, name }: { announcementId: string; name: string }) =>
|
||||
api.delete<Announcement>(`/api/v1/announcements/${announcementId}/reactions/${name}`),
|
||||
retry: false,
|
||||
onMutate: ({ announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : announcementSchema.parse({
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, -1, false),
|
||||
})),
|
||||
);
|
||||
},
|
||||
onError: (_, { announcementId: id, name }) => {
|
||||
queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) =>
|
||||
prevResult.map(value => value.id !== id ? value : announcementSchema.parse({
|
||||
...value,
|
||||
reactions: updateReactions(value.reactions, name, 1, true),
|
||||
})),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: data?.toSorted((a, b) => new Date(a.starts_at || a.published_at).getDate() - new Date(b.starts_at || b.published_at).getDate()),
|
||||
...result,
|
||||
addReaction,
|
||||
removeReaction,
|
||||
};
|
||||
};
|
||||
|
||||
export { useAnnouncements };
|
|
@ -1,4 +1,3 @@
|
|||
import { fetchAnnouncements } from 'soapbox/actions/announcements';
|
||||
import { expandNotifications } from 'soapbox/actions/notifications';
|
||||
import { expandHomeTimeline } from 'soapbox/actions/timelines';
|
||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
|
@ -24,8 +23,7 @@ function useUserStream() {
|
|||
/** Refresh home timeline and notifications. */
|
||||
function refresh(dispatch: AppDispatch, done?: () => void) {
|
||||
return dispatch(expandHomeTimeline({}, () =>
|
||||
dispatch(expandNotifications({}, () =>
|
||||
dispatch(fetchAnnouncements(done))))));
|
||||
dispatch(expandNotifications({}, done))));
|
||||
}
|
||||
|
||||
export { useUserStream };
|
|
@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
|
|||
|
||||
import { getTextDirection } from 'soapbox/utils/rtl';
|
||||
|
||||
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
|
||||
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/schemas';
|
||||
|
||||
interface IAnnouncementContent {
|
||||
announcement: AnnouncementEntity;
|
||||
|
@ -67,7 +67,7 @@ const AnnouncementContent: React.FC<IAnnouncementContent> = ({ announcement }) =
|
|||
} else if (link.textContent?.charAt(0) === '#' || (link.previousSibling?.textContent?.charAt(link.previousSibling.textContent.length - 1) === '#')) {
|
||||
link.addEventListener('click', onHashtagClick.bind(link, link.text), false);
|
||||
} else {
|
||||
const status = announcement.statuses.get(link.href);
|
||||
const status = announcement.statuses[link.href];
|
||||
if (status) {
|
||||
link.addEventListener('click', onStatusClick.bind(this, status), false);
|
||||
}
|
||||
|
|
|
@ -9,16 +9,14 @@ import AnnouncementContent from './announcement-content';
|
|||
import ReactionsBar from './reactions-bar';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
|
||||
import type { Announcement as AnnouncementEntity } from 'soapbox/schemas';
|
||||
|
||||
interface IAnnouncement {
|
||||
announcement: AnnouncementEntity;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
}
|
||||
|
||||
const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, removeReaction, emojiMap }) => {
|
||||
const Announcement: React.FC<IAnnouncement> = ({ announcement, emojiMap }) => {
|
||||
const features = useFeatures();
|
||||
|
||||
const startsAt = announcement.starts_at && new Date(announcement.starts_at);
|
||||
|
@ -64,8 +62,6 @@ const Announcement: React.FC<IAnnouncement> = ({ announcement, addReaction, remo
|
|||
<ReactionsBar
|
||||
reactions={announcement.reactions}
|
||||
announcementId={announcement.id}
|
||||
addReaction={addReaction}
|
||||
removeReaction={removeReaction}
|
||||
emojiMap={emojiMap}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -5,9 +5,9 @@ import { FormattedMessage } from 'react-intl';
|
|||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { addReaction as addReactionAction, removeReaction as removeReactionAction } from 'soapbox/actions/announcements';
|
||||
import { useAnnouncements } from 'soapbox/api/hooks/announcements';
|
||||
import { Card, HStack, Widget } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Announcement from './announcement';
|
||||
|
||||
|
@ -16,36 +16,30 @@ import type { RootState } from 'soapbox/store';
|
|||
const customEmojiMap = createSelector([(state: RootState) => state.custom_emojis], items => (items as ImmutableList<ImmutableMap<string, string>>).reduce((map, emoji) => map.set(emoji.get('shortcode')!, emoji), ImmutableMap<string, ImmutableMap<string, string>>()));
|
||||
|
||||
const AnnouncementsPanel = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const emojiMap = useAppSelector(state => customEmojiMap(state));
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const announcements = useAppSelector((state) => state.announcements.items);
|
||||
const { data: announcements } = useAnnouncements();
|
||||
|
||||
const addReaction = (id: string, name: string) => dispatch(addReactionAction(id, name));
|
||||
const removeReaction = (id: string, name: string) => dispatch(removeReactionAction(id, name));
|
||||
|
||||
if (announcements.size === 0) return null;
|
||||
if (!announcements || announcements.length === 0) return null;
|
||||
|
||||
const handleChangeIndex = (index: number) => {
|
||||
setIndex(index % announcements.size);
|
||||
setIndex(index % announcements.length);
|
||||
};
|
||||
|
||||
return (
|
||||
<Widget title={<FormattedMessage id='announcements.title' defaultMessage='Announcements' />}>
|
||||
<Card className='relative black:rounded-xl black:border black:border-gray-800' size='md' variant='rounded'>
|
||||
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
|
||||
{announcements.map((announcement) => (
|
||||
{announcements!.map((announcement) => (
|
||||
<Announcement
|
||||
key={announcement.id}
|
||||
announcement={announcement}
|
||||
emojiMap={emojiMap}
|
||||
addReaction={addReaction}
|
||||
removeReaction={removeReaction}
|
||||
/>
|
||||
)).reverse()}
|
||||
</ReactSwipeableViews>
|
||||
{announcements.size > 1 && (
|
||||
{announcements.length > 1 && (
|
||||
<HStack space={2} alignItems='center' justifyContent='center' className='relative'>
|
||||
{announcements.map((_, i) => (
|
||||
<button
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useAnnouncements } from 'soapbox/api/hooks/announcements';
|
||||
import AnimatedNumber from 'soapbox/components/animated-number';
|
||||
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||
|
||||
import Emoji from './emoji';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
||||
import type { AnnouncementReaction } from 'soapbox/schemas';
|
||||
|
||||
interface IReaction {
|
||||
announcementId: string;
|
||||
reaction: AnnouncementReaction;
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction, removeReaction, emojiMap, style }) => {
|
||||
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, style }) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const { addReaction, removeReaction } = useAnnouncements();
|
||||
|
||||
const handleClick = () => {
|
||||
if (reaction.me) {
|
||||
removeReaction(announcementId, reaction.name);
|
||||
removeReaction({ announcementId, name: reaction.name });
|
||||
} else {
|
||||
addReaction(announcementId, reaction.name);
|
||||
addReaction({ announcementId, name: reaction.name });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,28 +2,28 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
|
||||
import { useAnnouncements } from 'soapbox/api/hooks/announcements';
|
||||
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
|
||||
import Reaction from './reaction';
|
||||
|
||||
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
|
||||
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
||||
import type { AnnouncementReaction } from 'soapbox/schemas';
|
||||
|
||||
interface IReactionsBar {
|
||||
announcementId: string;
|
||||
reactions: ImmutableList<AnnouncementReaction>;
|
||||
reactions: AnnouncementReaction[];
|
||||
emojiMap: ImmutableMap<string, ImmutableMap<string, string>>;
|
||||
addReaction: (id: string, name: string) => void;
|
||||
removeReaction: (id: string, name: string) => void;
|
||||
}
|
||||
|
||||
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addReaction, removeReaction, emojiMap }) => {
|
||||
const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, emojiMap }) => {
|
||||
const { reduceMotion } = useSettings();
|
||||
const { addReaction } = useAnnouncements();
|
||||
|
||||
const handleEmojiPick = (data: Emoji) => {
|
||||
addReaction(announcementId, (data as NativeEmoji).native.replace(/:/g, ''));
|
||||
addReaction({ announcementId, name: (data as NativeEmoji).native.replace(/:/g, '') });
|
||||
};
|
||||
|
||||
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
|
||||
|
@ -36,25 +36,23 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
|
|||
key: reaction.name,
|
||||
data: reaction,
|
||||
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
|
||||
})).toArray();
|
||||
}));
|
||||
|
||||
return (
|
||||
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
|
||||
{items => (
|
||||
<div className={clsx('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
||||
<div className={clsx('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.length === 0 })}>
|
||||
{items.map(({ key, data, style }) => (
|
||||
<Reaction
|
||||
key={key}
|
||||
reaction={data}
|
||||
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
|
||||
announcementId={announcementId}
|
||||
addReaction={addReaction}
|
||||
removeReaction={removeReaction}
|
||||
emojiMap={emojiMap}
|
||||
/>
|
||||
))}
|
||||
|
||||
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
|
||||
{visibleReactions.length < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
|
||||
</div>
|
||||
)}
|
||||
</TransitionMotion>
|
||||
|
|
|
@ -1,18 +1,34 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { hexToHsl } from 'soapbox/utils/theme';
|
||||
|
||||
interface IBadge {
|
||||
title: React.ReactNode;
|
||||
slug: string;
|
||||
color?: string;
|
||||
}
|
||||
/** Badge to display on a user's profile. */
|
||||
const Badge: React.FC<IBadge> = ({ title, slug }) => {
|
||||
const Badge: React.FC<IBadge> = ({ title, slug, color }) => {
|
||||
const fallback = !['patron', 'admin', 'moderator', 'opaque', 'badge:donor'].includes(slug);
|
||||
|
||||
const isDark = useMemo(() => {
|
||||
if (!color) return false;
|
||||
|
||||
const hsl = hexToHsl(color);
|
||||
|
||||
if (hsl && hsl.l > 50) return false;
|
||||
|
||||
return true;
|
||||
}, [color]);
|
||||
|
||||
return (
|
||||
<span
|
||||
data-testid='badge'
|
||||
className={clsx('inline-flex items-center rounded px-2 py-0.5 text-xs font-medium', {
|
||||
className={clsx('inline-flex items-center rounded px-2 py-0.5 text-xs font-medium', color ? {
|
||||
'bg-gray-100 text-gray-100': isDark,
|
||||
'bg-gray-800 text-gray-900': !isDark,
|
||||
} : {
|
||||
'bg-fuchsia-700 text-white': slug === 'patron',
|
||||
'bg-emerald-800 text-white': slug === 'badge:donor',
|
||||
'bg-black text-white': slug === 'admin',
|
||||
|
@ -20,6 +36,7 @@ const Badge: React.FC<IBadge> = ({ title, slug }) => {
|
|||
'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100': fallback,
|
||||
'bg-white/75 text-gray-900': slug === 'opaque',
|
||||
})}
|
||||
style={color ? { background: color } : undefined}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
|
|
|
@ -38,6 +38,8 @@ const messages = defineMessages({
|
|||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
bookmarkSetFolder: { id: 'status.bookmark_folder', defaultMessage: 'Set bookmark folder' },
|
||||
bookmarkChangeFolder: { id: 'status.bookmark_folder_change', defaultMessage: 'Change bookmark folder' },
|
||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
|
||||
chat: { id: 'status.chat', defaultMessage: 'Chat with @{name}' },
|
||||
|
@ -112,6 +114,7 @@ interface IStatusActionBar {
|
|||
expandable?: boolean;
|
||||
space?: 'sm' | 'md' | 'lg';
|
||||
statusActionButtonTheme?: 'default' | 'inverse';
|
||||
fromBookmarks?: boolean;
|
||||
}
|
||||
|
||||
const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||
|
@ -120,6 +123,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
expandable = true,
|
||||
space = 'sm',
|
||||
statusActionButtonTheme = 'default',
|
||||
fromBookmarks = false,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
@ -201,6 +205,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
dispatch(toggleBookmark(status));
|
||||
};
|
||||
|
||||
const handleBookmarkFolderClick = () => {
|
||||
dispatch(openModal('SELECT_BOOKMARK_FOLDER', {
|
||||
statusId: status.id,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleExternalClick = () => {
|
||||
window.open(status.uri, '_blank');
|
||||
};
|
||||
|
@ -453,6 +463,14 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
});
|
||||
}
|
||||
|
||||
if (features.bookmarkFolders && fromBookmarks) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.pleroma.get('bookmark_folder') ? messages.bookmarkChangeFolder : messages.bookmarkSetFolder),
|
||||
action: handleBookmarkFolderClick,
|
||||
icon: require('@tabler/icons/outline/folders.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (features.federating && !account.local) {
|
||||
const { hostname: domain } = new URL(status.uri);
|
||||
menu.push({
|
||||
|
|
|
@ -129,6 +129,7 @@ const StatusList: React.FC<IStatusList> = ({
|
|||
contextType={timelineId}
|
||||
showGroup={showGroup}
|
||||
variant={divideType === 'border' ? 'slim' : 'rounded'}
|
||||
fromBookmarks={other.scrollKey === 'bookmarked_statuses'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -50,6 +50,7 @@ export interface IStatus {
|
|||
variant?: 'default' | 'rounded' | 'slim';
|
||||
showGroup?: boolean;
|
||||
accountAction?: React.ReactElement;
|
||||
fromBookmarks?: boolean;
|
||||
}
|
||||
|
||||
const Status: React.FC<IStatus> = (props) => {
|
||||
|
@ -69,6 +70,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
hideActionBar,
|
||||
variant = 'rounded',
|
||||
showGroup = true,
|
||||
fromBookmarks = false,
|
||||
} = props;
|
||||
|
||||
const intl = useIntl();
|
||||
|
@ -478,7 +480,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
|
||||
{(!hideActionBar && !isUnderReview) && (
|
||||
<div className='pt-4'>
|
||||
<StatusActionBar status={actualStatus} />
|
||||
<StatusActionBar status={actualStatus} fromBookmarks={fromBookmarks} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@ interface ICounter {
|
|||
/** A simple counter for notifications, etc. */
|
||||
const Counter: React.FC<ICounter> = ({ count, countMax }) => {
|
||||
return (
|
||||
<span className='flex h-5 min-w-[20px] max-w-[26px] items-center justify-center rounded-full bg-secondary-500 text-xs font-medium text-white ring-2 ring-white dark:ring-gray-800'>
|
||||
<span className='flex h-5 min-w-[20px] max-w-[26px] items-center justify-center rounded-full bg-secondary-500 text-xs font-medium text-white ring-2 ring-white black:ring-black dark:ring-gray-800'>
|
||||
{shortNumberFormat(count, countMax)}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -1,38 +1,43 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedDate, FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { deleteAnnouncement, fetchAdminAnnouncements, initAnnouncementModal } from 'soapbox/actions/admin';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { useAnnouncements } from 'soapbox/api/hooks/admin/useAnnouncements';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Button, Column, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { Announcement as AnnouncementEntity } from 'soapbox/types/entities';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { AdminAnnouncement } from 'soapbox/schemas';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.announcements', defaultMessage: 'Announcements' },
|
||||
deleteConfirm: { id: 'confirmations.admin.delete_announcement.confirm', defaultMessage: 'Delete' },
|
||||
deleteHeading: { id: 'confirmations.admin.delete_announcement.heading', defaultMessage: 'Delete announcement' },
|
||||
deleteMessage: { id: 'confirmations.admin.delete_announcement.message', defaultMessage: 'Are you sure you want to delete the announcement?' },
|
||||
deleteSuccess: { id: 'admin.edit_announcement.deleted', defaultMessage: 'Announcement deleted' },
|
||||
});
|
||||
|
||||
interface IAnnouncement {
|
||||
announcement: AnnouncementEntity;
|
||||
announcement: AdminAnnouncement;
|
||||
}
|
||||
|
||||
const Announcement: React.FC<IAnnouncement> = ({ announcement }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { deleteAnnouncement } = useAnnouncements();
|
||||
|
||||
const handleEditAnnouncement = (announcement: AnnouncementEntity) => () => {
|
||||
dispatch(initAnnouncementModal(announcement));
|
||||
const handleEditAnnouncement = () => {
|
||||
dispatch(openModal('EDIT_ANNOUNCEMENT', { announcement }));
|
||||
};
|
||||
|
||||
const handleDeleteAnnouncement = (id: string) => () => {
|
||||
const handleDeleteAnnouncement = () => {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.deleteHeading),
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => dispatch(deleteAnnouncement(id)),
|
||||
onConfirm: () => deleteAnnouncement(announcement.id, {
|
||||
onSuccess: () => toast.success(messages.deleteSuccess),
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
|
@ -68,10 +73,10 @@ const Announcement: React.FC<IAnnouncement> = ({ announcement }) => {
|
|||
</HStack>
|
||||
)}
|
||||
<HStack justifyContent='end' space={2}>
|
||||
<Button theme='primary' onClick={handleEditAnnouncement(announcement)}>
|
||||
<Button theme='primary' onClick={handleEditAnnouncement}>
|
||||
<FormattedMessage id='admin.announcements.edit' defaultMessage='Edit' />
|
||||
</Button>
|
||||
<Button theme='primary' onClick={handleDeleteAnnouncement(announcement.id)}>
|
||||
<Button theme='primary' onClick={handleDeleteAnnouncement}>
|
||||
<FormattedMessage id='admin.announcements.delete' defaultMessage='Delete' />
|
||||
</Button>
|
||||
</HStack>
|
||||
|
@ -84,15 +89,11 @@ const Announcements: React.FC = () => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const announcements = useAppSelector((state) => state.admin_announcements.items);
|
||||
const isLoading = useAppSelector((state) => state.admin_announcements.isLoading);
|
||||
const { data: announcements, isLoading } = useAnnouncements();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAdminAnnouncements());
|
||||
}, []);
|
||||
|
||||
const handleCreateAnnouncement = () => {
|
||||
dispatch(initAnnouncementModal());
|
||||
dispatch(openModal('EDIT_ANNOUNCEMENT'));
|
||||
};
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.admin.announcements' defaultMessage='There are no announcements yet.' />;
|
||||
|
@ -114,9 +115,9 @@ const Announcements: React.FC = () => {
|
|||
emptyMessage={emptyMessage}
|
||||
itemClassName='py-3 first:pt-0 last:pb-0'
|
||||
isLoading={isLoading}
|
||||
showLoading={isLoading && !announcements.count()}
|
||||
showLoading={isLoading && !announcements?.length}
|
||||
>
|
||||
{announcements.map((announcement) => (
|
||||
{announcements!.map((announcement) => (
|
||||
<Announcement key={announcement.id} announcement={announcement} />
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
|
|
@ -1,52 +1,80 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { changeAnnouncementAllDay, changeAnnouncementContent, changeAnnouncementEndTime, changeAnnouncementStartTime, handleCreateAnnouncement } from 'soapbox/actions/admin';
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import { useAnnouncements } from 'soapbox/api/hooks/admin/useAnnouncements';
|
||||
import { Form, FormGroup, HStack, Modal, Stack, Text, Textarea, Toggle } from 'soapbox/components/ui';
|
||||
import { DatePicker } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { AdminAnnouncement } from 'soapbox/schemas';
|
||||
|
||||
const messages = defineMessages({
|
||||
save: { id: 'admin.edit_announcement.save', defaultMessage: 'Save' },
|
||||
announcementContentPlaceholder: { id: 'admin.edit_announcement.fields.content_placeholder', defaultMessage: 'Announcement content' },
|
||||
announcementStartTimePlaceholder: { id: 'admin.edit_announcement.fields.start_time_placeholder', defaultMessage: 'Announcement starts on:' },
|
||||
announcementEndTimePlaceholder: { id: 'admin.edit_announcement.fields.end_time_placeholder', defaultMessage: 'Announcement ends on:' },
|
||||
announcementCreateSuccess: { id: 'admin.edit_announcement.created', defaultMessage: 'Announcement created' },
|
||||
announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' },
|
||||
});
|
||||
|
||||
interface IEditAnnouncementModal {
|
||||
onClose: (type?: string) => void;
|
||||
announcement?: AdminAnnouncement;
|
||||
}
|
||||
|
||||
const EditAnnouncementModal: React.FC<IEditAnnouncementModal> = ({ onClose }) => {
|
||||
const EditAnnouncementModal: React.FC<IEditAnnouncementModal> = ({ onClose, announcement }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { createAnnouncement, updateAnnouncement } = useAnnouncements();
|
||||
const intl = useIntl();
|
||||
|
||||
const id = useAppSelector((state) => state.admin_announcements.form.id);
|
||||
const content = useAppSelector((state) => state.admin_announcements.form.content);
|
||||
const startTime = useAppSelector((state) => state.admin_announcements.form.starts_at);
|
||||
const endTime = useAppSelector((state) => state.admin_announcements.form.ends_at);
|
||||
const allDay = useAppSelector((state) => state.admin_announcements.form.all_day);
|
||||
const [content, setContent] = useState(announcement?.content || '');
|
||||
const [startTime, setStartTime] = useState(announcement?.starts_at ? new Date(announcement.starts_at) : null);
|
||||
const [endTime, setEndTime] = useState(announcement?.ends_at ? new Date(announcement.ends_at) : null);
|
||||
const [allDay, setAllDay] = useState(announcement?.all_day || false);
|
||||
|
||||
const onChangeContent: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) =>
|
||||
dispatch(changeAnnouncementContent(target.value));
|
||||
const onChangeContent: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) => setContent(target.value);
|
||||
|
||||
const onChangeStartTime = (date: Date | null) => dispatch(changeAnnouncementStartTime(date));
|
||||
const onChangeStartTime = (date: Date | null) => setStartTime(date);
|
||||
|
||||
const onChangeEndTime = (date: Date | null) => dispatch(changeAnnouncementEndTime(date));
|
||||
const onChangeEndTime = (date: Date | null) => setEndTime(date);
|
||||
|
||||
const onChangeAllDay: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => dispatch(changeAnnouncementAllDay(target.checked));
|
||||
const onChangeAllDay: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => setAllDay(target.checked);
|
||||
|
||||
const onClickClose = () => {
|
||||
onClose('EDIT_ANNOUNCEMENT');
|
||||
};
|
||||
|
||||
const handleSubmit = () => dispatch(handleCreateAnnouncement()).then(() => dispatch(closeModal('EDIT_ANNOUNCEMENT')));
|
||||
const handleSubmit = () => {
|
||||
const form = {
|
||||
content,
|
||||
starts_at: startTime?.toISOString() || null,
|
||||
ends_at: endTime?.toISOString() || null,
|
||||
all_day: allDay,
|
||||
};
|
||||
|
||||
if (announcement) {
|
||||
updateAnnouncement({ ...form, id: announcement.id }, {
|
||||
onSuccess: () => {
|
||||
dispatch(closeModal('EDIT_ANNOUNCEMENT'));
|
||||
toast.success(messages.announcementUpdateSuccess);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
createAnnouncement(form, {
|
||||
onSuccess: () => {
|
||||
dispatch(closeModal('EDIT_ANNOUNCEMENT'));
|
||||
toast.success(messages.announcementCreateSuccess);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={onClickClose}
|
||||
title={id
|
||||
title={announcement
|
||||
? <FormattedMessage id='column.admin.edit_announcement' defaultMessage='Edit announcement' />
|
||||
: <FormattedMessage id='column.admin.create_announcement' defaultMessage='Create announcement' />}
|
||||
confirmationAction={handleSubmit}
|
||||
|
|
|
@ -7,7 +7,6 @@ import Markup from 'soapbox/components/markup';
|
|||
import { dateFormatOptions } from 'soapbox/components/relative-timestamp';
|
||||
import { Icon, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { badgeToTag, getBadges as getAccountBadges } from 'soapbox/utils/badges';
|
||||
import { capitalize } from 'soapbox/utils/strings';
|
||||
|
||||
import ProfileFamiliarFollowers from './profile-familiar-followers';
|
||||
|
@ -58,13 +57,14 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
|||
};
|
||||
|
||||
const getCustomBadges = (): React.ReactNode[] => {
|
||||
const badges = account ? getAccountBadges(account) : [];
|
||||
const badges = account?.roles || [];
|
||||
|
||||
return badges.map(badge => (
|
||||
return badges.filter(badge => badge.highlighted).map(badge => (
|
||||
<Badge
|
||||
key={badge}
|
||||
slug={badge}
|
||||
title={capitalize(badgeToTag(badge))}
|
||||
key={badge.id || badge.name}
|
||||
slug={badge.name}
|
||||
title={capitalize(badge.name)}
|
||||
color={badge.color}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@ import { Switch, useHistory, useLocation, Redirect } from 'react-router-dom';
|
|||
|
||||
import { fetchFollowRequests } from 'soapbox/actions/accounts';
|
||||
import { fetchReports, fetchUsers, fetchConfig } from 'soapbox/actions/admin';
|
||||
import { fetchAnnouncements } from 'soapbox/actions/announcements';
|
||||
import { fetchCustomEmojis } from 'soapbox/actions/custom-emojis';
|
||||
import { fetchFilters } from 'soapbox/actions/filters';
|
||||
import { fetchMarker } from 'soapbox/actions/markers';
|
||||
|
@ -418,8 +417,6 @@ const UI: React.FC<IUI> = ({ children }) => {
|
|||
.then(() => dispatch(fetchMarker(['notifications'])))
|
||||
.catch(console.error);
|
||||
|
||||
dispatch(fetchAnnouncements());
|
||||
|
||||
if (account.staff) {
|
||||
dispatch(fetchReports({ resolved: false }));
|
||||
dispatch(fetchUsers(['local', 'need_approval']));
|
||||
|
|
|
@ -1481,6 +1481,8 @@
|
|||
"status.approval.rejected": "Rejected",
|
||||
"status.bookmark": "Bookmark",
|
||||
"status.bookmark.select_folder": "Select folder",
|
||||
"status.bookmark_folder": "Set bookmark folder",
|
||||
"status.bookmark_folder_change": "Change bookmark folder",
|
||||
"status.bookmark_folder_changed": "Changed folder",
|
||||
"status.bookmarked": "Bookmark added.",
|
||||
"status.cancel_reblog_private": "Un-repost",
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
/**
|
||||
* Announcement reaction normalizer:
|
||||
* Converts API announcement emoji reactions into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/announcementreaction/}
|
||||
*/
|
||||
import { Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/announcement/
|
||||
export const AnnouncementReactionRecord = ImmutableRecord({
|
||||
name: '',
|
||||
count: 0,
|
||||
me: false,
|
||||
url: null as string | null,
|
||||
static_url: null as string | null,
|
||||
announcement_id: '',
|
||||
});
|
||||
|
||||
export const normalizeAnnouncementReaction = (announcementReaction: Record<string, any>, announcementId?: string) => {
|
||||
return AnnouncementReactionRecord(ImmutableMap(fromJS(announcementReaction)).withMutations(reaction => {
|
||||
reaction.set('announcement_id', announcementId as any);
|
||||
}));
|
||||
};
|
|
@ -1,89 +0,0 @@
|
|||
/**
|
||||
* Announcement normalizer:
|
||||
* Converts API announcements into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/announcement/}
|
||||
*/
|
||||
import {
|
||||
Map as ImmutableMap,
|
||||
List as ImmutableList,
|
||||
Record as ImmutableRecord,
|
||||
fromJS,
|
||||
} from 'immutable';
|
||||
|
||||
import emojify from 'soapbox/features/emoji';
|
||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||
|
||||
import { normalizeAnnouncementReaction } from './announcement-reaction';
|
||||
import { normalizeMention } from './mention';
|
||||
|
||||
import type { AnnouncementReaction, Emoji, Mention } from 'soapbox/types/entities';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/announcement/
|
||||
export const AnnouncementRecord = ImmutableRecord({
|
||||
id: '',
|
||||
content: '',
|
||||
starts_at: null as Date | null,
|
||||
ends_at: null as Date | null,
|
||||
all_day: false,
|
||||
read: false,
|
||||
published_at: Date,
|
||||
reactions: ImmutableList<AnnouncementReaction>(),
|
||||
statuses: ImmutableMap<string, string>(),
|
||||
mentions: ImmutableList<Mention>(),
|
||||
tags: ImmutableList<ImmutableMap<string, any>>(),
|
||||
emojis: ImmutableList<Emoji>(),
|
||||
updated_at: Date,
|
||||
|
||||
pleroma: ImmutableMap<string, any>(),
|
||||
|
||||
// Internal fields
|
||||
contentHtml: '',
|
||||
});
|
||||
|
||||
const normalizeMentions = (announcement: ImmutableMap<string, any>) => {
|
||||
return announcement.update('mentions', ImmutableList(), mentions => {
|
||||
return mentions.map(normalizeMention);
|
||||
});
|
||||
};
|
||||
|
||||
// Normalize reactions
|
||||
const normalizeReactions = (announcement: ImmutableMap<string, any>) => {
|
||||
return announcement.update('reactions', ImmutableList(), reactions => {
|
||||
return reactions.map((reaction: ImmutableMap<string, any>) => normalizeAnnouncementReaction(reaction, announcement.get('id')));
|
||||
});
|
||||
};
|
||||
|
||||
// Normalize emojis
|
||||
const normalizeEmojis = (announcement: ImmutableMap<string, any>) => {
|
||||
return announcement.update('emojis', ImmutableList(), emojis => {
|
||||
return emojis.map(normalizeEmoji);
|
||||
});
|
||||
};
|
||||
|
||||
const normalizeContent = (announcement: ImmutableMap<string, any>) => {
|
||||
const emojiMap = makeEmojiMap(announcement.get('emojis'));
|
||||
const contentHtml = emojify(announcement.get('content'), emojiMap);
|
||||
|
||||
return announcement.set('contentHtml', contentHtml);
|
||||
};
|
||||
|
||||
const normalizeStatuses = (announcement: ImmutableMap<string, any>) => {
|
||||
const statuses = announcement
|
||||
.get('statuses', ImmutableList())
|
||||
.reduce((acc: ImmutableMap<string, string>, curr: ImmutableMap<string, any>) => acc.set(curr.get('url'), `/@${curr.getIn(['account', 'acct'])}/${curr.get('id')}`), ImmutableMap());
|
||||
|
||||
return announcement.set('statuses', statuses);
|
||||
};
|
||||
|
||||
export const normalizeAnnouncement = (announcement: Record<string, any>) => {
|
||||
return AnnouncementRecord(
|
||||
ImmutableMap(fromJS(announcement)).withMutations(announcement => {
|
||||
normalizeMentions(announcement);
|
||||
normalizeReactions(announcement);
|
||||
normalizeEmojis(announcement);
|
||||
normalizeContent(announcement);
|
||||
normalizeStatuses(announcement);
|
||||
}),
|
||||
);
|
||||
};
|
|
@ -1,8 +1,6 @@
|
|||
export { AccountRecord, FieldRecord, normalizeAccount } from './account';
|
||||
export { AdminAccountRecord, normalizeAdminAccount } from './admin-account';
|
||||
export { AdminReportRecord, normalizeAdminReport } from './admin-report';
|
||||
export { AnnouncementRecord, normalizeAnnouncement } from './announcement';
|
||||
export { AnnouncementReactionRecord, normalizeAnnouncementReaction } from './announcement-reaction';
|
||||
export { AttachmentRecord, normalizeAttachment } from './attachment';
|
||||
export { ChatRecord, normalizeChat } from './chat';
|
||||
export { ChatMessageRecord, normalizeChatMessage } from './chat-message';
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import {
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_CONTENT,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_END_TIME,
|
||||
ADMIN_ANNOUNCEMENT_CHANGE_START_TIME,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_FAIL,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_REQUEST,
|
||||
ADMIN_ANNOUNCEMENT_CREATE_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_DELETE_SUCCESS,
|
||||
ADMIN_ANNOUNCEMENT_MODAL_INIT,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_FAIL,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_REQUEST,
|
||||
ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||
} from 'soapbox/actions/admin';
|
||||
import { normalizeAnnouncement } from 'soapbox/normalizers';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { Announcement, APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
const AnnouncementFormRecord = ImmutableRecord({
|
||||
id: null as string | null,
|
||||
content: '',
|
||||
starts_at: null as Date | null,
|
||||
ends_at: null as Date | null,
|
||||
all_day: false,
|
||||
is_submitting: false,
|
||||
});
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
items: ImmutableList<Announcement>(),
|
||||
isLoading: false,
|
||||
page: -1,
|
||||
form: AnnouncementFormRecord(),
|
||||
});
|
||||
|
||||
export default function adminAnnouncementsReducer(state = ReducerRecord(), action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case ADMIN_ANNOUNCEMENTS_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case ADMIN_ANNOUNCEMENTS_FETCH_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
const items = ImmutableList<Announcement>((action.announcements).map((announcement: APIEntity) => normalizeAnnouncement(announcement)));
|
||||
|
||||
map.set('items', items);
|
||||
map.set('isLoading', false);
|
||||
});
|
||||
case ADMIN_ANNOUNCEMENTS_FETCH_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
case ADMIN_ANNOUNCEMENT_DELETE_SUCCESS:
|
||||
return state.update('items', list => {
|
||||
const idx = list.findIndex(x => x.id === action.id);
|
||||
|
||||
if (idx > -1) {
|
||||
return list.delete(idx);
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
case ADMIN_ANNOUNCEMENT_CHANGE_CONTENT:
|
||||
return state.setIn(['form', 'content'], action.value);
|
||||
case ADMIN_ANNOUNCEMENT_CHANGE_START_TIME:
|
||||
return state.setIn(['form', 'starts_at'], action.value);
|
||||
case ADMIN_ANNOUNCEMENT_CHANGE_END_TIME:
|
||||
return state.setIn(['form', 'ends_at'], action.value);
|
||||
case ADMIN_ANNOUNCEMENT_CHANGE_ALL_DAY:
|
||||
return state.setIn(['form', 'all_day'], action.value);
|
||||
case ADMIN_ANNOUNCEMENT_CREATE_REQUEST:
|
||||
return state.setIn(['form', 'is_submitting'], true);
|
||||
case ADMIN_ANNOUNCEMENT_CREATE_SUCCESS:
|
||||
case ADMIN_ANNOUNCEMENT_CREATE_FAIL:
|
||||
return state.setIn(['form', 'is_submitting'], true);
|
||||
case ADMIN_ANNOUNCEMENT_MODAL_INIT:
|
||||
return state.set('form', action.announcement ? AnnouncementFormRecord({
|
||||
id: action.announcement.id,
|
||||
content: action.announcement.content,
|
||||
starts_at: action.announcement.starts_at ? new Date(action.announcement.starts_at) : null,
|
||||
ends_at: action.announcement.ends_at ? new Date(action.announcement.ends_at) : null,
|
||||
all_day: action.announcement.all_day,
|
||||
}) : AnnouncementFormRecord());
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
import { List as ImmutableList, Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
|
||||
|
||||
import announcements from 'soapbox/__fixtures__/announcements.json';
|
||||
import {
|
||||
ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||
ANNOUNCEMENTS_UPDATE,
|
||||
} from 'soapbox/actions/announcements';
|
||||
|
||||
import reducer from './announcements';
|
||||
|
||||
|
||||
describe('accounts reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {} as any)).toMatchObject({
|
||||
items: ImmutableList(),
|
||||
isLoading: false,
|
||||
show: false,
|
||||
unread: ImmutableSet(),
|
||||
});
|
||||
});
|
||||
|
||||
describe('ANNOUNCEMENTS_FETCH_SUCCESS', () => {
|
||||
it('parses announcements as Records', () => {
|
||||
const action = { type: ANNOUNCEMENTS_FETCH_SUCCESS, announcements };
|
||||
const result = reducer(undefined, action).items;
|
||||
|
||||
expect(result.every((announcement) => ImmutableRecord.isRecord(announcement))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ANNOUNCEMENTS_UPDATE', () => {
|
||||
it('updates announcements', () => {
|
||||
const state = reducer(undefined, { type: ANNOUNCEMENTS_FETCH_SUCCESS, announcements: [announcements[0]] });
|
||||
|
||||
const action = { type: ANNOUNCEMENTS_UPDATE, announcement: { ...announcements[0], content: '<p>Updated to Soapbox v3.0.0.</p>' } };
|
||||
const result = reducer(state, action).items;
|
||||
|
||||
expect(result.size === 1);
|
||||
expect(result.first()?.content === '<p>Updated to Soapbox v3.0.0.</p>');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,110 +0,0 @@
|
|||
import { List as ImmutableList, Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
|
||||
|
||||
import {
|
||||
ANNOUNCEMENTS_FETCH_REQUEST,
|
||||
ANNOUNCEMENTS_FETCH_SUCCESS,
|
||||
ANNOUNCEMENTS_FETCH_FAIL,
|
||||
ANNOUNCEMENTS_UPDATE,
|
||||
ANNOUNCEMENTS_REACTION_UPDATE,
|
||||
ANNOUNCEMENTS_REACTION_ADD_REQUEST,
|
||||
ANNOUNCEMENTS_REACTION_ADD_FAIL,
|
||||
ANNOUNCEMENTS_REACTION_REMOVE_REQUEST,
|
||||
ANNOUNCEMENTS_REACTION_REMOVE_FAIL,
|
||||
ANNOUNCEMENTS_TOGGLE_SHOW,
|
||||
ANNOUNCEMENTS_DELETE,
|
||||
ANNOUNCEMENTS_DISMISS_SUCCESS,
|
||||
} from 'soapbox/actions/announcements';
|
||||
import { normalizeAnnouncement, normalizeAnnouncementReaction } from 'soapbox/normalizers';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { Announcement, AnnouncementReaction, APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
items: ImmutableList<Announcement>(),
|
||||
isLoading: false,
|
||||
show: false,
|
||||
unread: ImmutableSet<string>(),
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
||||
const updateReaction = (state: State, id: string, name: string, updater: (a: AnnouncementReaction) => AnnouncementReaction) => state.update('items', list => list.map(announcement => {
|
||||
if (announcement.id === id) {
|
||||
return announcement.update('reactions', reactions => {
|
||||
const idx = reactions.findIndex(reaction => reaction.name === name);
|
||||
|
||||
if (idx > -1) {
|
||||
return reactions.update(idx, reaction => updater(reaction!));
|
||||
}
|
||||
|
||||
return reactions.push(updater(normalizeAnnouncementReaction({ name, count: 0 })));
|
||||
});
|
||||
}
|
||||
|
||||
return announcement;
|
||||
}));
|
||||
|
||||
const updateReactionCount = (state: State, reaction: AnnouncementReaction) => updateReaction(state, reaction.announcement_id, reaction.name, x => x.set('count', reaction.count));
|
||||
|
||||
const addReaction = (state: State, id: string, name: string) => updateReaction(state, id, name, (x: AnnouncementReaction) => x.set('me', true).update('count', y => y + 1));
|
||||
|
||||
const removeReaction = (state: State, id: string, name: string) => updateReaction(state, id, name, (x: AnnouncementReaction) => x.set('me', false).update('count', y => y - 1));
|
||||
|
||||
const sortAnnouncements = (list: ImmutableList<Announcement>) => list.sortBy(x => x.starts_at || x.published_at);
|
||||
|
||||
const updateAnnouncement = (state: State, announcement: Announcement) => {
|
||||
const idx = state.items.findIndex(x => x.id === announcement.id);
|
||||
|
||||
if (idx > -1) {
|
||||
// Deep merge is used because announcements from the streaming API do not contain
|
||||
// personalized data about which reactions have been selected by the given user,
|
||||
// and that is information we want to preserve
|
||||
return state.update('items', list => sortAnnouncements(list.update(idx, x => x!.mergeDeep(announcement))));
|
||||
}
|
||||
|
||||
return state.update('items', list => sortAnnouncements(list.unshift(announcement)));
|
||||
};
|
||||
|
||||
export default function announcementsReducer(state = ReducerRecord(), action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case ANNOUNCEMENTS_TOGGLE_SHOW:
|
||||
return state.withMutations(map => {
|
||||
map.set('show', !map.show);
|
||||
});
|
||||
case ANNOUNCEMENTS_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case ANNOUNCEMENTS_FETCH_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
const items = ImmutableList<Announcement>((action.announcements).map((announcement: APIEntity) => normalizeAnnouncement(announcement)));
|
||||
|
||||
map.set('items', items);
|
||||
map.set('isLoading', false);
|
||||
});
|
||||
case ANNOUNCEMENTS_FETCH_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
case ANNOUNCEMENTS_UPDATE:
|
||||
return updateAnnouncement(state, normalizeAnnouncement(action.announcement));
|
||||
case ANNOUNCEMENTS_REACTION_UPDATE:
|
||||
return updateReactionCount(state, action.reaction);
|
||||
case ANNOUNCEMENTS_REACTION_ADD_REQUEST:
|
||||
case ANNOUNCEMENTS_REACTION_REMOVE_FAIL:
|
||||
return addReaction(state, action.id, action.name);
|
||||
case ANNOUNCEMENTS_REACTION_REMOVE_REQUEST:
|
||||
case ANNOUNCEMENTS_REACTION_ADD_FAIL:
|
||||
return removeReaction(state, action.id, action.name);
|
||||
case ANNOUNCEMENTS_DISMISS_SUCCESS:
|
||||
return updateAnnouncement(state, normalizeAnnouncement({ id: action.id, read: true }));
|
||||
case ANNOUNCEMENTS_DELETE:
|
||||
return state.update('items', list => {
|
||||
const idx = list.findIndex(x => x.id === action.id);
|
||||
|
||||
if (idx > -1) {
|
||||
return list.delete(idx);
|
||||
}
|
||||
|
||||
return list;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -7,10 +7,8 @@ import entities from 'soapbox/entity-store/reducer';
|
|||
|
||||
import accounts_meta from './accounts-meta';
|
||||
import admin from './admin';
|
||||
import admin_announcements from './admin-announcements';
|
||||
import admin_user_index from './admin-user-index';
|
||||
import aliases from './aliases';
|
||||
import announcements from './announcements';
|
||||
import auth from './auth';
|
||||
import backups from './backups';
|
||||
import chat_message_lists from './chat-message-lists';
|
||||
|
@ -66,10 +64,8 @@ import user_lists from './user-lists';
|
|||
const reducers = {
|
||||
accounts_meta,
|
||||
admin,
|
||||
admin_announcements,
|
||||
admin_user_index,
|
||||
aliases,
|
||||
announcements,
|
||||
auth,
|
||||
backups,
|
||||
chat_message_lists,
|
||||
|
|
|
@ -72,25 +72,26 @@ const handleAuthFetch = (state: Instance) => {
|
|||
};
|
||||
};
|
||||
|
||||
const getHost = (instance: { uri: string }) => {
|
||||
const getHost = (instance: { uri?: string; domain?: string }) => {
|
||||
const domain = instance.uri || instance.domain as string;
|
||||
try {
|
||||
return new URL(instance.uri).host;
|
||||
return new URL(domain).host;
|
||||
} catch {
|
||||
try {
|
||||
return new URL(`https://${instance.uri}`).host;
|
||||
return new URL(`https://${domain}`).host;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const persistInstance = (instance: { uri: string }, host: string | null = getHost(instance)) => {
|
||||
const persistInstance = ({ instance }: { instance: { uri: string } }, host: string | null = getHost(instance)) => {
|
||||
if (host) {
|
||||
KVStore.setItem(`instance:${host}`, instance).catch(console.error);
|
||||
}
|
||||
};
|
||||
|
||||
const persistInstanceV2 = (instance: { uri: string }, host: string | null = getHost(instance)) => {
|
||||
const persistInstanceV2 = ({ instance }: { instance: { domain: string } }, host: string | null = getHost(instance)) => {
|
||||
if (host) {
|
||||
KVStore.setItem(`instanceV2:${host}`, instance).catch(console.error);
|
||||
}
|
||||
|
|
|
@ -17,12 +17,21 @@ const headerMissing = require('soapbox/assets/images/header-missing.png');
|
|||
|
||||
const birthdaySchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
||||
|
||||
const hexSchema = z.string().regex(/^#[a-f0-9]{6}$/i);
|
||||
|
||||
const fieldSchema = z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
verified_at: z.string().datetime().nullable().catch(null),
|
||||
});
|
||||
|
||||
const roleSchema = z.object({
|
||||
id: z.string().catch(''),
|
||||
name: z.string().catch(''),
|
||||
color: hexSchema.catch(''),
|
||||
highlighted: z.boolean().catch(true),
|
||||
});
|
||||
|
||||
const baseAccountSchema = z.object({
|
||||
acct: z.string().catch(''),
|
||||
avatar: z.string().catch(avatarMissing),
|
||||
|
@ -85,6 +94,7 @@ const baseAccountSchema = z.object({
|
|||
relationship: relationshipSchema.optional().catch(undefined),
|
||||
tags: z.array(z.string()).catch([]),
|
||||
}).optional().catch(undefined),
|
||||
roles: filteredArray(roleSchema),
|
||||
source: z.object({
|
||||
approved: z.boolean().catch(true),
|
||||
chats_onboarded: z.boolean().catch(true),
|
||||
|
@ -118,6 +128,9 @@ const getDomain = (url: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const filterBadges = (tags?: string[]) =>
|
||||
tags?.filter(tag => tag.startsWith('badge:')).map(tag => ({ id: tag, name: tag.replace(/^badge:/, '') }));
|
||||
|
||||
/** Add internal fields to the account. */
|
||||
const transformAccount = <T extends TransformableAccount>({ pleroma, other_settings, fields, ...account }: T) => {
|
||||
const customEmojiMap = makeCustomEmojiMap(account.emojis);
|
||||
|
@ -156,6 +169,7 @@ const transformAccount = <T extends TransformableAccount>({ pleroma, other_setti
|
|||
const { relationship, ...rest } = pleroma;
|
||||
return rest;
|
||||
})(),
|
||||
roles: account.roles || filterBadges(pleroma?.tags),
|
||||
relationship: pleroma?.relationship,
|
||||
staff: pleroma?.is_admin || pleroma?.is_moderator || false,
|
||||
suspended: account.suspended || pleroma?.deactivated || false,
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { Resolve } from 'soapbox/utils/types';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/announcement/
|
||||
const announcementReactionSchema = z.object({
|
||||
name: z.string().catch(''),
|
||||
count: z.number().int().nonnegative().catch(0),
|
||||
me: z.boolean().catch(false),
|
||||
url: z.string().nullable().catch(null),
|
||||
static_url: z.string().nullable().catch(null),
|
||||
announcement_id: z.string().catch(''),
|
||||
});
|
||||
|
||||
type AnnouncementReaction = Resolve<z.infer<typeof announcementReactionSchema>>;
|
||||
|
||||
export { announcementReactionSchema, type AnnouncementReaction };
|
|
@ -0,0 +1,58 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import emojify from 'soapbox/features/emoji';
|
||||
|
||||
import { announcementReactionSchema } from './announcement-reaction';
|
||||
import { customEmojiSchema } from './custom-emoji';
|
||||
import { mentionSchema } from './mention';
|
||||
import { tagSchema } from './tag';
|
||||
import { dateSchema, filteredArray, makeCustomEmojiMap } from './utils';
|
||||
|
||||
import type { Resolve } from 'soapbox/utils/types';
|
||||
|
||||
const transformAnnouncement = (announcement: Resolve<z.infer<typeof baseAnnouncementSchema>>) => {
|
||||
const emojiMap = makeCustomEmojiMap(announcement.emojis);
|
||||
|
||||
const contentHtml = emojify(announcement.content, emojiMap);
|
||||
|
||||
return {
|
||||
...announcement,
|
||||
contentHtml,
|
||||
};
|
||||
};
|
||||
|
||||
// https://docs.joinmastodon.org/entities/announcement/
|
||||
const baseAnnouncementSchema = z.object({
|
||||
id: z.string(),
|
||||
content: z.string().catch(''),
|
||||
starts_at: z.string().datetime().nullable().catch(null),
|
||||
ends_at: z.string().datetime().nullable().catch(null),
|
||||
all_day: z.boolean().catch(false),
|
||||
read: z.boolean().catch(false),
|
||||
published_at: dateSchema,
|
||||
reactions: filteredArray(announcementReactionSchema),
|
||||
statuses: z.preprocess(
|
||||
(statuses: any) => Array.isArray(statuses)
|
||||
? Object.fromEntries(statuses.map((status: any) => [status.url, status.account?.acct]) || [])
|
||||
: statuses,
|
||||
z.record(z.string(), z.string()),
|
||||
),
|
||||
mentions: filteredArray(mentionSchema),
|
||||
tags: filteredArray(tagSchema),
|
||||
emojis: filteredArray(customEmojiSchema),
|
||||
updated_at: dateSchema,
|
||||
});
|
||||
|
||||
const announcementSchema = baseAnnouncementSchema.transform(transformAnnouncement);
|
||||
|
||||
type Announcement = Resolve<z.infer<typeof announcementSchema>>;
|
||||
|
||||
const adminAnnouncementSchema = baseAnnouncementSchema.extend({
|
||||
pleroma: z.object({
|
||||
raw_content: z.string().catch(''),
|
||||
}),
|
||||
}).transform(transformAnnouncement);
|
||||
|
||||
type AdminAnnouncement = Resolve<z.infer<typeof adminAnnouncementSchema>>;
|
||||
|
||||
export { announcementSchema, adminAnnouncementSchema, type Announcement, type AdminAnnouncement };
|
|
@ -1,4 +1,6 @@
|
|||
export { accountSchema, type Account } from './account';
|
||||
export { announcementSchema, adminAnnouncementSchema, type Announcement, type AdminAnnouncement } from './announcement';
|
||||
export { announcementReactionSchema, type AnnouncementReaction } from './announcement-reaction';
|
||||
export { attachmentSchema, type Attachment } from './attachment';
|
||||
export { bookmarkFolderSchema, type BookmarkFolder } from './bookmark-folder';
|
||||
export { cardSchema, type Card } from './card';
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import {
|
||||
AdminAccountRecord,
|
||||
AdminReportRecord,
|
||||
AnnouncementRecord,
|
||||
AnnouncementReactionRecord,
|
||||
AttachmentRecord,
|
||||
ChatRecord,
|
||||
ChatMessageRecord,
|
||||
|
@ -27,8 +25,6 @@ import type { LegacyMap } from 'soapbox/utils/legacy';
|
|||
|
||||
type AdminAccount = ReturnType<typeof AdminAccountRecord>;
|
||||
type AdminReport = ReturnType<typeof AdminReportRecord>;
|
||||
type Announcement = ReturnType<typeof AnnouncementRecord>;
|
||||
type AnnouncementReaction = ReturnType<typeof AnnouncementReactionRecord>;
|
||||
type Attachment = ReturnType<typeof AttachmentRecord>;
|
||||
type Chat = ReturnType<typeof ChatRecord>;
|
||||
type ChatMessage = ReturnType<typeof ChatMessageRecord>;
|
||||
|
@ -61,8 +57,6 @@ export {
|
|||
Account,
|
||||
AdminAccount,
|
||||
AdminReport,
|
||||
Announcement,
|
||||
AnnouncementReaction,
|
||||
Attachment,
|
||||
Chat,
|
||||
ChatMessage,
|
||||
|
|
|
@ -231,7 +231,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
* @see DELETE /api/v1/announcements/:id/reactions/:name
|
||||
* @see {@link https://docs.joinmastodon.org/methods/announcements/}
|
||||
*/
|
||||
announcementsReactions: v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
||||
announcementsReactions: true, // v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
||||
|
||||
/**
|
||||
* Pleroma backups.
|
||||
|
|
|
@ -117,7 +117,7 @@ export const generateThemeCss = (soapboxConfig: SoapboxConfig): string => {
|
|||
return colorsToCss(soapboxConfig.colors.toJS() as TailwindColorPalette);
|
||||
};
|
||||
|
||||
const hexToHsl = (hex: string): Hsl | null => {
|
||||
export const hexToHsl = (hex: string): Hsl | null => {
|
||||
const rgb = hexToRgb(hex);
|
||||
return rgb ? rgbToHsl(rgb) : null;
|
||||
};
|
||||
|
|
Ładowanie…
Reference in New Issue