Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
update-emoji-mart^2
marcin mikołajczak 2022-07-06 23:25:19 +02:00
rodzic 82e437cdda
commit b3b6a7e4bc
15 zmienionych plików z 595 dodań i 3 usunięć

Wyświetl plik

@ -0,0 +1,187 @@
import api from '../api';
import type { AxiosError } from 'axios';
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) => {
dispatch(fetchAnnouncementsRequest());
return api(getState).get('/api/v1/announcements').then(response => {
dispatch(fetchAnnouncementsSuccess(response.data));
}).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: AxiosError) => ({
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));
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: AxiosError) => ({
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));
}
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: AxiosError) => ({
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));
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: AxiosError) => ({
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,
});

Wyświetl plik

@ -3,6 +3,12 @@ import messages from 'soapbox/locales/messages';
import { connectStream } from '../stream';
import {
deleteAnnouncement,
fetchAnnouncements,
updateAnnouncements,
updateReaction as updateAnnouncementsReaction,
} from './announcements';
import { updateConversations } from './conversations';
import { fetchFilters } from './filters';
import { updateNotificationsQueue, expandNotifications } from './notifications';
@ -100,13 +106,24 @@ const connectTimelineStream = (
case 'pleroma:follow_relationships_update':
dispatch(updateFollowRelationships(JSON.parse(data.payload)));
break;
case 'announcement':
dispatch(updateAnnouncements(JSON.parse(data.payload)));
break;
case 'announcement.reaction':
dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
break;
case 'announcement.delete':
dispatch(deleteAnnouncement(data.payload));
break;
}
},
};
});
const refreshHomeTimelineAndNotification = (dispatch: AppDispatch, done?: () => void) =>
dispatch(expandHomeTimeline({}, () => dispatch(expandNotifications({} as any, done))));
dispatch(expandHomeTimeline({}, () =>
dispatch(expandNotifications({}, () =>
dispatch(fetchAnnouncements(done))))));
const connectUserStream = () =>
connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);

Wyświetl plik

@ -7,7 +7,7 @@ import snackbar from 'soapbox/actions/snackbar';
import { Button, Form, FormGroup, Input, FormActions, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
const messages = defineMessages({
const messages = defineMessages({
mfa_setup_disable_button: { id: 'column.mfa_disable_button', defaultMessage: 'Disable' },
disableFail: { id: 'security.disable.fail', defaultMessage: 'Incorrect password. Try again.' },
mfaDisableSuccess: { id: 'mfa.disable.success_message', defaultMessage: 'MFA disabled' },

Wyświetl plik

@ -7,7 +7,7 @@ import snackbar from 'soapbox/actions/snackbar';
import { Button, FormActions, Spinner, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
const messages = defineMessages({
const messages = defineMessages({
mfaCancelButton: { id: 'column.mfa_cancel', defaultMessage: 'Cancel' },
mfaSetupButton: { id: 'column.mfa_setup', defaultMessage: 'Proceed to Setup' },
codesFail: { id: 'security.codes.fail', defaultMessage: 'Failed to fetch backup codes' },

Wyświetl plik

@ -0,0 +1,161 @@
import classNames from 'classnames';
import React, { useEffect, useRef, useState } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';
import ReactSwipeableViews from 'react-swipeable-views';
import { Card, HStack, Widget } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/types/entities';
const AnnouncementContent = ({ announcement }: { announcement: AnnouncementEntity }) => {
const history = useHistory();
const node = useRef<HTMLDivElement>(null);
useEffect(() => {
updateLinks();
});
const onMentionClick = (mention: MentionEntity, e: MouseEvent) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
history.push(`/@${mention.acct}`);
}
};
const onHashtagClick = (hashtag: string, e: MouseEvent) => {
hashtag = hashtag.replace(/^#/, '').toLowerCase();
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
history.push(`/tags/${hashtag}`);
}
};
/** For regular links, just stop propogation */
const onLinkClick = (e: MouseEvent) => {
e.stopPropagation();
};
const updateLinks = () => {
if (!node.current) return;
const links = node.current.querySelectorAll('a');
links.forEach(link => {
// Skip already processed
if (link.classList.contains('status-link')) return;
// Add attributes
link.classList.add('status-link');
link.setAttribute('rel', 'nofollow noopener');
link.setAttribute('target', '_blank');
const mention = announcement.mentions.find(mention => link.href === `${mention.url}`);
// Add event listeners on mentions and hashtags
if (mention) {
link.addEventListener('click', onMentionClick.bind(link, mention), false);
link.setAttribute('title', mention.acct);
} 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 {
link.setAttribute('title', link.href);
link.addEventListener('click', onLinkClick.bind(link), false);
}
});
};
return (
<div
className='translate text-sm'
ref={node}
dangerouslySetInnerHTML={{ __html: announcement.content }}
// onMouseEnter={handleMouseEnter}
// onMouseLeave={handleMouseLeave}
/>
);
};
const Announcement = ({ announcement }: { announcement: AnnouncementEntity }) => {
const startsAt = announcement.starts_at && new Date(announcement.starts_at);
const endsAt = announcement.ends_at && new Date(announcement.ends_at);
const now = new Date();
const hasTimeRange = startsAt && endsAt;
const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
const skipTime = announcement.all_day;
return (
<div>
<strong>
{hasTimeRange && <span> · <FormattedDate value={startsAt} hour12={false} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} hour12={false} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
</strong>
<AnnouncementContent announcement={announcement} />
{/* <ReactionsBar
reactions={announcement.get('reactions')}
announcementId={announcement.get('id')}
addReaction={this.props.addReaction}
removeReaction={this.props.removeReaction}
emojiMap={this.props.emojiMap}
/> */}
</div>
);
};
const AnnouncementsPanel = () => {
// const dispatch = useDispatch();
const [index, setIndex] = useState(0);
const announcements = useAppSelector((state) => state.announcements.items);
if (announcements.size === 0) return null;
const handleChangeIndex = (index: number) => {
setIndex(index % announcements.size);
};
return (
<Widget title={<FormattedMessage id='announcements.title' defaultMessage='Announcements' />}>
<Card className='relative' size='lg' variant='rounded'>
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
{announcements.map((announcement) => (
<Announcement
key={announcement.id}
announcement={announcement}
// emojiMap={emojiMap}
// addReaction={addReaction}
// removeReaction={removeReaction}
// selected={index === idx}
// disabled={disableSwiping}
/>
)).reverse()}
</ReactSwipeableViews>
<HStack space={2} alignItems='center' justifyContent='center' className='relative'>
{announcements.map((_, i) => (
<button
key={i}
tabIndex={0}
onClick={() => setIndex(i)}
className={classNames({
'w-2 h-2 rounded-full focus:ring-primary-600 focus:ring-2 focus:ring-offset-2': true,
'bg-gray-200 hover:bg-gray-300': i !== index,
'bg-primary-600': i === index,
})}
/>
))}
</HStack>
</Card>
</Widget>
);
};
export default AnnouncementsPanel;

Wyświetl plik

@ -9,6 +9,7 @@ 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 { fetchChats } from 'soapbox/actions/chats';
import { uploadCompose, resetCompose } from 'soapbox/actions/compose';
import { fetchCustomEmojis } from 'soapbox/actions/custom_emojis';
@ -448,6 +449,8 @@ const UI: React.FC = ({ children }) => {
.then(() => dispatch(fetchMarker(['notifications'])))
.catch(console.error);
dispatch(fetchAnnouncements());
if (features.chats) {
dispatch(fetchChats());
}

Wyświetl plik

@ -521,3 +521,7 @@ export function VerifySmsModal() {
export function FamiliarFollowersModal() {
return import(/*webpackChunkName: "modals/familiar_followers_modal" */'../components/familiar_followers_modal');
}
export function AnnouncementsPanel() {
return import(/* webpackChunkName: "features/announcements" */'../components/announcements-panel');
}

Wyświetl plik

@ -0,0 +1,65 @@
/**
* 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 { normalizeEmoji } from 'soapbox/normalizers/emoji';
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,
mentions: ImmutableList<Mention>(),
tags: ImmutableList<ImmutableMap<string, any>>(),
emojis: ImmutableList<Emoji>(),
updated_at: Date,
});
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);
});
};
export const normalizeAnnouncement = (announcement: Record<string, any>) => {
return AnnouncementRecord(
ImmutableMap(fromJS(announcement)).withMutations(announcement => {
normalizeMentions(announcement);
normalizeReactions(announcement);
normalizeEmojis(announcement);
}),
);
};

Wyświetl plik

@ -0,0 +1,22 @@
/**
* 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);
}));
};

Wyświetl plik

@ -1,6 +1,8 @@
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 { CardRecord, normalizeCard } from './card';
export { ChatRecord, normalizeChat } from './chat';

Wyświetl plik

@ -12,6 +12,7 @@ import {
CryptoDonatePanel,
BirthdayPanel,
CtaBanner,
AnnouncementsPanel,
} from 'soapbox/features/ui/util/async-components';
import { useAppSelector, useOwnAccount, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
@ -74,6 +75,11 @@ const HomePage: React.FC = ({ children }) => {
{Component => <Component />}
</BundleContainer>
)}
{me && features.announcements && (
<BundleContainer fetchComponent={AnnouncementsPanel}>
{Component => <Component key='announcements-panel' />}
</BundleContainer>
)}
{features.trends && (
<BundleContainer fetchComponent={TrendsPanel}>
{Component => <Component limit={3} />}

Wyświetl plik

@ -0,0 +1,112 @@
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 { normalizeAnnouncementReaction } from 'soapbox/normalizers';
import { normalizeAnnouncement } from 'soapbox/normalizers/announcement';
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;
}
}

Wyświetl plik

@ -12,6 +12,7 @@ import admin from './admin';
import admin_log from './admin_log';
import alerts from './alerts';
import aliases from './aliases';
import announcements from './announcements';
import auth from './auth';
import backups from './backups';
import carousels from './carousels';
@ -124,6 +125,7 @@ const reducers = {
rules,
history,
carousels,
announcements,
};
// Build a default state from all reducers: it has the key and `undefined`

Wyświetl plik

@ -2,6 +2,8 @@ import {
AdminAccountRecord,
AdminReportRecord,
AccountRecord,
AnnouncementRecord,
AnnouncementReactionRecord,
AttachmentRecord,
CardRecord,
ChatRecord,
@ -26,6 +28,8 @@ import type { Record as ImmutableRecord } from 'immutable';
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 Card = ReturnType<typeof CardRecord>;
type Chat = ReturnType<typeof ChatRecord>;
@ -64,6 +68,8 @@ export {
AdminAccount,
AdminReport,
Account,
Announcement,
AnnouncementReaction,
Attachment,
Card,
Chat,

Wyświetl plik

@ -142,6 +142,11 @@ const getInstanceFeatures = (instance: Instance) => {
*/
accountWebsite: v.software === TRUTHSOCIAL,
announcements: any([
v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
v.software === PLEROMA && gte(v.version, '2.2.49'),
]),
/**
* Set your birthday and view upcoming birthdays.
* @see GET /api/v1/pleroma/birthdays