Add: #95 リストへの新着投稿通知 (#192)

* Add: テーブル定義、内部処理

* Add: 通知の定期削除処理、自動削除、テスト

* Add: Web画面の表示、設定

* Fix test
This commit is contained in:
KMY(雪あすか) 2023-10-31 08:59:31 +09:00 committed by GitHub
parent 2cc60253c4
commit f8280ca5d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 300 additions and 9 deletions

View file

@ -151,10 +151,15 @@ export const createListFail = error => ({
error,
});
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => {
export const updateList = (id, title, shouldReset, isExclusive, replies_policy, notify) => (dispatch, getState) => {
dispatch(updateListRequest(id));
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
api(getState).put(`/api/v1/lists/${id}`, {
title,
replies_policy,
exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive,
notify: typeof notify === 'undefined' ? undefined : !!notify,
}).then(({ data }) => {
dispatch(updateListSuccess(data));
if (shouldReset) {

View file

@ -148,6 +148,7 @@ const excludeTypesFromFilter = filter => {
'mention',
'poll',
'status',
'list_status',
'update',
'admin.sign_up',
'admin.report',

View file

@ -154,13 +154,19 @@ class ListTimeline extends PureComponent {
handleRepliesPolicyChange = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateList(id, undefined, false, undefined, target.value));
dispatch(updateList(id, undefined, false, undefined, target.value, undefined));
};
onExclusiveToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateList(id, undefined, false, target.checked, undefined));
dispatch(updateList(id, undefined, false, target.checked, undefined, undefined));
};
onNotifyToggle = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateList(id, undefined, false, undefined, undefined, target.checked));
};
render () {
@ -170,6 +176,7 @@ class ListTimeline extends PureComponent {
const title = list ? list.get('title') : id;
const replies_policy = list ? list.get('replies_policy') : undefined;
const isExclusive = list ? list.get('exclusive') : undefined;
const isNotify = list ? list.get('notify') : undefined;
const antennas = list ? (list.get('antennas')?.toArray() || []) : [];
if (typeof list === 'undefined') {
@ -216,6 +223,13 @@ class ListTimeline extends PureComponent {
</label>
</div>
<div className='setting-toggle'>
<Toggle id={`list-${id}-exclusive`} defaultChecked={isNotify} onChange={this.onNotifyToggle} />
<label htmlFor={`list-${id}-notify`} className='setting-toggle__label'>
<FormattedMessage id='lists.notify' defaultMessage='Notify these posts' />
</label>
</div>
{ replies_policy !== undefined && (
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
<span id={`list-${id}-replies-policy`} className='column-settings__section'>

View file

@ -190,6 +190,17 @@ export default class ColumnSettings extends PureComponent {
</div>
</div>
<div role='group' aria-labelledby='notifications-list_status'>
<span id='notifications-list_status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.list_status' defaultMessage='New posts of list:' /></span>
<div className='column-settings__row'>
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'list_status']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'list_status']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'list_status']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'list_status']} onChange={onChange} label={soundStr} />
</div>
</div>
<div role='group' aria-labelledby='notifications-update'>
<span id='notifications-update' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>

View file

@ -13,6 +13,7 @@ import { ReactComponent as FlagIcon } from '@material-symbols/svg-600/outlined/f
import { ReactComponent as HomeIcon } from '@material-symbols/svg-600/outlined/home-fill.svg';
import { ReactComponent as InsertChartIcon } from '@material-symbols/svg-600/outlined/insert_chart.svg';
import { ReactComponent as ReferenceIcon } from '@material-symbols/svg-600/outlined/link.svg';
import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg';
import { ReactComponent as PersonIcon } from '@material-symbols/svg-600/outlined/person-fill.svg';
import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outlined/person_add-fill.svg';
import { ReactComponent as RepeatIcon } from '@material-symbols/svg-600/outlined/repeat.svg';
@ -38,6 +39,7 @@ const messages = defineMessages({
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
listStatus: { id: 'notification.list_status', defaultMessage: '{name} post is added on {listName}' },
statusReference: { id: 'notification.status_reference', defaultMessage: '{name} refered' },
update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
warning: { id: 'notification.warning', defaultMessage: 'You have been warned and "{action}" has been executed. Check your mailbox' },
@ -358,6 +360,42 @@ class Notification extends ImmutablePureComponent {
);
}
renderListStatus (notification, listLink, link) {
const { intl, unread, status } = this.props;
if (!status) {
return null;
}
return (
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-list_status focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.listStatus, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='list-ul' icon={ListAltIcon} />
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.list_status' defaultMessage='{name} post is added to {listName}' values={{ listName: listLink, name: link }} />
</span>
</div>
<StatusContainer
id={notification.get('status')}
account={notification.get('account')}
contextType='notifications'
muted
withDismiss
hidden={this.props.hidden}
getScrollPosition={this.props.getScrollPosition}
updateScrollBottom={this.props.updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
withoutEmojiReactions
/>
</div>
</HotKeys>
);
}
renderUpdate (notification, link) {
const { intl, unread, status } = this.props;
@ -531,6 +569,10 @@ class Notification extends ImmutablePureComponent {
return this.renderStatusReference(notification, link);
case 'status':
return this.renderStatus(notification, link);
case 'list_status':
const list = notification.get('list');
const listLink = <bdi><Link className='notification__display-name' href={`/lists/${list.get('id')}`} title={list.get('title')} to={`/lists/${list.get('id')}`}>{list.get('title')}</Link></bdi>;
return this.renderListStatus(notification, listLink, link);
case 'update':
return this.renderUpdate(notification, link);
case 'poll':

View file

@ -399,6 +399,7 @@
"lists.exclusive": "Hide list or antenna account posts from home",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.notify": "Notify these posts",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.list": "Members of the list",
"lists.replies_policy.none": "No one",
@ -448,6 +449,7 @@
"notification.favourite": "{name} favorited your post",
"notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you",
"notification.list_status": "{name} post is added to {listName}",
"notification.mention": "{name} mentioned you",
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended",

View file

@ -474,6 +474,7 @@
"lists.exclusive": "ホームからリスト・アンテナに登録されたアカウントの投稿を非表示にする",
"lists.new.create": "リストを作成",
"lists.new.title_placeholder": "新規リスト名",
"lists.notify": "これらの投稿を通知する",
"lists.replies_policy.followed": "フォロー中のユーザー全員",
"lists.replies_policy.list": "リストのメンバー",
"lists.replies_policy.none": "表示しない",
@ -525,6 +526,7 @@
"notification.favourite": "{name}さんがお気に入りしました",
"notification.follow": "{name}さんにフォローされました",
"notification.follow_request": "{name}さんがあなたにフォローリクエストしました",
"notification.list_status": "{name}さんの投稿が{listName}に追加されました",
"notification.mention": "{name}さんがあなたに返信しました",
"notification.own_poll": "アンケートが終了しました",
"notification.poll": "アンケートが終了しました",

View file

@ -55,6 +55,7 @@ const notificationToMap = notification => ImmutableMap({
created_at: notification.created_at,
emoji_reaction: ImmutableMap(notification.emoji_reaction),
status: notification.status ? notification.status.id : null,
list: notification.list ? ImmutableMap(notification.list) : null,
report: notification.report ? fromJS(notification.report) : null,
account_warning: notification.account_warning ? ImmutableMap(notification.account_warning) : null,
});

View file

@ -42,6 +42,7 @@ const initialState = ImmutableMap({
mention: false,
poll: false,
status: false,
list_status: false,
update: false,
emoji_reaction: false,
status_reference: false,
@ -66,6 +67,7 @@ const initialState = ImmutableMap({
mention: true,
poll: true,
status: true,
list_status: true,
update: true,
emoji_reaction: true,
status_reference: true,
@ -81,6 +83,7 @@ const initialState = ImmutableMap({
mention: true,
poll: true,
status: true,
list_status: true,
update: true,
emoji_reaction: true,
status_reference: true,