Compare commits
5 Commits
4c5cf43ea1
...
a9f5743495
| Author | SHA1 | Date |
|---|---|---|
|
|
a9f5743495 | |
|
|
a1c87d21c4 | |
|
|
afc3c3f0c7 | |
|
|
17bb39c760 | |
|
|
9f69035607 |
|
|
@ -16,6 +16,10 @@
|
|||
|
||||
### General
|
||||
- OAuth 2.0のサポート
|
||||
- お知らせ機能の強化
|
||||
- ユーザー個別のお知らせを作成可能に
|
||||
- お知らせのバナー表示やダイアログ表示が可能に
|
||||
- お知らせのアイコンを設定可能に
|
||||
- チャンネルをセンシティブ指定できるようになりました
|
||||
|
||||
### Client
|
||||
|
|
|
|||
|
|
@ -1098,6 +1098,8 @@ export interface Locale {
|
|||
"doYouAgree": string;
|
||||
"beSureToReadThisAsItIsImportant": string;
|
||||
"iHaveReadXCarefullyAndAgree": string;
|
||||
"dialog": string;
|
||||
"icon": string;
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ watch: "ウォッチ"
|
|||
unwatch: "ウォッチ解除"
|
||||
accept: "許可"
|
||||
reject: "拒否"
|
||||
normal: "正常"
|
||||
normal: "通常"
|
||||
instanceName: "サーバー名"
|
||||
instanceDescription: "サーバーの紹介"
|
||||
maintainerName: "管理者の名前"
|
||||
|
|
@ -1095,6 +1095,8 @@ expired: "期限切れ"
|
|||
doYouAgree: "同意しますか?"
|
||||
beSureToReadThisAsItIsImportant: "重要ですので必ずお読みください。"
|
||||
iHaveReadXCarefullyAndAgree: "「{x}」の内容をよく読み、同意します。"
|
||||
dialog: "ダイアログ"
|
||||
icon: "アイコン"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
export class RefineAnnouncement1691649257651 {
|
||||
name = 'RefineAnnouncement1691649257651'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "announcement" ADD "display" character varying(256) NOT NULL DEFAULT 'normal'`);
|
||||
await queryRunner.query(`ALTER TABLE "announcement" ADD "needConfirmationToRead" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "announcement" ADD "isActive" boolean NOT NULL DEFAULT true`);
|
||||
await queryRunner.query(`ALTER TABLE "announcement" ADD "forExistingUsers" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "announcement" ADD "userId" character varying(32)`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_bc1afcc8ef7e9400cdc3c0a87e" ON "announcement" ("isActive") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_da795d3a83187e8832005ba19d" ON "announcement" ("forExistingUsers") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_fd25dfe3da37df1715f11ba6ec" ON "announcement" ("userId") `);
|
||||
await queryRunner.query(`ALTER TABLE "announcement" ADD CONSTRAINT "FK_fd25dfe3da37df1715f11ba6ec8" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "announcement" DROP CONSTRAINT "FK_fd25dfe3da37df1715f11ba6ec8"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_fd25dfe3da37df1715f11ba6ec"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_da795d3a83187e8832005ba19d"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_bc1afcc8ef7e9400cdc3c0a87e"`);
|
||||
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "userId"`);
|
||||
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "forExistingUsers"`);
|
||||
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "isActive"`);
|
||||
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "needConfirmationToRead"`);
|
||||
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "display"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export class RefineAnnouncement21691657412740 {
|
||||
name = 'RefineAnnouncement21691657412740'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "announcement" ADD "icon" character varying(256) NOT NULL DEFAULT 'info'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "icon"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,7 @@ export class AnnouncementService {
|
|||
title: values.title,
|
||||
text: values.text,
|
||||
imageUrl: values.imageUrl,
|
||||
icon: values.icon,
|
||||
display: values.display,
|
||||
forExistingUsers: values.forExistingUsers,
|
||||
needConfirmationToRead: values.needConfirmationToRead,
|
||||
|
|
@ -118,6 +119,7 @@ export class AnnouncementService {
|
|||
text: announcement.text,
|
||||
title: announcement.title,
|
||||
imageUrl: announcement.imageUrl,
|
||||
icon: announcement.icon,
|
||||
display: announcement.display,
|
||||
needConfirmationToRead: announcement.needConfirmationToRead,
|
||||
forYou: announcement.userId === me?.id,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ export class Announcement {
|
|||
})
|
||||
public imageUrl: string | null;
|
||||
|
||||
// info, warning, error, success
|
||||
@Column('varchar', {
|
||||
length: 256, nullable: false,
|
||||
default: 'info',
|
||||
})
|
||||
public icon: string;
|
||||
|
||||
// normal ... お知らせページ掲載
|
||||
// banner ... お知らせページ掲載 + バナー表示
|
||||
// dialog ... お知らせページ掲載 + ダイアログ表示
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ export const packedAnnouncementSchema = {
|
|||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
icon: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
display: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export const paramDef = {
|
|||
title: { type: 'string', minLength: 1 },
|
||||
text: { type: 'string', minLength: 1 },
|
||||
imageUrl: { type: 'string', nullable: true, minLength: 1 },
|
||||
icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'], default: 'info' },
|
||||
display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' },
|
||||
forExistingUsers: { type: 'boolean', default: false },
|
||||
needConfirmationToRead: { type: 'boolean', default: false },
|
||||
|
|
@ -76,6 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
title: ps.title,
|
||||
text: ps.text,
|
||||
imageUrl: ps.imageUrl,
|
||||
icon: ps.icon,
|
||||
display: ps.display,
|
||||
forExistingUsers: ps.forExistingUsers,
|
||||
needConfirmationToRead: ps.needConfirmationToRead,
|
||||
|
|
|
|||
|
|
@ -108,9 +108,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
title: announcement.title,
|
||||
text: announcement.text,
|
||||
imageUrl: announcement.imageUrl,
|
||||
icon: announcement.icon,
|
||||
display: announcement.display,
|
||||
isActive: announcement.isActive,
|
||||
forExistingUsers: announcement.forExistingUsers,
|
||||
needConfirmationToRead: announcement.needConfirmationToRead,
|
||||
userId: announcement.userId,
|
||||
reads: reads.get(announcement)!,
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,12 +31,13 @@ export const paramDef = {
|
|||
title: { type: 'string', minLength: 1 },
|
||||
text: { type: 'string', minLength: 1 },
|
||||
imageUrl: { type: 'string', nullable: true, minLength: 0 },
|
||||
icon: { type: 'string', enum: ['info', 'warning', 'error', 'success'] },
|
||||
display: { type: 'string', enum: ['normal', 'banner', 'dialog'] },
|
||||
forExistingUsers: { type: 'boolean' },
|
||||
needConfirmationToRead: { type: 'boolean' },
|
||||
isActive: { type: 'boolean' },
|
||||
},
|
||||
required: ['id', 'title', 'text', 'imageUrl', 'display', 'forExistingUsers', 'needConfirmationToRead', 'isActive'],
|
||||
required: ['id', 'title', 'text', 'imageUrl', 'icon', 'display', 'forExistingUsers', 'needConfirmationToRead', 'isActive'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
@ -58,6 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
|
||||
imageUrl: ps.imageUrl || null,
|
||||
display: ps.display,
|
||||
icon: ps.icon,
|
||||
forExistingUsers: ps.forExistingUsers,
|
||||
needConfirmationToRead: ps.needConfirmationToRead,
|
||||
isActive: ps.isActive,
|
||||
|
|
|
|||
|
|
@ -108,8 +108,8 @@ function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Pr
|
|||
})
|
||||
.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
|
||||
if (res.status >= 500 && res.status < 600) {
|
||||
// サーバーエラー(5xx)の場合をrejectとする
|
||||
// (認証エラーなど4xxはresolve)
|
||||
// サーバーエラー(5xx)の場合をrejectとする
|
||||
// (認証エラーなど4xxはresolve)
|
||||
return fail2(res);
|
||||
}
|
||||
res.json().then(done2, fail2);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,21 @@ export async function mainBoot() {
|
|||
}
|
||||
});
|
||||
|
||||
for (const announcement of ($i.unreadAnnouncements ?? []).filter(x => x.display === 'dialog')) {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
|
||||
announcement,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
|
||||
stream.on('announcementCreated', (ev) => {
|
||||
const announcement = ev.announcement;
|
||||
if (announcement.display === 'dialog') {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
|
||||
announcement,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
});
|
||||
|
||||
if ($i.isDeleted) {
|
||||
alert({
|
||||
type: 'warning',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" :zPriority="'middle'" @closed="$emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.header">
|
||||
<span :class="$style.icon">
|
||||
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
||||
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
|
||||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<span :class="$style.title">{{ announcement.title }}</span>
|
||||
</div>
|
||||
<div :class="$style.text"><Mfm :text="announcement.text"/></div>
|
||||
<MkButton primary full @click="ok">{{ i18n.ts.ok }}</MkButton>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
announcement: misskey.entities.Announcement;
|
||||
}>(), {
|
||||
});
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
||||
function ok() {
|
||||
modal.value.close();
|
||||
os.api('i/read-announcement', { announcementId: props.announcement.id });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
box-sizing: border-box;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 1em 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -19,6 +19,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInput v-model="announcement.imageUrl">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkRadios v-model="announcement.icon">
|
||||
<template #label>{{ i18n.ts.icon }}</template>
|
||||
<option value="info"><i class="ti ti-info-circle"></i></option>
|
||||
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option>
|
||||
<option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
|
||||
<option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
|
||||
</MkRadios>
|
||||
<MkRadios v-model="announcement.display">
|
||||
<template #label>{{ i18n.ts.display }}</template>
|
||||
<option value="normal">{{ i18n.ts.normal }}</option>
|
||||
<option value="banner">{{ i18n.ts.banner }}</option>
|
||||
<option value="dialog">{{ i18n.ts.dialog }}</option>
|
||||
</MkRadios>
|
||||
<MkSwitch v-model="announcement.forExistingUsers">
|
||||
{{ i18n.ts._announcement.forExistingUsers }}
|
||||
<template #caption>{{ i18n.ts._announcement.forExistingUsersDescription }}</template>
|
||||
|
|
@ -30,8 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
|
||||
<div class="buttons _buttons">
|
||||
<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton class="button" inline @click="deactivate(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts._announcement.deactivate }}</MkButton>
|
||||
<MkButton class="button" inline danger @click="remove(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
<MkButton v-if="announcement.id != null" class="button" inline @click="deactivate(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts._announcement.deactivate }}</MkButton>
|
||||
<MkButton v-if="announcement.id != null" class="button" inline danger @click="remove(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -46,6 +59,8 @@ import XHeader from './_header_.vue';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
|
@ -62,6 +77,7 @@ function add() {
|
|||
title: '',
|
||||
text: '',
|
||||
imageUrl: null,
|
||||
icon: 'info',
|
||||
display: 'normal',
|
||||
forExistingUsers: false,
|
||||
needConfirmationToRead: false,
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ export type MeDetailed = UserDetailed & {
|
|||
noCrawle: boolean;
|
||||
receiveAnnouncementEmail: boolean;
|
||||
usePasswordLessLogin: boolean;
|
||||
unreadAnnouncements: Announcement[];
|
||||
[other: string]: any;
|
||||
};
|
||||
|
||||
|
|
@ -413,6 +414,10 @@ export type Announcement = {
|
|||
text: string;
|
||||
title: string;
|
||||
imageUrl: string | null;
|
||||
display: 'normal' | 'banner' | 'dialog';
|
||||
icon: 'info' | 'warning' | 'error' | 'success';
|
||||
needConfirmationToRead: boolean;
|
||||
forYou: boolean;
|
||||
isRead?: boolean;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue