Merge remote-tracking branch 'parent/main' into upstream-20240822
This commit is contained in:
commit
55f11765ea
34 changed files with 401 additions and 601 deletions
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module KmyblueCapabilitiesHelper
|
module KmyblueCapabilitiesHelper
|
||||||
|
KMYBLUE_API_VERSION = 1
|
||||||
|
|
||||||
def fedibird_capabilities
|
def fedibird_capabilities
|
||||||
capabilities = %i(
|
capabilities = %i(
|
||||||
enable_wide_emoji
|
enable_wide_emoji
|
||||||
|
|
|
@ -746,12 +746,12 @@ export function expandMentionedUsersFail(id, error) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReblogWithoutConfirmation(status, privacy) {
|
function toggleReblogWithoutConfirmation(status, visibility) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog({ statusId: status.get('id') }));
|
dispatch(unreblog({ statusId: status.get('id') }));
|
||||||
} else {
|
} else {
|
||||||
dispatch(reblog({ statusId: status.get('id'), privacy }));
|
dispatch(reblog({ statusId: status.get('id'), visibility }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
} from 'mastodon/api_types/notifications';
|
} from 'mastodon/api_types/notifications';
|
||||||
import { allNotificationTypes } from 'mastodon/api_types/notifications';
|
import { allNotificationTypes } from 'mastodon/api_types/notifications';
|
||||||
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||||
|
import { usePendingItems } from 'mastodon/initial_state';
|
||||||
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
|
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
|
||||||
import {
|
import {
|
||||||
selectSettingsNotificationsExcludedTypes,
|
selectSettingsNotificationsExcludedTypes,
|
||||||
|
@ -103,6 +104,28 @@ export const fetchNotificationsGap = createDataLoadingThunk(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const pollRecentNotifications = createDataLoadingThunk(
|
||||||
|
'notificationGroups/pollRecentNotifications',
|
||||||
|
async (_params, { getState }) => {
|
||||||
|
return apiFetchNotifications({
|
||||||
|
max_id: undefined,
|
||||||
|
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
|
||||||
|
since_id: usePendingItems
|
||||||
|
? getState().notificationGroups.groups.find(
|
||||||
|
(group) => group.type !== 'gap',
|
||||||
|
)?.page_max_id
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||||
|
dispatch(importFetchedAccounts(accounts));
|
||||||
|
dispatch(importFetchedStatuses(statuses));
|
||||||
|
dispatchAssociatedRecords(dispatch, notifications);
|
||||||
|
|
||||||
|
return { notifications };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const processNewNotificationForGroups = createAppAsyncThunk(
|
export const processNewNotificationForGroups = createAppAsyncThunk(
|
||||||
'notificationGroups/processNew',
|
'notificationGroups/processNew',
|
||||||
(notification: ApiNotificationJSON, { dispatch, getState }) => {
|
(notification: ApiNotificationJSON, { dispatch, getState }) => {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
deleteAnnouncement,
|
deleteAnnouncement,
|
||||||
} from './announcements';
|
} from './announcements';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
import { processNewNotificationForGroups, refreshStaleNotificationGroups } from './notification_groups';
|
import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups';
|
||||||
import { updateNotifications, expandNotifications, updateEmojiReactions } from './notifications';
|
import { updateNotifications, expandNotifications, updateEmojiReactions } from './notifications';
|
||||||
import { updateStatus } from './statuses';
|
import { updateStatus } from './statuses';
|
||||||
import {
|
import {
|
||||||
|
@ -38,7 +38,7 @@ const randomUpTo = max =>
|
||||||
* @param {string} channelName
|
* @param {string} channelName
|
||||||
* @param {Object.<string, string>} params
|
* @param {Object.<string, string>} params
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {function(Function): Promise<void>} [options.fallback]
|
* @param {function(Function, Function): Promise<void>} [options.fallback]
|
||||||
* @param {function(): void} [options.fillGaps]
|
* @param {function(): void} [options.fillGaps]
|
||||||
* @param {function(object): boolean} [options.accept]
|
* @param {function(object): boolean} [options.accept]
|
||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
|
@ -53,11 +53,11 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
let pollingId;
|
let pollingId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function(Function): Promise<void>} fallback
|
* @param {function(Function, Function): Promise<void>} fallback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const useFallback = async fallback => {
|
const useFallback = async fallback => {
|
||||||
await fallback(dispatch);
|
await fallback(dispatch, getState);
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
|
||||||
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
|
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
|
||||||
};
|
};
|
||||||
|
@ -144,10 +144,23 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} dispatch
|
* @param {Function} dispatch
|
||||||
|
* @param {Function} getState
|
||||||
*/
|
*/
|
||||||
async function refreshHomeTimelineAndNotification(dispatch) {
|
async function refreshHomeTimelineAndNotification(dispatch, getState) {
|
||||||
await dispatch(expandHomeTimeline({ maxId: undefined }));
|
await dispatch(expandHomeTimeline({ maxId: undefined }));
|
||||||
await dispatch(expandNotifications({}));
|
|
||||||
|
// TODO: remove this once the groups feature replaces the previous one
|
||||||
|
if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
|
||||||
|
// TODO: polling for merged notifications
|
||||||
|
try {
|
||||||
|
await dispatch(pollRecentGroupNotifications());
|
||||||
|
} catch (error) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await dispatch(expandNotifications({}));
|
||||||
|
}
|
||||||
|
|
||||||
await dispatch(fetchAnnouncements());
|
await dispatch(fetchAnnouncements());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ const exceptInvalidNotifications = (
|
||||||
export const apiFetchNotifications = async (params?: {
|
export const apiFetchNotifications = async (params?: {
|
||||||
exclude_types?: string[];
|
exclude_types?: string[];
|
||||||
max_id?: string;
|
max_id?: string;
|
||||||
|
since_id?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const response = await api().request<ApiNotificationGroupsResultJSON>({
|
const response = await api().request<ApiNotificationGroupsResultJSON>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
export const DisplayedName: React.FC<{
|
||||||
|
accountIds: string[];
|
||||||
|
}> = ({ accountIds }) => {
|
||||||
|
const lastAccountId = accountIds[0] ?? '0';
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(lastAccountId));
|
||||||
|
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`/@${account.acct}`}
|
||||||
|
title={`@${account.acct}`}
|
||||||
|
data-hover-card-account={account.id}
|
||||||
|
>
|
||||||
|
<bdi dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,51 +0,0 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useAppSelector } from 'mastodon/store';
|
|
||||||
|
|
||||||
export const NamesList: React.FC<{
|
|
||||||
accountIds: string[];
|
|
||||||
total: number;
|
|
||||||
seeMoreHref?: string;
|
|
||||||
}> = ({ accountIds, total, seeMoreHref }) => {
|
|
||||||
const lastAccountId = accountIds[0] ?? '0';
|
|
||||||
const account = useAppSelector((state) => state.accounts.get(lastAccountId));
|
|
||||||
|
|
||||||
if (!account) return null;
|
|
||||||
|
|
||||||
const displayedName = (
|
|
||||||
<Link
|
|
||||||
to={`/@${account.acct}`}
|
|
||||||
title={`@${account.acct}`}
|
|
||||||
data-hover-card-account={account.id}
|
|
||||||
>
|
|
||||||
<bdi dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (total === 1) {
|
|
||||||
return displayedName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seeMoreHref)
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
id='name_and_others_with_link'
|
|
||||||
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a>'
|
|
||||||
values={{
|
|
||||||
name: displayedName,
|
|
||||||
count: total - 1,
|
|
||||||
a: (chunks) => <Link to={seeMoreHref}>{chunks}</Link>,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormattedMessage
|
|
||||||
id='name_and_others'
|
|
||||||
defaultMessage='{name} and {count, plural, one {# other} other {# others}}'
|
|
||||||
values={{ name: displayedName, count: total - 1 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -6,13 +6,27 @@ import type { NotificationGroupAdminSignUp } from 'mastodon/models/notification_
|
||||||
import type { LabelRenderer } from './notification_group_with_status';
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
const labelRenderer: LabelRenderer = (values) => (
|
const labelRenderer: LabelRenderer = (displayedName, total) => {
|
||||||
<FormattedMessage
|
if (total === 1)
|
||||||
id='notification.admin.sign_up'
|
return (
|
||||||
defaultMessage='{name} signed up'
|
<FormattedMessage
|
||||||
values={values}
|
id='notification.admin.sign_up'
|
||||||
/>
|
defaultMessage='{name} signed up'
|
||||||
);
|
values={{ name: displayedName }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.admin.sign_up.name_and_others'
|
||||||
|
defaultMessage='{name} and {count, plural, one {# other} other {# others}} signed up'
|
||||||
|
values={{
|
||||||
|
name: displayedName,
|
||||||
|
count: total - 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const NotificationAdminSignUp: React.FC<{
|
export const NotificationAdminSignUp: React.FC<{
|
||||||
notification: NotificationGroupAdminSignUp;
|
notification: NotificationGroupAdminSignUp;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import EmojiReactionIcon from '@/material-icons/400-24px/mood.svg?react';
|
import EmojiReactionIcon from '@/material-icons/400-24px/mood.svg?react';
|
||||||
import type { NotificationGroupEmojiReaction } from 'mastodon/models/notification_group';
|
import type { NotificationGroupEmojiReaction } from 'mastodon/models/notification_group';
|
||||||
import { useAppSelector } from 'mastodon/store';
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
@ -7,13 +9,29 @@ import { useAppSelector } from 'mastodon/store';
|
||||||
import type { LabelRenderer } from './notification_group_with_status';
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
const labelRenderer: LabelRenderer = (values) => (
|
const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
|
||||||
<FormattedMessage
|
if (total === 1)
|
||||||
id='notification.emoji_reaction'
|
return (
|
||||||
defaultMessage='{name} reacted your status with emoji'
|
<FormattedMessage
|
||||||
values={values}
|
id='notification.emoji_reaction'
|
||||||
/>
|
defaultMessage='{name} reacted your status with emoji'
|
||||||
);
|
values={{ name: displayedName }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.emoji_reaction.name_and_others_with_link'
|
||||||
|
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> reacted your post with emoji'
|
||||||
|
values={{
|
||||||
|
name: displayedName,
|
||||||
|
count: total - 1,
|
||||||
|
a: (chunks) =>
|
||||||
|
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const NotificationEmojiReaction: React.FC<{
|
export const NotificationEmojiReaction: React.FC<{
|
||||||
notification: NotificationGroupEmojiReaction;
|
notification: NotificationGroupEmojiReaction;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||||
import type { NotificationGroupFavourite } from 'mastodon/models/notification_group';
|
import type { NotificationGroupFavourite } from 'mastodon/models/notification_group';
|
||||||
import { useAppSelector } from 'mastodon/store';
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
@ -7,13 +9,29 @@ import { useAppSelector } from 'mastodon/store';
|
||||||
import type { LabelRenderer } from './notification_group_with_status';
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
const labelRenderer: LabelRenderer = (values) => (
|
const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
|
||||||
<FormattedMessage
|
if (total === 1)
|
||||||
id='notification.favourite'
|
return (
|
||||||
defaultMessage='{name} favorited your status'
|
<FormattedMessage
|
||||||
values={values}
|
id='notification.favourite'
|
||||||
/>
|
defaultMessage='{name} favorited your status'
|
||||||
);
|
values={{ name: displayedName }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.favourite.name_and_others_with_link'
|
||||||
|
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post'
|
||||||
|
values={{
|
||||||
|
name: displayedName,
|
||||||
|
count: total - 1,
|
||||||
|
a: (chunks) =>
|
||||||
|
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const NotificationFavourite: React.FC<{
|
export const NotificationFavourite: React.FC<{
|
||||||
notification: NotificationGroupFavourite;
|
notification: NotificationGroupFavourite;
|
||||||
|
|
|
@ -10,13 +10,27 @@ import { useAppSelector } from 'mastodon/store';
|
||||||
import type { LabelRenderer } from './notification_group_with_status';
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
const labelRenderer: LabelRenderer = (values) => (
|
const labelRenderer: LabelRenderer = (displayedName, total) => {
|
||||||
<FormattedMessage
|
if (total === 1)
|
||||||
id='notification.follow'
|
return (
|
||||||
defaultMessage='{name} followed you'
|
<FormattedMessage
|
||||||
values={values}
|
id='notification.follow'
|
||||||
/>
|
defaultMessage='{name} followed you'
|
||||||
);
|
values={{ name: displayedName }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.follow.name_and_others'
|
||||||
|
defaultMessage='{name} and {count, plural, one {# other} other {# others}} followed you'
|
||||||
|
values={{
|
||||||
|
name: displayedName,
|
||||||
|
count: total - 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const FollowerCount: React.FC<{ accountId: string }> = ({ accountId }) => {
|
const FollowerCount: React.FC<{ accountId: string }> = ({ accountId }) => {
|
||||||
const account = useAppSelector((s) => s.accounts.get(accountId));
|
const account = useAppSelector((s) => s.accounts.get(accountId));
|
||||||
|
|
|
@ -21,13 +21,27 @@ const messages = defineMessages({
|
||||||
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
|
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const labelRenderer: LabelRenderer = (values) => (
|
const labelRenderer: LabelRenderer = (displayedName, total) => {
|
||||||
<FormattedMessage
|
if (total === 1)
|
||||||
id='notification.follow_request'
|
return (
|
||||||
defaultMessage='{name} has requested to follow you'
|
<FormattedMessage
|
||||||
values={values}
|
id='notification.follow_request'
|
||||||
/>
|
defaultMessage='{name} has requested to follow you'
|
||||||
);
|
values={{ name: displayedName }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.follow_request.name_and_others'
|
||||||
|
defaultMessage='{name} and {count, plural, one {# other} other {# others}} has requested to follow you'
|
||||||
|
values={{
|
||||||
|
name: displayedName,
|
||||||
|
count: total - 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const NotificationFollowRequest: React.FC<{
|
export const NotificationFollowRequest: React.FC<{
|
||||||
notification: NotificationGroupFollowRequest;
|
notification: NotificationGroupFollowRequest;
|
||||||
|
|
|
@ -14,11 +14,13 @@ import type { EmojiReactionGroup } from 'mastodon/models/notification_group';
|
||||||
import { useAppDispatch } from 'mastodon/store';
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import { AvatarGroup } from './avatar_group';
|
import { AvatarGroup } from './avatar_group';
|
||||||
|
import { DisplayedName } from './displayed_name';
|
||||||
import { EmbeddedStatus } from './embedded_status';
|
import { EmbeddedStatus } from './embedded_status';
|
||||||
import { NamesList } from './names_list';
|
|
||||||
|
|
||||||
export type LabelRenderer = (
|
export type LabelRenderer = (
|
||||||
values: Record<string, React.ReactNode>,
|
displayedName: JSX.Element,
|
||||||
|
total: number,
|
||||||
|
seeMoreHref?: string,
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
|
|
||||||
export const NotificationGroupWithStatus: React.FC<{
|
export const NotificationGroupWithStatus: React.FC<{
|
||||||
|
@ -54,15 +56,11 @@ export const NotificationGroupWithStatus: React.FC<{
|
||||||
|
|
||||||
const label = useMemo(
|
const label = useMemo(
|
||||||
() =>
|
() =>
|
||||||
labelRenderer({
|
labelRenderer(
|
||||||
name: (
|
<DisplayedName accountIds={accountIds} />,
|
||||||
<NamesList
|
count,
|
||||||
accountIds={accountIds}
|
labelSeeMoreHref,
|
||||||
total={count}
|
),
|
||||||
seeMoreHref={labelSeeMoreHref}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
[labelRenderer, accountIds, count, labelSeeMoreHref],
|
[labelRenderer, accountIds, count, labelSeeMoreHref],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,12 @@ import { NotificationWithStatus } from './notification_with_status';
|
||||||
const createLabelRenderer = (
|
const createLabelRenderer = (
|
||||||
notification: NotificationGroupListStatus,
|
notification: NotificationGroupListStatus,
|
||||||
): LabelRenderer => {
|
): LabelRenderer => {
|
||||||
const renderer: LabelRenderer = (values) => {
|
const renderer: LabelRenderer = (displayedName) => {
|
||||||
const list = notification.list;
|
const list = notification.list;
|
||||||
|
let listHref: JSX.Element | undefined;
|
||||||
|
|
||||||
if (list) {
|
if (list) {
|
||||||
const listLink = (
|
listHref = (
|
||||||
<bdi>
|
<bdi>
|
||||||
<Link
|
<Link
|
||||||
className='notification__display-name'
|
className='notification__display-name'
|
||||||
|
@ -26,14 +28,13 @@ const createLabelRenderer = (
|
||||||
</Link>
|
</Link>
|
||||||
</bdi>
|
</bdi>
|
||||||
);
|
);
|
||||||
values.listName = listLink;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='notification.list_status'
|
id='notification.list_status'
|
||||||
defaultMessage='{name} post is added to {listName}'
|
defaultMessage='{name} post is added to {listName}'
|
||||||
values={values}
|
values={{ name: displayedName, listName: listHref }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||||
import type { NotificationGroupReblog } from 'mastodon/models/notification_group';
|
import type { NotificationGroupReblog } from 'mastodon/models/notification_group';
|
||||||
import { useAppSelector } from 'mastodon/store';
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
@ -7,13 +9,29 @@ import { useAppSelector } from 'mastodon/store';
|
||||||
import type { LabelRenderer } from './notification_group_with_status';
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
const labelRenderer: LabelRenderer = (values) => (
|
const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
|
||||||
<FormattedMessage
|
if (total === 1)
|
||||||
id='notification.reblog'
|
return (
|
||||||
defaultMessage='{name} boosted your status'
|
<FormattedMessage
|
||||||
values={values}
|
id='notification.reblog'
|
||||||
/>
|
defaultMessage='{name} boosted your status'
|
||||||
);
|
values={{ name: displayedName }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.reblog.name_and_others_with_link'
|
||||||
|
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post'
|
||||||
|
values={{
|
||||||
|
name: displayedName,
|
||||||
|
count: total - 1,
|
||||||
|
a: (chunks) =>
|
||||||
|
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const NotificationReblog: React.FC<{
|
export const NotificationReblog: React.FC<{
|
||||||
notification: NotificationGroupReblog;
|
notification: NotificationGroupReblog;
|
||||||
|
|
|
@ -6,11 +6,11 @@ import type { NotificationGroupStatus } from 'mastodon/models/notification_group
|
||||||
import type { LabelRenderer } from './notification_group_with_status';
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
import { NotificationWithStatus } from './notification_with_status';
|
import { NotificationWithStatus } from './notification_with_status';
|
||||||
|
|
||||||
const labelRenderer: LabelRenderer = (values) => (
|
const labelRenderer: LabelRenderer = (displayedName) => (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='notification.status'
|
id='notification.status'
|
||||||
defaultMessage='{name} just posted'
|
defaultMessage='{name} just posted'
|
||||||
values={values}
|
values={{ name: displayedName }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,11 @@ import type { NotificationGroupStatusReference } from 'mastodon/models/notificat
|
||||||
import type { LabelRenderer } from './notification_group_with_status';
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
import { NotificationWithStatus } from './notification_with_status';
|
import { NotificationWithStatus } from './notification_with_status';
|
||||||
|
|
||||||
const labelRenderer: LabelRenderer = (values) => (
|
const labelRenderer: LabelRenderer = (displayedName) => (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='notification.status_reference'
|
id='notification.status_reference'
|
||||||
defaultMessage='{name} quoted your post'
|
defaultMessage='{name} quoted your post'
|
||||||
values={values}
|
values={{ name: displayedName }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,11 @@ import type { NotificationGroupUpdate } from 'mastodon/models/notification_group
|
||||||
import type { LabelRenderer } from './notification_group_with_status';
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
import { NotificationWithStatus } from './notification_with_status';
|
import { NotificationWithStatus } from './notification_with_status';
|
||||||
|
|
||||||
const labelRenderer: LabelRenderer = (values) => (
|
const labelRenderer: LabelRenderer = (displayedName) => (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='notification.update'
|
id='notification.update'
|
||||||
defaultMessage='{name} edited a post'
|
defaultMessage='{name} edited a post'
|
||||||
values={values}
|
values={{ name: displayedName }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { Icon } from 'mastodon/components/icon';
|
||||||
import Status from 'mastodon/containers/status_container';
|
import Status from 'mastodon/containers/status_container';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import { NamesList } from './names_list';
|
import { DisplayedName } from './displayed_name';
|
||||||
import type { LabelRenderer } from './notification_group_with_status';
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
|
|
||||||
export const NotificationWithStatus: React.FC<{
|
export const NotificationWithStatus: React.FC<{
|
||||||
|
@ -42,10 +42,7 @@ export const NotificationWithStatus: React.FC<{
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const label = useMemo(
|
const label = useMemo(
|
||||||
() =>
|
() => labelRenderer(<DisplayedName accountIds={accountIds} />, count),
|
||||||
labelRenderer({
|
|
||||||
name: <NamesList accountIds={accountIds} total={count} />,
|
|
||||||
}),
|
|
||||||
[labelRenderer, accountIds, count],
|
[labelRenderer, accountIds, count],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -572,8 +572,6 @@
|
||||||
"mute_modal.title": "Mute user?",
|
"mute_modal.title": "Mute user?",
|
||||||
"mute_modal.you_wont_see_mentions": "You won't see posts that mention them.",
|
"mute_modal.you_wont_see_mentions": "You won't see posts that mention them.",
|
||||||
"mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.",
|
"mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.",
|
||||||
"name_and_others": "{name} and {count, plural, one {# other} other {# others}}",
|
|
||||||
"name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a>",
|
|
||||||
"navigation_bar.about": "About",
|
"navigation_bar.about": "About",
|
||||||
"navigation_bar.advanced_interface": "Open in advanced web interface",
|
"navigation_bar.advanced_interface": "Open in advanced web interface",
|
||||||
"navigation_bar.antennas": "Antenna",
|
"navigation_bar.antennas": "Antenna",
|
||||||
|
@ -612,10 +610,15 @@
|
||||||
"notification.admin.report_statuses": "{name} reported {target} for {category}",
|
"notification.admin.report_statuses": "{name} reported {target} for {category}",
|
||||||
"notification.admin.report_statuses_other": "{name} reported {target}",
|
"notification.admin.report_statuses_other": "{name} reported {target}",
|
||||||
"notification.admin.sign_up": "{name} signed up",
|
"notification.admin.sign_up": "{name} signed up",
|
||||||
"notification.emoji_reaction": "{name} reacted your status with emoji",
|
"notification.admin.sign_up.name_and_others": "{name} and {count, plural, one {# other} other {# others}} signed up",
|
||||||
|
"notification.emoji_reaction": "{name} reacted your post with emoji",
|
||||||
|
"notification.emoji_reaction.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> reacted your post with emoji",
|
||||||
"notification.favourite": "{name} favorited your post",
|
"notification.favourite": "{name} favorited your post",
|
||||||
|
"notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post",
|
||||||
"notification.follow": "{name} followed you",
|
"notification.follow": "{name} followed you",
|
||||||
|
"notification.follow.name_and_others": "{name} and {count, plural, one {# other} other {# others}} followed you",
|
||||||
"notification.follow_request": "{name} has requested to follow you",
|
"notification.follow_request": "{name} has requested to follow you",
|
||||||
|
"notification.follow_request.name_and_others": "{name} and {count, plural, one {# other} other {# others}} has requested to follow you",
|
||||||
"notification.label.mention": "Mention",
|
"notification.label.mention": "Mention",
|
||||||
"notification.label.private_mention": "Private mention",
|
"notification.label.private_mention": "Private mention",
|
||||||
"notification.label.private_reply": "Private reply",
|
"notification.label.private_reply": "Private reply",
|
||||||
|
@ -635,6 +638,7 @@
|
||||||
"notification.own_poll": "Your poll has ended",
|
"notification.own_poll": "Your poll has ended",
|
||||||
"notification.poll": "A poll you voted in has ended",
|
"notification.poll": "A poll you voted in has ended",
|
||||||
"notification.reblog": "{name} boosted your post",
|
"notification.reblog": "{name} boosted your post",
|
||||||
|
"notification.reblog.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post",
|
||||||
"notification.relationships_severance_event": "Lost connections with {name}",
|
"notification.relationships_severance_event": "Lost connections with {name}",
|
||||||
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
|
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
|
||||||
"notification.relationships_severance_event.domain_block": "An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
|
"notification.relationships_severance_event.domain_block": "An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
|
||||||
|
|
|
@ -577,6 +577,7 @@
|
||||||
"notification.admin.report_statuses_other": "{name}さんが{target}さんを通報しました",
|
"notification.admin.report_statuses_other": "{name}さんが{target}さんを通報しました",
|
||||||
"notification.admin.sign_up": "{name}さんがサインアップしました",
|
"notification.admin.sign_up": "{name}さんがサインアップしました",
|
||||||
"notification.emoji_reaction": "{name}さんがあなたの投稿に絵文字をつけました",
|
"notification.emoji_reaction": "{name}さんがあなたの投稿に絵文字をつけました",
|
||||||
|
"notification.emoji_reaction.name_and_others_with_link": "{name}さんと<a>{count, plural, other {他#名}}</a>があなたの投稿に絵文字をつけました",
|
||||||
"notification.favourite": "{name}さんがお気に入りしました",
|
"notification.favourite": "{name}さんがお気に入りしました",
|
||||||
"notification.follow": "{name}さんにフォローされました",
|
"notification.follow": "{name}さんにフォローされました",
|
||||||
"notification.follow_request": "{name}さんがあなたにフォローリクエストしました",
|
"notification.follow_request": "{name}さんがあなたにフォローリクエストしました",
|
||||||
|
|
|
@ -20,12 +20,16 @@ import {
|
||||||
mountNotifications,
|
mountNotifications,
|
||||||
unmountNotifications,
|
unmountNotifications,
|
||||||
refreshStaleNotificationGroups,
|
refreshStaleNotificationGroups,
|
||||||
|
pollRecentNotifications,
|
||||||
} from 'mastodon/actions/notification_groups';
|
} from 'mastodon/actions/notification_groups';
|
||||||
import {
|
import {
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
timelineDelete,
|
timelineDelete,
|
||||||
} from 'mastodon/actions/timelines_typed';
|
} from 'mastodon/actions/timelines_typed';
|
||||||
import type { ApiNotificationJSON } from 'mastodon/api_types/notifications';
|
import type {
|
||||||
|
ApiNotificationJSON,
|
||||||
|
ApiNotificationGroupJSON,
|
||||||
|
} from 'mastodon/api_types/notifications';
|
||||||
import { compareId } from 'mastodon/compare_id';
|
import { compareId } from 'mastodon/compare_id';
|
||||||
import { usePendingItems } from 'mastodon/initial_state';
|
import { usePendingItems } from 'mastodon/initial_state';
|
||||||
import {
|
import {
|
||||||
|
@ -331,6 +335,106 @@ function commitLastReadId(state: NotificationGroupsState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fillNotificationsGap(
|
||||||
|
groups: NotificationGroupsState['groups'],
|
||||||
|
gap: NotificationGap,
|
||||||
|
notifications: ApiNotificationGroupJSON[],
|
||||||
|
): NotificationGroupsState['groups'] {
|
||||||
|
// find the gap in the existing notifications
|
||||||
|
const gapIndex = groups.findIndex(
|
||||||
|
(groupOrGap) =>
|
||||||
|
groupOrGap.type === 'gap' &&
|
||||||
|
groupOrGap.sinceId === gap.sinceId &&
|
||||||
|
groupOrGap.maxId === gap.maxId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (gapIndex < 0)
|
||||||
|
// We do not know where to insert, let's return
|
||||||
|
return groups;
|
||||||
|
|
||||||
|
// Filling a disconnection gap means we're getting historical data
|
||||||
|
// about groups we may know or may not know about.
|
||||||
|
|
||||||
|
// The notifications timeline is split in two by the gap, with
|
||||||
|
// group information newer than the gap, and group information older
|
||||||
|
// than the gap.
|
||||||
|
|
||||||
|
// Filling a gap should not touch anything before the gap, so any
|
||||||
|
// information on groups already appearing before the gap should be
|
||||||
|
// discarded, while any information on groups appearing after the gap
|
||||||
|
// can be updated and re-ordered.
|
||||||
|
|
||||||
|
const oldestPageNotification = notifications.at(-1)?.page_min_id;
|
||||||
|
|
||||||
|
// replace the gap with the notifications + a new gap
|
||||||
|
|
||||||
|
const newerGroupKeys = groups
|
||||||
|
.slice(0, gapIndex)
|
||||||
|
.filter(isNotificationGroup)
|
||||||
|
.map((group) => group.group_key);
|
||||||
|
|
||||||
|
const toInsert: NotificationGroupsState['groups'] = notifications
|
||||||
|
.map((json) => createNotificationGroupFromJSON(json))
|
||||||
|
.filter((notification) => !newerGroupKeys.includes(notification.group_key));
|
||||||
|
|
||||||
|
const apiGroupKeys = (toInsert as NotificationGroup[]).map(
|
||||||
|
(group) => group.group_key,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sinceId = gap.sinceId;
|
||||||
|
if (
|
||||||
|
notifications.length > 0 &&
|
||||||
|
!(
|
||||||
|
oldestPageNotification &&
|
||||||
|
sinceId &&
|
||||||
|
compareId(oldestPageNotification, sinceId) <= 0
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
|
||||||
|
// Similarly, if we've fetched more than the gap's, this means we have completely filled it
|
||||||
|
toInsert.push({
|
||||||
|
type: 'gap',
|
||||||
|
maxId: notifications.at(-1)?.page_max_id,
|
||||||
|
sinceId,
|
||||||
|
} as NotificationGap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove older groups covered by the API
|
||||||
|
groups = groups.filter(
|
||||||
|
(groupOrGap) =>
|
||||||
|
groupOrGap.type !== 'gap' && !apiGroupKeys.includes(groupOrGap.group_key),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace the gap with API results (+ the new gap if needed)
|
||||||
|
groups.splice(gapIndex, 1, ...toInsert);
|
||||||
|
|
||||||
|
// Finally, merge any adjacent gaps that could have been created by filtering
|
||||||
|
// groups earlier
|
||||||
|
mergeGaps(groups);
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the groups list starts with a gap, mutating it to prepend one if needed
|
||||||
|
function ensureLeadingGap(
|
||||||
|
groups: NotificationGroupsState['groups'],
|
||||||
|
): NotificationGap {
|
||||||
|
if (groups[0]?.type === 'gap') {
|
||||||
|
// We're expecting new notifications, so discard the maxId if there is one
|
||||||
|
groups[0].maxId = undefined;
|
||||||
|
|
||||||
|
return groups[0];
|
||||||
|
} else {
|
||||||
|
const gap: NotificationGap = {
|
||||||
|
type: 'gap',
|
||||||
|
sinceId: groups[0]?.page_min_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
groups.unshift(gap);
|
||||||
|
return gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||||
initialState,
|
initialState,
|
||||||
(builder) => {
|
(builder) => {
|
||||||
|
@ -344,86 +448,36 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||||
updateLastReadId(state);
|
updateLastReadId(state);
|
||||||
})
|
})
|
||||||
.addCase(fetchNotificationsGap.fulfilled, (state, action) => {
|
.addCase(fetchNotificationsGap.fulfilled, (state, action) => {
|
||||||
const { notifications } = action.payload;
|
state.groups = fillNotificationsGap(
|
||||||
|
state.groups,
|
||||||
// find the gap in the existing notifications
|
action.meta.arg.gap,
|
||||||
const gapIndex = state.groups.findIndex(
|
action.payload.notifications,
|
||||||
(groupOrGap) =>
|
|
||||||
groupOrGap.type === 'gap' &&
|
|
||||||
groupOrGap.sinceId === action.meta.arg.gap.sinceId &&
|
|
||||||
groupOrGap.maxId === action.meta.arg.gap.maxId,
|
|
||||||
);
|
);
|
||||||
|
state.isLoading = false;
|
||||||
|
|
||||||
if (gapIndex < 0)
|
updateLastReadId(state);
|
||||||
// We do not know where to insert, let's return
|
})
|
||||||
return;
|
.addCase(pollRecentNotifications.fulfilled, (state, action) => {
|
||||||
|
if (usePendingItems) {
|
||||||
// Filling a disconnection gap means we're getting historical data
|
const gap = ensureLeadingGap(state.pendingGroups);
|
||||||
// about groups we may know or may not know about.
|
state.pendingGroups = fillNotificationsGap(
|
||||||
|
state.pendingGroups,
|
||||||
// The notifications timeline is split in two by the gap, with
|
gap,
|
||||||
// group information newer than the gap, and group information older
|
action.payload.notifications,
|
||||||
// than the gap.
|
);
|
||||||
|
} else {
|
||||||
// Filling a gap should not touch anything before the gap, so any
|
const gap = ensureLeadingGap(state.groups);
|
||||||
// information on groups already appearing before the gap should be
|
state.groups = fillNotificationsGap(
|
||||||
// discarded, while any information on groups appearing after the gap
|
state.groups,
|
||||||
// can be updated and re-ordered.
|
gap,
|
||||||
|
action.payload.notifications,
|
||||||
const oldestPageNotification = notifications.at(-1)?.page_min_id;
|
|
||||||
|
|
||||||
// replace the gap with the notifications + a new gap
|
|
||||||
|
|
||||||
const newerGroupKeys = state.groups
|
|
||||||
.slice(0, gapIndex)
|
|
||||||
.filter(isNotificationGroup)
|
|
||||||
.map((group) => group.group_key);
|
|
||||||
|
|
||||||
const toInsert: NotificationGroupsState['groups'] = notifications
|
|
||||||
.map((json) => createNotificationGroupFromJSON(json))
|
|
||||||
.filter(
|
|
||||||
(notification) => !newerGroupKeys.includes(notification.group_key),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const apiGroupKeys = (toInsert as NotificationGroup[]).map(
|
|
||||||
(group) => group.group_key,
|
|
||||||
);
|
|
||||||
|
|
||||||
const sinceId = action.meta.arg.gap.sinceId;
|
|
||||||
if (
|
|
||||||
notifications.length > 0 &&
|
|
||||||
!(
|
|
||||||
oldestPageNotification &&
|
|
||||||
sinceId &&
|
|
||||||
compareId(oldestPageNotification, sinceId) <= 0
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
|
|
||||||
// Similarly, if we've fetched more than the gap's, this means we have completely filled it
|
|
||||||
toInsert.push({
|
|
||||||
type: 'gap',
|
|
||||||
maxId: notifications.at(-1)?.page_max_id,
|
|
||||||
sinceId,
|
|
||||||
} as NotificationGap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove older groups covered by the API
|
|
||||||
state.groups = state.groups.filter(
|
|
||||||
(groupOrGap) =>
|
|
||||||
groupOrGap.type !== 'gap' &&
|
|
||||||
!apiGroupKeys.includes(groupOrGap.group_key),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Replace the gap with API results (+ the new gap if needed)
|
|
||||||
state.groups.splice(gapIndex, 1, ...toInsert);
|
|
||||||
|
|
||||||
// Finally, merge any adjacent gaps that could have been created by filtering
|
|
||||||
// groups earlier
|
|
||||||
mergeGaps(state.groups);
|
|
||||||
|
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
|
|
||||||
updateLastReadId(state);
|
updateLastReadId(state);
|
||||||
|
trimNotifications(state);
|
||||||
})
|
})
|
||||||
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
|
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
|
||||||
const notification = action.payload;
|
const notification = action.payload;
|
||||||
|
@ -438,10 +492,11 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||||
})
|
})
|
||||||
.addCase(disconnectTimeline, (state, action) => {
|
.addCase(disconnectTimeline, (state, action) => {
|
||||||
if (action.payload.timeline === 'home') {
|
if (action.payload.timeline === 'home') {
|
||||||
if (state.groups.length > 0 && state.groups[0]?.type !== 'gap') {
|
const groups = usePendingItems ? state.pendingGroups : state.groups;
|
||||||
state.groups.unshift({
|
if (groups.length > 0 && groups[0]?.type !== 'gap') {
|
||||||
|
groups.unshift({
|
||||||
type: 'gap',
|
type: 'gap',
|
||||||
sinceId: state.groups[0]?.page_min_id,
|
sinceId: groups[0]?.page_min_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -488,12 +543,13 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trimNotifications(state);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Then build the consolidated list and clear pending groups
|
// Then build the consolidated list and clear pending groups
|
||||||
state.groups = state.pendingGroups.concat(state.groups);
|
state.groups = state.pendingGroups.concat(state.groups);
|
||||||
state.pendingGroups = [];
|
state.pendingGroups = [];
|
||||||
|
mergeGaps(state.groups);
|
||||||
|
trimNotifications(state);
|
||||||
})
|
})
|
||||||
.addCase(updateScrollPosition.fulfilled, (state, action) => {
|
.addCase(updateScrollPosition.fulfilled, (state, action) => {
|
||||||
state.scrolledToTop = action.payload.top;
|
state.scrolledToTop = action.payload.top;
|
||||||
|
@ -553,13 +609,21 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.addMatcher(
|
.addMatcher(
|
||||||
isAnyOf(fetchNotifications.pending, fetchNotificationsGap.pending),
|
isAnyOf(
|
||||||
|
fetchNotifications.pending,
|
||||||
|
fetchNotificationsGap.pending,
|
||||||
|
pollRecentNotifications.pending,
|
||||||
|
),
|
||||||
(state) => {
|
(state) => {
|
||||||
state.isLoading = true;
|
state.isLoading = true;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.addMatcher(
|
.addMatcher(
|
||||||
isAnyOf(fetchNotifications.rejected, fetchNotificationsGap.rejected),
|
isAnyOf(
|
||||||
|
fetchNotifications.rejected,
|
||||||
|
fetchNotificationsGap.rejected,
|
||||||
|
pollRecentNotifications.rejected,
|
||||||
|
),
|
||||||
(state) => {
|
(state) => {
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
},
|
},
|
||||||
|
|
|
@ -414,9 +414,6 @@
|
||||||
border-color: transparent transparent $white;
|
border-color: transparent transparent $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-widget,
|
|
||||||
.moved-account-widget,
|
|
||||||
.memoriam-widget,
|
|
||||||
.activity-stream,
|
.activity-stream,
|
||||||
.nothing-here,
|
.nothing-here,
|
||||||
.directory__tag > a,
|
.directory__tag > a,
|
||||||
|
|
|
@ -130,21 +130,11 @@
|
||||||
.older {
|
.older {
|
||||||
float: left;
|
float: left;
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
|
|
||||||
.fa {
|
|
||||||
display: inline-block;
|
|
||||||
margin-inline-end: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.newer {
|
.newer {
|
||||||
float: right;
|
float: right;
|
||||||
padding-inline-end: 0;
|
padding-inline-end: 0;
|
||||||
|
|
||||||
.fa {
|
|
||||||
display: inline-block;
|
|
||||||
margin-inline-start: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
|
|
|
@ -122,10 +122,6 @@ $content-width: 840px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
i.fa {
|
|
||||||
margin-inline-end: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
transition: all 100ms linear;
|
transition: all 100ms linear;
|
||||||
|
@ -306,10 +302,6 @@ $content-width: 840px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.directory__tag .table-action-link .fa {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.directory__tag h4 {
|
.directory__tag h4 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|
|
@ -2981,10 +2981,6 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
padding-inline-end: 30px;
|
padding-inline-end: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search__icon .fa {
|
|
||||||
top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
|
@ -3520,26 +3516,6 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
height: calc(100% - 10px);
|
height: calc(100% - 10px);
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
.hero-widget {
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
&__text,
|
|
||||||
&__img,
|
|
||||||
&__img img {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
padding: 15px;
|
|
||||||
color: $secondary-text-color;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-weight: 700;
|
|
||||||
color: $primary-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.compose-form {
|
.compose-form {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
@ -6529,7 +6505,7 @@ a.status-card {
|
||||||
}
|
}
|
||||||
|
|
||||||
.boost-modal__container {
|
.boost-modal__container {
|
||||||
overflow-x: scroll;
|
overflow-y: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
|
|
|
@ -113,10 +113,6 @@
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
|
@ -942,10 +942,6 @@ code {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa {
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,14 +41,6 @@ body.rtl {
|
||||||
no-repeat left 8px center / auto 16px;
|
no-repeat left 8px center / auto 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa-chevron-left::before {
|
|
||||||
content: '\F054';
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-chevron-right::before {
|
|
||||||
content: '\F053';
|
|
||||||
}
|
|
||||||
|
|
||||||
.dismissable-banner,
|
.dismissable-banner,
|
||||||
.warning-banner {
|
.warning-banner {
|
||||||
&__action {
|
&__action {
|
||||||
|
|
|
@ -142,11 +142,6 @@ a.table-action-link {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
i.fa {
|
|
||||||
font-weight: 400;
|
|
||||||
margin-inline-end: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-inline-start: 0;
|
padding-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,295 +1,3 @@
|
||||||
@use 'sass:math';
|
|
||||||
|
|
||||||
.hero-widget {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__img {
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
background: $base-shadow-color;
|
|
||||||
|
|
||||||
img {
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 4px 4px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__text {
|
|
||||||
background: $ui-base-color;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 0 0 4px 4px;
|
|
||||||
font-size: 15px;
|
|
||||||
color: $darker-text-color;
|
|
||||||
line-height: 20px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
.emojione {
|
|
||||||
min-width: 20px;
|
|
||||||
max-width: min(10em, 100%);
|
|
||||||
height: 20px;
|
|
||||||
margin: -3px 0 0;
|
|
||||||
margin-inline-start: 0.075em;
|
|
||||||
margin-inline-end: 0.075em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
em {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-weight: 700;
|
|
||||||
background: transparent;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: inherit;
|
|
||||||
color: lighten($darker-text-color, 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $secondary-text-color;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: $no-gap-breakpoint) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.endorsements-widget {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
padding: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 13px;
|
|
||||||
color: $darker-text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account {
|
|
||||||
padding: 10px 0;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account__display-name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.trends__item {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.trends-widget {
|
|
||||||
h4 {
|
|
||||||
color: $darker-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-widget {
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 2px dashed $dark-text-color;
|
|
||||||
text-align: center;
|
|
||||||
color: $darker-text-color;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.moved-account-widget {
|
|
||||||
padding: 15px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: $ui-base-color;
|
|
||||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
|
||||||
color: $secondary-text-color;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
strong,
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
@each $lang in $cjk-langs {
|
|
||||||
&:lang(#{$lang}) {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: underline;
|
|
||||||
|
|
||||||
&.mention {
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
span {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus,
|
|
||||||
&:hover,
|
|
||||||
&:active {
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
span {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__message {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
|
|
||||||
.fa {
|
|
||||||
margin-inline-end: 5px;
|
|
||||||
color: $darker-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__card {
|
|
||||||
.detailed-status__display-avatar {
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailed-status__display-name {
|
|
||||||
margin-bottom: 0;
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.memoriam-widget {
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: $base-shadow-color;
|
|
||||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
|
||||||
font-size: 14px;
|
|
||||||
color: $darker-text-color;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.directory {
|
|
||||||
background: var(--background-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
|
||||||
|
|
||||||
&__tag {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
& > a,
|
|
||||||
& > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
border: 1px solid var(--background-border-color);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 15px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
& > a {
|
|
||||||
&:hover,
|
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
background: $ui-base-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active > a {
|
|
||||||
background: $ui-highlight-color;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled > div {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: $primary-text-color;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
.fa {
|
|
||||||
color: $darker-text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
display: block;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 15px;
|
|
||||||
margin-top: 8px;
|
|
||||||
color: $darker-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active h4 {
|
|
||||||
&,
|
|
||||||
.fa,
|
|
||||||
small,
|
|
||||||
.trends__item__current {
|
|
||||||
color: $primary-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-stack {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: (36px + 4px) * 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active .avatar-stack .account__avatar {
|
|
||||||
border-color: $ui-highlight-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trends__item__current {
|
|
||||||
padding-inline-end: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.accounts-table {
|
.accounts-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@ -367,9 +75,7 @@
|
||||||
padding-inline-end: 16px;
|
padding-inline-end: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fa {
|
.icon {
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
}
|
}
|
||||||
|
@ -389,27 +95,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.moved-account-widget,
|
|
||||||
.memoriam-widget,
|
|
||||||
.directory {
|
|
||||||
@media screen and (max-width: $no-gap-breakpoint) {
|
|
||||||
margin-bottom: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-widget {
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
color: $ui-highlight-color;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
|
|
||||||
attributes :domain, :title, :version, :source_url, :description,
|
attributes :domain, :title, :version, :source_url, :description,
|
||||||
:usage, :thumbnail, :languages, :configuration,
|
:usage, :thumbnail, :languages, :configuration,
|
||||||
:registrations, :fedibird_capabilities
|
:registrations, :fedibird_capabilities, :api_versions
|
||||||
|
|
||||||
has_one :contact, serializer: ContactSerializer
|
has_one :contact, serializer: ContactSerializer
|
||||||
has_many :rules, serializer: REST::RuleSerializer
|
has_many :rules, serializer: REST::RuleSerializer
|
||||||
|
@ -119,6 +119,13 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def api_versions
|
||||||
|
{
|
||||||
|
mastodon: 1,
|
||||||
|
kmyblue: KMYBLUE_API_VERSION,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def registrations_enabled?
|
def registrations_enabled?
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
:ruby
|
:ruby
|
||||||
show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired?
|
show_results = (user_signed_in? && poll.voted?(current_account)) || poll.expired?
|
||||||
own_votes = user_signed_in? ? poll.own_votes(current_account) : []
|
|
||||||
total_votes_count = poll.voters_count || poll.votes_count
|
total_votes_count = poll.voters_count || poll.votes_count
|
||||||
|
|
||||||
.poll
|
.poll
|
||||||
%ul
|
%ul
|
||||||
- poll.loaded_options.each_with_index do |option, index|
|
- poll.loaded_options.each do |option|
|
||||||
%li
|
%li
|
||||||
- if show_results
|
- if show_results
|
||||||
- percent = total_votes_count.positive? ? 100 * option.votes_count / total_votes_count : 0
|
- percent = total_votes_count.positive? ? 100 * option.votes_count / total_votes_count : 0
|
||||||
|
@ -14,9 +13,6 @@
|
||||||
#{percent.round}%
|
#{percent.round}%
|
||||||
%span.poll__option__text
|
%span.poll__option__text
|
||||||
= prerender_custom_emojis(h(option.title), status.emojis)
|
= prerender_custom_emojis(h(option.title), status.emojis)
|
||||||
- if own_votes.include?(index)
|
|
||||||
%span.poll__voted
|
|
||||||
%i.poll__voted__mark.fa.fa-check
|
|
||||||
|
|
||||||
%progress{ max: 100, value: [percent, 1].max, 'aria-hidden': 'true' }
|
%progress{ max: 100, value: [percent, 1].max, 'aria-hidden': 'true' }
|
||||||
%span.poll__chart
|
%span.poll__chart
|
||||||
|
|
|
@ -18,6 +18,7 @@ describe 'Instances' do
|
||||||
expect(body_as_json)
|
expect(body_as_json)
|
||||||
.to be_present
|
.to be_present
|
||||||
.and include(title: 'Mastodon')
|
.and include(title: 'Mastodon')
|
||||||
|
.and include_api_versions
|
||||||
.and include_configuration_limits
|
.and include_configuration_limits
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -32,6 +33,7 @@ describe 'Instances' do
|
||||||
expect(body_as_json)
|
expect(body_as_json)
|
||||||
.to be_present
|
.to be_present
|
||||||
.and include(title: 'Mastodon')
|
.and include(title: 'Mastodon')
|
||||||
|
.and include_api_versions
|
||||||
.and include_configuration_limits
|
.and include_configuration_limits
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -53,5 +55,13 @@ describe 'Instances' do
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_api_versions
|
||||||
|
include(
|
||||||
|
api_versions: include(
|
||||||
|
mastodon: anything
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue