1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2025-01-18 15:53:10 +09:00

enhance(frontend/backend): 예약된 노트 게시에 실패할 경우 사용자에게 알림 ([penginn-net/kokonect@a0e47980](a0e4798047))

This commit is contained in:
NoriDev 2024-12-03 10:37:05 +09:00
parent d4ab680078
commit d415d091f1
16 changed files with 126 additions and 7 deletions

View File

@ -47,6 +47,7 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024110](CHANG
- 기간은 최소 `1`일부터 최대 `30`일까지 설정할 수 있습니다.
- `0`일로 설정하면 최소 기간인 `1`일로 설정되며, `30`일을 초과하는 값을 입력하면 최대 기간인 `30`일로 설정됩니다.
- 여기서 설정된 기간은 `초대제로 전환` 옵션과 `공개 노트 허용` 옵션 모두에 적용됩니다.
- Enhance: 예약된 노트 게시에 실패할 경우 사용자에게 알림 ([penginn-net/kokonect@a0e47980](https://github.com/penginn-net/kokonect/commit/a0e47980470b49e79e84ff3b7ccaf2b4502928c8))
### Client
- Enhance: 미디어 그리드 레이아옷 조정

View File

@ -2820,12 +2820,19 @@ _notification:
achievementEarned: "Achievement unlocked"
exportCompleted: "The export has been completed"
login: "Sign In"
scheduleNote: "Scheduled note posting failed"
test: "Notification test"
app: "Notifications from linked apps"
_actions:
followBack: "followed you back"
reply: "Reply"
renote: "Renote"
_scheduleNote:
unknown: "The cause is unknown"
renoteTargetNotFound: "Renote target note not found"
channelTargetNotFound: "Channel not found"
replyTargetNotFound: "Reply target note not found"
invalidFilesCount: "No attachments"
_deck:
alwaysShowMainColumn: "Always show main column"
columnAlign: "Align columns"

26
locales/index.d.ts vendored
View File

@ -10989,6 +10989,10 @@ export interface Locale extends ILocale {
*
*/
"login": string;
/**
* 稿
*/
"scheduleNote": string;
/**
*
*/
@ -11012,6 +11016,28 @@ export interface Locale extends ILocale {
*/
"renote": string;
};
"_scheduleNote": {
/**
*
*/
"unknown": string;
/**
*
*/
"renoteTargetNotFound": string;
/**
*
*/
"channelTargetNotFound": string;
/**
*
*/
"replyTargetNotFound": string;
/**
*
*/
"invalidFilesCount": string;
};
};
"_deck": {
/**

View File

@ -2895,6 +2895,7 @@ _notification:
achievementEarned: "実績の獲得"
exportCompleted: "エクスポートが完了した"
login: "ログイン"
scheduleNote: "予約投稿に失敗"
test: "通知のテスト"
app: "連携アプリからの通知"
@ -2903,6 +2904,13 @@ _notification:
reply: "返信"
renote: "リノート"
_scheduleNote:
unknown: "原因は不明です"
renoteTargetNotFound: "引用元がありません"
channelTargetNotFound: "対象のチャンネルがありません"
replyTargetNotFound: "返信先がありません"
invalidFilesCount: "添付ファイルがありません"
_deck:
alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ"

View File

@ -2823,12 +2823,19 @@ _notification:
achievementEarned: "도전 과제 획득"
exportCompleted: "내보내기를 완료함"
login: "로그인"
scheduleNote: "게시가 예약된 노트의 게시가 실패함"
test: "알림 테스트"
app: "연동된 앱을 통한 알림"
_actions:
followBack: "팔로우"
reply: "답글"
renote: "리노트"
_scheduleNote:
unknown: "알 수 없는 오류가 발생했어요"
renoteTargetNotFound: "인용할 대상이 없어요"
channelTargetNotFound: "해당 채널이 존재하지 않아요"
replyTargetNotFound: "답장할 대상이 없어요"
invalidFilesCount: "첨부 파일이 없어요"
_deck:
alwaysShowMainColumn: "메인 칼럼 항상 표시"
columnAlign: "칼럼 정렬"

View File

@ -202,6 +202,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'login' ? {
ip: notification.userIp,
} : {}),
...(notification.type === 'scheduleNote' ? {
errorType: notification.errorType,
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader,

View File

@ -98,6 +98,11 @@ export type MiNotification = {
id: string;
createdAt: string;
userIp: string;
} | {
type: 'scheduleNote';
id: string;
createdAt: string;
errorType: string;
} | {
type: 'app';
id: string;

View File

@ -336,6 +336,20 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['scheduleNote'],
},
errorType: {
type: 'string',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {

View File

@ -9,6 +9,7 @@ import { bindThis } from '@/decorators.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import type { ChannelsRepository, DriveFilesRepository, MiDriveFile, NoteScheduleRepository, NotesRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { ScheduleNotePostJobData } from '../types.js';
@ -32,6 +33,7 @@ export class ScheduleNotePostProcessorService {
private noteCreateService: NoteCreateService,
private queueLoggerService: QueueLoggerService,
private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('schedule-note-post');
}
@ -72,6 +74,25 @@ export class ScheduleNotePostProcessorService {
//キューに積んだときは有った物が消滅してたら予約投稿をキャンセルする
this.logger.warn('cancel schedule note');
await this.noteScheduleRepository.remove(data);
if (data.userId && me) { //ユーザーが特定できる場合に失敗を通知
let errorType = 'unknown';
if (note.renote && !renote) {
errorType = 'renoteTargetNotFound';
}
if (note.reply && !reply) {
errorType = 'replyTargetNotFound';
}
if (note.channel && !channel) {
errorType = 'channelTargetNotFound';
}
if (note.files.length !== files.length) {
errorType = 'invalidFilesCount';
}
this.notificationService.createNotification(data.userId, 'scheduleNote', {
errorType,
});
}
return;
}
await this.noteCreateService.create(me, {

View File

@ -38,6 +38,7 @@ export const notificationTypes = [
'achievementEarned',
'exportCompleted',
'login',
'scheduleNote',
'app',
'test',
] as const;

View File

@ -3004,7 +3004,7 @@ type Notification_2 = components['schemas']['Notification'];
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
// @public (undocumented)
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"];
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "scheduleNote"];
// @public (undocumented)
export function nyaize(text: string): string;

View File

@ -4715,6 +4715,14 @@ export type components = {
/** @enum {string} */
type: 'login';
ip: string;
} | {
/** Format: id */
id: string;
/** Format: date-time */
createdAt: string;
/** @enum {string} */
type: 'scheduleNote';
errorType: string;
} | ({
/** Format: id */
id: string;
@ -19667,8 +19675,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'pollVote')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'pollVote')[];
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'scheduleNote' | 'app' | 'test' | 'pollVote')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'scheduleNote' | 'app' | 'test' | 'pollVote')[];
};
};
};
@ -19735,8 +19743,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[];
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'scheduleNote' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'login' | 'scheduleNote' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'note:grouped' | 'pollVote')[];
};
};
};

View File

@ -16,7 +16,7 @@ import type {
UserLite,
} from './autogen/models.js';
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'scheduleNote'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;

View File

@ -70,6 +70,7 @@ export const notificationTypes = [
'achievementEarned',
'exportCompleted',
'login',
'scheduleNote',
'test',
'app',
] as const;

View File

@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'note:grouped'" :class="[$style.icon, $style.icon_noteGroup]"><i class="ti ti-pencil" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'scheduleNote'" :class="[$style.icon, $style.icon_scheduleNote]"><i class="ti ti-alert-triangle" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
@ -64,6 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span>
<span v-else-if="notification.type === 'scheduleNote'">{{ i18n.ts._notification._types.scheduleNote }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
@ -141,6 +143,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectGroupInvitation()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
</div>
</template>
<span v-else-if="notification.type === 'scheduleNote'" :class="$style.text">{{ i18n.ts._notification._scheduleNote[notification.errorType] }}</span>
<span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span>
<span v-else-if="notification.type === 'app'" :class="$style.text">
<Mfm :text="notification.body" :nowrap="false"/>
@ -285,7 +288,8 @@ const rejectGroupInvitation = () => {
.icon_reactionGroup,
.icon_reactionGroupHeart,
.icon_renoteGroup,
.icon_noteGroup {
.icon_noteGroup,
.icon_scheduleNote {
display: grid;
align-items: center;
justify-items: center;
@ -312,6 +316,13 @@ const rejectGroupInvitation = () => {
background: var(--eventRenote);
}
.icon_scheduleNote {
width: 100%;
height: 100%;
color: var(--warn);
background: var(--eventOther);
}
.icon_app {
border-radius: 6px;
}

View File

@ -262,6 +262,12 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
data,
}];
case 'scheduleNote':
return [i18n.ts._notification._types.scheduleNote, {
body: data.body.errorType,
data,
}];
case 'app':
return [data.body.header ?? data.body.body, {
body: data.body.header ? data.body.body : '',