Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 個人宛てお知らせ機能 #100

Merged
2 changes: 2 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,8 @@ export interface Locale {
"additionalEmojiDictionary": string;
"installed": string;
"branding": string;
"newUserAnnouncementAvailable": string;
"viewAnnouncement": string;
"_initialAccountSetting": {
"accountCreated": string;
"letsStartAccountSetup": string;
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,8 @@ goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
branding: "ブランディング"
newUserAnnouncementAvailable: "新着のあなた宛てのお知らせがあります"
viewAnnouncement: "お知らせを見る"

_initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!"
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/migration/1688647797135-userannouncement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class Userannouncement1688647797135 {
name = 'Userannouncement1688647797135'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "announcement" ADD COLUMN "userId" character varying(64)`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "userId"`);
}
}
13 changes: 10 additions & 3 deletions packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, Not } from 'typeorm';
import { In, IsNull, Not } from 'typeorm';
import * as Redis from 'ioredis';
import Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
Expand Down Expand Up @@ -217,9 +217,16 @@ export class UserEntityService implements OnModuleInit {
userId: userId,
});

const count = await this.announcementsRepository.countBy(reads.length > 0 ? {
const count1 = await this.announcementsRepository.countBy(reads.length > 0 ? {
id: Not(In(reads.map(read => read.announcementId))),
} : {});
userId: IsNull(),
} : { userId: IsNull() });
const count2 = await this.announcementsRepository.countBy(reads.length > 0 ? {
id: Not(In(reads.map(read => read.announcementId))),
userId: userId,
} : { userId: userId });

const count = count1 + count2;
Comment on lines +220 to +229
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1クエリでできそう


return count > 0;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/models/entities/Announcement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export class Announcement {
})
public imageUrl: string | null;

@Column('varchar', {
length: 64, nullable: true,
})
public userId: string | null;

constructor(data: Partial<Announcement>) {
if (data == null) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
userId: {
type: 'string',
optional: false, nullable: true,
},
},
},
} as const;
Expand All @@ -52,6 +56,7 @@ export const paramDef = {
title: { type: 'string', minLength: 1 },
text: { type: 'string', minLength: 1 },
imageUrl: { type: 'string', nullable: true, minLength: 1 },
userId: { type: 'string', nullable: true },
},
required: ['title', 'text', 'imageUrl'],
} as const;
Expand All @@ -73,8 +78,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
title: ps.title,
text: ps.text,
imageUrl: ps.imageUrl,
userId: ps.userId,
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));

return Object.assign({}, announcement, { createdAt: announcement.createdAt.toISOString(), updatedAt: null });
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/index.js';
import type { AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository } from '@/models/index.js';
import type { Announcement } from '@/models/entities/Announcement.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
Expand Down Expand Up @@ -46,6 +46,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
userId: {
type: 'string',
optional: false, nullable: true,
},
reads: {
type: 'number',
optional: false, nullable: false,
Expand All @@ -61,6 +65,7 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id' },
},
required: [],
} as const;
Expand All @@ -74,11 +79,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {

@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,

@Inject(DI.usersRepository)
private usersRepository: UsersRepository,

private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
const builder = this.announcementsRepository.createQueryBuilder('announcement');
if (ps.userId) {
builder.where('"userId" = :userId', { userId: ps.userId });
} else {
builder.where('"userId" IS NULL');
}

const query = this.queryService.makePaginationQuery(builder, ps.sinceId, ps.untilId);

const announcements = await query.take(ps.limit).getMany();

Expand All @@ -90,15 +105,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}));
}

return announcements.map(announcement => ({
return await Promise.all(announcements.map(async announcement => ({
id: announcement.id,
createdAt: announcement.createdAt.toISOString(),
updatedAt: announcement.updatedAt?.toISOString() ?? null,
title: announcement.title,
text: announcement.text,
imageUrl: announcement.imageUrl,
userId: announcement.userId,
user: announcement.userId ? await this.usersRepository.findOneBy({ id: announcement.userId }) : null,
CyberRex0 marked this conversation as resolved.
Show resolved Hide resolved
reads: reads.get(announcement)!,
}));
})));
});
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AnnouncementsRepository } from '@/models/index.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';

Expand All @@ -26,8 +26,9 @@ export const paramDef = {
title: { type: 'string', minLength: 1 },
text: { type: 'string', minLength: 1 },
imageUrl: { type: 'string', nullable: true, minLength: 0 },
userId: { type: 'string', nullable: true },
},
required: ['id', 'title', 'text', 'imageUrl'],
required: ['id', 'title', 'text', 'imageUrl', 'userId'],
} as const;

// eslint-disable-next-line import/no-default-export
Expand All @@ -36,11 +37,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,

@Inject(DI.announcementReadsRepository)
private announcementsReadsRepository: AnnouncementReadsRepository,
) {
super(meta, paramDef, async (ps, me) => {
const announcement = await this.announcementsRepository.findOneBy({ id: ps.id });

if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);

if (announcement.userId && announcement.userId !== ps.userId) {
await this.announcementsReadsRepository.delete({ id: announcement.id, userId: announcement.userId });
}

await this.announcementsRepository.update(announcement.id, {
updatedAt: new Date(),
Expand Down
19 changes: 18 additions & 1 deletion packages/backend/src/server/api/endpoints/announcements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export const meta = {
type: 'boolean',
optional: true, nullable: false,
},
isPrivate: {
type: 'boolean',
optional: false, nullable: true,
},
},
},
},
Expand All @@ -60,6 +64,7 @@ export const paramDef = {
withUnreads: { type: 'boolean', default: false },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
privateOnly: { type: 'boolean', default: false },
},
required: [],
} as const;
Expand All @@ -77,8 +82,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
const builder = this.announcementsRepository.createQueryBuilder('announcement');
if (me) {
if (ps.privateOnly) {
builder.where('"userId" = :userId', { userId: me.id });
} else {
builder.where('"userId" IS NULL');
builder.orWhere('"userId" = :userId', { userId: me.id });
}
} else {
builder.where('"userId" IS NULL');
}

const query = this.queryService.makePaginationQuery(builder, ps.sinceId, ps.untilId);
const announcements = await query.take(ps.limit).getMany();

if (me) {
Expand All @@ -95,6 +111,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
...a,
createdAt: a.createdAt.toISOString(),
updatedAt: a.updatedAt?.toISOString() ?? null,
isPrivate: !!a.userId,
}));
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Check if announcement exists
const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId });

if (announcement == null) {
if (announcement == null || (announcement.userId && announcement.userId !== me.id)) {
throw new ApiError(meta.errors.noSuchAnnouncement);
}

Expand Down
7 changes: 6 additions & 1 deletion packages/frontend/src/boot/main-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { computed, createApp, watch, markRaw, version as vueVersion, defineAsync
import { common } from './common';
import { version, ui, lang, updateLocale } from '@/config';
import { i18n, updateI18n } from '@/i18n';
import { confirm, alert, post, popup, toast } from '@/os';
import { confirm, alert, post, popup, toast, api } from '@/os';
import { useStream } from '@/stream';
import * as sound from '@/scripts/sound';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
Expand Down Expand Up @@ -245,6 +245,11 @@ export async function mainBoot() {
main.on('myTokenRegenerated', () => {
signout();
});

const unreadUserAnnouncementsList = await api('announcements', { privateOnly: true, withUnreads: true, limit: 1 });
if (unreadUserAnnouncementsList.length > 0) {
popup(defineAsyncComponent(() => import('@/components/MkNewUserAnnouncementModal.vue')), {}, {}, 'closed');
}
}

// shortcut
Expand Down
49 changes: 49 additions & 0 deletions packages/frontend/src/components/MkNewUserAnnouncementModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<MkModal ref="modal" :zPriority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
<div :class="$style.root">
<div :class="$style.title">{{ i18n.ts.newUserAnnouncementAvailable }}</div>
<MkButton :class="$style.gotIt" primary full @click="jumpTo">{{ i18n.ts.viewAnnouncement }}</MkButton>
</div>
</MkModal>
</template>

<script setup lang="ts">
import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n';
import { useRouter } from '@/router';
const router = useRouter();
const modal = shallowRef<InstanceType<typeof MkModal>>();

function jumpTo() {
modal.value.close();
router.push('/announcements');
}
</script>

<style lang="scss" module>
.root {
margin: auto;
position: relative;
padding: 32px;
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
text-align: center;
background: var(--panel);
border-radius: var(--radius);
}

.title {
font-weight: bold;
}

.version {
margin: 1em 0;
}

.gotIt {
margin: 8px 0 0 0;
}
</style>
Loading