Merge branch 'develop' into vue3

This commit is contained in:
syuilo
2020-08-25 08:54:57 +09:00
24 changed files with 390 additions and 145 deletions

View File

@@ -2,7 +2,7 @@
<x-column :column="column" :is-stacked="isStacked" :menu="menu">
<template #header><fa :icon="faBell" style="margin-right: 8px;"/>{{ column.name }}</template>
<x-notifications/>
<x-notifications :include-types="column.includingTypes"/>
</x-column>
</template>
@@ -38,28 +38,14 @@ export default defineComponent({
},
created() {
if (this.column.notificationType == null) {
this.column.notificationType = 'all';
this.$store.commit('deviceUser/updateDeckColumn', this.column);
}
this.menu = [{
icon: faCog,
text: this.$t('notificationType'),
action: () => {
this.$root.dialog({
title: this.$t('notificationType'),
type: null,
select: {
items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
value: x, text: this.$t(`_notification._types.${x}`)
}))
default: this.column.notificationType,
},
showCancelButton: true
}).then(({ canceled, result: type }) => {
if (canceled) return;
this.column.notificationType = type;
text: this.$t('notificationSetting'),
action: async () => {
this.$root.new(await import('../notification-setting-window.vue').then(m => m.default), {
includingTypes: this.column.includingTypes,
}).$on('ok', async ({ includingTypes }) => {
this.$set(this.column, 'includingTypes', includingTypes);
this.$store.commit('deviceUser/updateDeckColumn', this.column);
});
}

View File

@@ -0,0 +1,98 @@
<template>
<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()">
<template #header>{{ $t('notificationSetting') }}</template>
<div class="vv94n3oa">
<div v-if="showGlobalToggle">
<mk-switch v-model="useGlobalSetting">
{{ $t('useGlobalSetting') }}
<template #desc>{{ $t('useGlobalSettingDesc') }}</template>
</mk-switch>
</div>
<div v-if="!useGlobalSetting">
<mk-info>{{ $t('notificationSettingDesc') }}</mk-info>
<mk-button inline @click="disableAll">{{ $t('disableAll') }}</mk-button>
<mk-button inline @click="enableAll">{{ $t('enableAll') }}</mk-button>
<mk-switch v-for="type in notificationTypes" :key="type" v-model="typesMap[type]">{{ $t(`_notification._types.${type}`) }}</mk-switch>
</div>
</div>
</x-window>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
import XWindow from './window.vue';
import MkSwitch from './ui/switch.vue';
import MkInfo from './ui/info.vue';
import MkButton from './ui/button.vue';
import { notificationTypes } from '../../types';
export default Vue.extend({
components: {
XWindow,
MkSwitch,
MkInfo,
MkButton
},
props: {
includingTypes: {
// TODO: これで型に合わないものを弾いてくれるのかどうか要調査
type: Array as PropType<typeof notificationTypes[number][]>,
required: false,
default: null,
},
showGlobalToggle: {
type: Boolean,
required: false,
default: true,
}
},
data() {
return {
typesMap: {} as Record<typeof notificationTypes[number], boolean>,
useGlobalSetting: false,
notificationTypes,
};
},
created() {
this.useGlobalSetting = this.includingTypes === null && this.showGlobalToggle;
for (const type of this.notificationTypes) {
Vue.set(this.typesMap, type, this.includingTypes === null || this.includingTypes.includes(type));
}
},
methods: {
ok() {
const includingTypes = this.useGlobalSetting ? null : (Object.keys(this.typesMap) as typeof notificationTypes[number][])
.filter(type => this.typesMap[type]);
this.$emit('ok', { includingTypes });
this.$refs.window.close();
},
disableAll() {
for (const type in this.typesMap) {
this.typesMap[type as typeof notificationTypes[number]] = false;
}
},
enableAll() {
for (const type in this.typesMap) {
this.typesMap[type as typeof notificationTypes[number]] = true;
}
}
}
});
</script>
<style lang="scss" scoped>
.vv94n3oa {
> div {
border-top: solid 1px var(--divider);
padding: 24px;
}
}
</style>

View File

@@ -17,11 +17,12 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, PropType } from 'vue';
import paging from '../scripts/paging';
import XNotification from './notification.vue';
import XList from './date-separated-list.vue';
import XNote from './note.vue';
import { notificationTypes } from '../../types';
export default defineComponent({
components: {
@@ -35,9 +36,10 @@ export default defineComponent({
],
props: {
type: {
type: String,
required: false
includeTypes: {
type: Array as PropType<typeof notificationTypes[number][]>,
required: false,
default: null,
},
},
@@ -48,15 +50,26 @@ export default defineComponent({
endpoint: 'i/notifications',
limit: 10,
params: () => ({
includeTypes: this.type ? [this.type] : undefined
includeTypes: this.allIncludeTypes || undefined,
})
},
};
},
computed: {
allIncludeTypes() {
return this.includeTypes ?? this.$store.state.i.includingNotificationTypes;
}
},
watch: {
type() {
includeTypes() {
this.reload();
},
'$store.state.i.includingNotificationTypes'() {
if (this.includeTypes === null) {
this.reload();
}
}
},
@@ -71,16 +84,20 @@ export default defineComponent({
methods: {
onNotification(notification) {
if (document.visibilityState === 'visible') {
//
const isMuted = !!this.allIncludeTypes && !this.allIncludeTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') {
this.$root.stream.send('readNotification', {
id: notification.id
});
}
this.prepend({
...notification,
isRead: document.visibilityState === 'visible'
});
if (!isMuted) {
this.prepend({
...notification,
isRead: document.visibilityState === 'visible'
});
}
},
noteUpdated(oldValue, newValue) {

View File

@@ -13,7 +13,11 @@
>
<slot></slot>
</select>
<div class="suffix"><slot name="suffix"></slot></div>
<div class="suffix">
<slot name="suffix">
<fa :icon="faChevronDown"/>
</slot>
</div>
</div>
<div class="text"><slot name="text"></slot></div>
</div>
@@ -21,6 +25,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
export default defineComponent({
props: {
@@ -43,7 +48,8 @@ export default defineComponent({
},
data() {
return {
focused: false
focused: false,
faChevronDown,
};
},
computed: {
@@ -157,6 +163,8 @@ export default defineComponent({
border-radius: 0;
outline: none;
box-shadow: none;
appearance: none;
-webkit-appearance: none;
color: var(--fg);
option,
@@ -172,7 +180,7 @@ export default defineComponent({
justify-self: center;
font-size: 1em;
line-height: 32px;
color: rgba(#000, 0.54);
color: var(--inputLabel);
pointer-events: none;
&:empty {

View File

@@ -155,6 +155,11 @@ export default defineComponent({
},
async onNotification(notification) {
const t = this.$store.state.i.includingNotificationTypes;
if (!!t && !t.includes(notification.type)) {
return;
}
if (document.visibilityState === 'visible') {
this.$root.stream.send('readNotification', {
id: notification.id
@@ -164,7 +169,6 @@ export default defineComponent({
notification
});
}
this.$root.sound('notification');
},

View File

@@ -325,6 +325,10 @@ export default defineComponent({
},
async onNotification(notification) {
const t = this.$store.state.i.includingNotificationTypes;
if (!!t && !t.includes(notification.type)) {
return;
}
if (document.visibilityState === 'visible') {
this.$root.stream.send('readNotification', {
id: notification.id

View File

@@ -21,6 +21,9 @@
<mk-button @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</mk-button>
<mk-button @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</mk-button>
</div>
<div class="_content">
<mk-button @click="configure">{{ $t('notificationSetting') }}</mk-button>
</div>
</section>
<x-import-export class="_vMargin"/>
@@ -108,6 +111,24 @@ export default defineComponent({
readAllNotifications() {
this.$root.api('notifications/mark-all-as-read');
},
async configure() {
this.$root.new(await import('../../components/notification-setting-window.vue').then(m => m.default), {
includingTypes: this.$store.state.i.includingNotificationTypes,
showGlobalToggle: false,
}).$on('ok', async ({ includingTypes: value }: any) => {
await this.$root.api('i/update', {
includingNotificationTypes: value,
}).then(i => {
this.$store.state.i.includingNotificationTypes = i.includingNotificationTypes;
}).catch(err => {
this.$root.dialog({
type: 'error',
text: err.message
});
});
});
}
}
});
</script>

View File

@@ -76,7 +76,7 @@
<mk-container :body-togglable="true" :expanded="true">
<template #header><fa :icon="faCode"/> {{ $t('script') }}</template>
<div>
<prism-editor v-model="script" :line-numbers="false" language="js"/>
<prism-editor class="_code" v-model="script" :highlight="highlighter" :line-numbers="false"/>
</div>
</mk-container>
</div>
@@ -85,9 +85,13 @@
<script lang="ts">
import { defineComponent } from 'vue';
import * as XDraggable from 'vuedraggable';
import "prismjs";
import 'prismjs';
import { highlight, languages } from 'prismjs/components/prism-core';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript';
import 'prismjs/themes/prism-okaidia.css';
import PrismEditor from 'vue-prism-editor';
import { PrismEditor } from 'vue-prism-editor';
import 'vue-prism-editor/dist/prismeditor.min.css';
import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import { v4 as uuid } from 'uuid';
@@ -416,7 +420,11 @@ export default defineComponent({
removeEyeCatchingImage() {
this.eyeCatchingImageId = null;
}
},
highlighter(code) {
return highlight(code, languages.js, 'javascript');
},
}
});
</script>

View File

@@ -3,7 +3,7 @@
<teleport to="#_teleport_header"><fa :icon="faTerminal"/>{{ $t('scratchpad') }}</teleport>
<div class="_panel">
<prism-editor v-model="code" :line-numbers="false" language="js"/>
<prism-editor class="_code" v-model="code" :highlight="highlighter" :line-numbers="false"/>
<mk-button style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><fa :icon="faPlay"/></mk-button>
</div>
@@ -23,9 +23,13 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { faTerminal, faPlay } from '@fortawesome/free-solid-svg-icons';
import "prismjs";
import 'prismjs';
import { highlight, languages } from 'prismjs/components/prism-core';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript';
import 'prismjs/themes/prism-okaidia.css';
import PrismEditor from 'vue-prism-editor';
import { PrismEditor } from 'vue-prism-editor';
import 'vue-prism-editor/dist/prismeditor.min.css';
import { AiScript, parse, utils, values } from '@syuilo/aiscript';
import MkContainer from '../components/ui/container.vue';
import MkButton from '../components/ui/button.vue';
@@ -118,7 +122,11 @@ export default defineComponent({
text: e
});
}
}
},
highlighter(code) {
return highlight(code, languages.js, 'javascript');
},
}
});
</script>

View File

@@ -21,6 +21,11 @@ export type FormItem = {
default: string | null;
hidden?: boolean;
enum: string[];
} | {
label?: string;
type: 'array';
default: unknown[] | null;
hidden?: boolean;
};
export type Form = Record<string, FormItem>;

View File

@@ -448,6 +448,19 @@ hr {
opacity: 0.7;
}
._code {
background: #2d2d2d;
color: #ccc;
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
font-size: 14px;
line-height: 1.5;
padding: 5px;
}
.prism-editor__textarea:focus {
outline: none;
}
.zoom-enter-active, .zoom-leave-active {
transition: opacity 0.5s, transform 0.5s !important;
}

View File

@@ -1,16 +1,17 @@
<template>
<mk-container :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true">
<template #header><fa :icon="faBell"/>{{ $t('notifications') }}</template>
<template #func><button @click="configure()" class="_button"><fa :icon="faCog"/></button></template>
<div>
<x-notifications/>
<x-notifications :include-types="props.includingTypes"/>
</div>
</mk-container>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faBell } from '@fortawesome/free-solid-svg-icons';
import { faBell, faCog } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../components/ui/container.vue';
import XNotifications from '../components/notifications.vue';
import define from './define';
@@ -26,6 +27,11 @@ const widget = define({
type: 'number',
default: 300,
},
includingTypes: {
type: 'array',
hidden: true,
default: null,
},
})
});
@@ -38,8 +44,19 @@ export default defineComponent({
data() {
return {
faBell
faBell, faCog
};
},
methods: {
async configure() {
this.$root.new(await import('../components/notification-setting-window.vue').then(m => m.default), {
includingTypes: this.props.includingTypes,
}).$on('ok', async ({ includingTypes }) => {
this.props.includingTypes = includingTypes;
this.save();
});
}
}
});
</script>

View File

@@ -2,6 +2,7 @@ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'type
import { id } from '../id';
import { User } from './user';
import { Page } from './page';
import { notificationTypes } from '../../types';
@Entity()
export class UserProfile {
@@ -158,6 +159,13 @@ export class UserProfile {
})
public mutedWords: string[][];
@Column('enum', {
enum: notificationTypes,
array: true,
nullable: true,
})
public includingNotificationTypes: typeof notificationTypes[number][] | null;
//#region Denormalized fields
@Index()
@Column('varchar', {

View File

@@ -248,6 +248,7 @@ export class UserRepository extends Repository<User> {
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
integrations: profile!.integrations,
mutedWords: profile!.mutedWords,
includingNotificationTypes: profile?.includingNotificationTypes,
} : {}),
...(opts.includeSecrets ? {

View File

@@ -44,12 +44,10 @@ export const meta = {
includeTypes: {
validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])),
default: [] as string[]
},
excludeTypes: {
validator: $.optional.arr($.str.or(notificationTypes as unknown as string[])),
default: [] as string[]
}
},
@@ -65,6 +63,14 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
// includeTypes が空の場合はクエリしない
if (ps.includeTypes && ps.includeTypes.length === 0) {
return [];
}
// excludeTypes に全指定されている場合はクエリしない
if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
return [];
}
const followingQuery = Followings.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: user.id });
@@ -91,9 +97,9 @@ export default define(meta, async (ps, user) => {
query.setParameters(followingQuery.getParameters());
}
if (ps.includeTypes!.length > 0) {
if (ps.includeTypes?.length > 0) {
query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes });
} else if (ps.excludeTypes!.length > 0) {
} else if (ps.excludeTypes?.length > 0) {
query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes });
}

View File

@@ -14,6 +14,7 @@ import { Users, DriveFiles, UserProfiles, Pages } from '../../../../models';
import { User } from '../../../../models/entities/user';
import { UserProfile } from '../../../../models/entities/user-profile';
import { ensure } from '../../../../prelude/ensure';
import { notificationTypes } from '../../../../types';
export const meta = {
desc: {
@@ -147,6 +148,10 @@ export const meta = {
mutedWords: {
validator: $.optional.arr($.arr($.str))
},
includingNotificationTypes: {
validator: $.optional.arr($.str.or(notificationTypes as unknown as string[]))
},
},
errors: {
@@ -201,6 +206,7 @@ export default define(meta, async (ps, user, token) => {
profileUpdates.mutedWords = ps.mutedWords;
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
}
if (ps.includingNotificationTypes !== undefined) profileUpdates.includingNotificationTypes = ps.includingNotificationTypes as typeof notificationTypes[number][];
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;

View File

@@ -1,6 +1,6 @@
import { publishMainStream } from './stream';
import pushSw from './push-notification';
import { Notifications, Mutings } from '../models';
import { Notifications, Mutings, UserProfiles } from '../models';
import { genId } from '../misc/gen-id';
import { User } from '../models/entities/user';
import { Notification } from '../models/entities/notification';
@@ -14,13 +14,18 @@ export async function createNotification(
return null;
}
const profile = await UserProfiles.findOne({ userId: notifieeId });
const isMuted = !profile?.includingNotificationTypes?.includes(type);
// Create notification
const notification = await Notifications.save({
id: genId(),
createdAt: new Date(),
notifieeId: notifieeId,
type: type,
isRead: false,
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
isRead: isMuted,
...data
} as Partial<Notification>);