Merge branch 'develop' into notification-read-api

This commit is contained in:
tamaina 2021-12-25 02:02:49 +09:00
commit a456a63e34
19 changed files with 212 additions and 171 deletions

View file

@ -7,6 +7,13 @@
-->
## 12.x.x (unreleased)
### Improvements
### Bugfixes
- クライアント: 一部のコンポーネントが裏に隠れるのを修正
## 12.100.2 (2021/12/18)
### Bugfixes

View file

@ -87,7 +87,7 @@ Configuration files are located in [`/.github/workflows`](/.github/workflows).
## Vue
Misskey uses Vue(v3) as its front-end framework.
**When creating a new component, please use the Composition API instead of the Options API.**
**When creating a new component, please use the Composition API (and [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html)) instead of the Options API.**
Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome.
## Adding MisskeyRoom items

View file

@ -614,7 +614,6 @@ regenerateLoginToken: "ログイントークンを再生成"
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
fileIdOrUrl: "ファイルIDまたはURL"
chatOpenBehavior: "チャットを開くときの動作"
behavior: "動作"
sample: "サンプル"
abuseReports: "通報"

View file

@ -4,6 +4,7 @@ block vars
- const user = note.user;
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
- const url = `${config.url}/notes/${note.id}`;
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
block title
= `${title} | ${instanceName}`
@ -19,7 +20,7 @@ block og
meta(property='og:image' content= user.avatarUrl)
block meta
if user.host || profile.noCrawle
if user.host || isRenote || profile.noCrawle
meta(name='robots' content='noindex')
meta(name='misskey:user-username' content=user.username)

View file

@ -3,10 +3,33 @@ import config from '@/config/index';
import { SwSubscriptions } from '@/models/index';
import { fetchMeta } from '@/misc/fetch-meta';
import { Packed } from '@/misc/schema';
import { getNoteSummary } from '@/misc/get-note-summary';
type notificationType = 'notification' | 'unreadMessagingMessage';
type notificationBody = Packed<'Notification'> | Packed<'MessagingMessage'>;
// プッシュメッセージサーバーには文字数制限があるため、内容を削減します
function truncateNotification(notification: Packed<'Notification'>): any {
if (notification.note) {
return {
...notification,
note: {
...notification.note,
// textをgetNoteSummaryしたものに置き換える
text: getNoteSummary(notification.type === 'renote' ? notification.note.renote as Packed<'Note'> : notification.note),
...{
cw: undefined,
reply: undefined,
renote: undefined,
user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる
}
}
};
}
return notification;
}
export default async function(userId: string, type: notificationType, body: notificationBody) {
const meta = await fetchMeta();
@ -32,7 +55,9 @@ export default async function(userId: string, type: notificationType, body: noti
};
push.sendNotification(pushSubscription, JSON.stringify({
type, body,
type,
body: type === 'notification' ? truncateNotification(body as Packed<'Notification'>) : body,
userId,
}), {
proxy: config.proxy,
}).catch((err: any) => {

View file

@ -106,11 +106,6 @@ export default defineComponent({
return;
}
if (this.to.startsWith('/my/messaging')) {
if (ColdDeviceStorage.get('chatOpenBehavior') === 'window') return this.window();
if (ColdDeviceStorage.get('chatOpenBehavior') === 'popout') return this.popout();
}
if (this.behavior) {
if (this.behavior === 'window') {
return this.window();

View file

@ -1,7 +1,5 @@
<template>
<component :is="self ? 'MkA' : 'a'" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
<component :is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
@contextmenu.stop="() => {}"
>
<template v-if="!self">
@ -20,11 +18,11 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, ref } from 'vue';
import { toUnicode as decodePunycode } from 'punycode/';
import { url as local } from '@/config';
import { isTouchUsing } from '@/scripts/touch';
import * as os from '@/os';
import { useTooltip } from '@/scripts/use-tooltip';
export default defineComponent({
props: {
@ -35,74 +33,36 @@ export default defineComponent({
rel: {
type: String,
required: false,
default: null,
}
},
data() {
const self = this.url.startsWith(local);
setup(props) {
const self = props.url.startsWith(local);
const url = new URL(props.url);
const el = ref();
useTooltip(el, (showing) => {
os.popup(import('@/components/url-preview-popup.vue'), {
showing,
url: props.url,
source: el.value,
}, {}, 'closed');
});
return {
local,
schema: null as string | null,
hostname: null as string | null,
port: null as string | null,
pathname: null as string | null,
query: null as string | null,
hash: null as string | null,
schema: url.protocol,
hostname: decodePunycode(url.hostname),
port: url.port,
pathname: decodeURIComponent(url.pathname),
query: decodeURIComponent(url.search),
hash: decodeURIComponent(url.hash),
self: self,
attr: self ? 'to' : 'href',
target: self ? null : '_blank',
showTimer: null,
hideTimer: null,
checkTimer: null,
close: null,
el,
};
},
created() {
const url = new URL(this.url);
this.schema = url.protocol;
this.hostname = decodePunycode(url.hostname);
this.port = url.port;
this.pathname = decodeURIComponent(url.pathname);
this.query = decodeURIComponent(url.search);
this.hash = decodeURIComponent(url.hash);
},
methods: {
async showPreview() {
if (!document.body.contains(this.$el)) return;
if (this.close) return;
const { dispose } = await os.popup(import('@/components/url-preview-popup.vue'), {
url: this.url,
source: this.$el
});
this.close = () => {
dispose();
};
this.checkTimer = setInterval(() => {
if (!document.body.contains(this.$el)) this.closePreview();
}, 1000);
},
closePreview() {
if (this.close) {
clearInterval(this.checkTimer);
this.close();
this.close = null;
}
},
onMouseover() {
if (isTouchUsing) return;
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.showTimer = setTimeout(this.showPreview, 500);
},
onMouseleave() {
if (isTouchUsing) return;
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(this.closePreview, 500);
}
}
});
</script>

View file

@ -105,6 +105,7 @@ export default defineComponent({
return {
previewable,
gallery,
pswpZIndex: os.claimZIndex('middle'),
};
},
});
@ -188,3 +189,11 @@ export default defineComponent({
}
}
</style>
<style lang="scss">
.pswp {
//
//z-index: v-bind(pswpZIndex);
z-index: 2000000;
}
</style>

View file

@ -16,7 +16,13 @@
<template #headerLeft>
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
</template>
<div class="yrolvcoq">
<template #headerRight>
<button v-tooltip="$ts.showInPage" class="_button" @click="expand()"><i class="fas fa-expand-alt"></i></button>
<button v-tooltip="$ts.popout" class="_button" @click="popout()"><i class="fas fa-external-link-alt"></i></button>
<button class="_button" @click="menu"><i class="fas fa-ellipsis-h"></i></button>
</template>
<div class="yrolvcoq" :style="{ background: pageInfo?.bg }">
<MkStickyContainer>
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
<component :is="component" v-bind="props" :ref="changePage"/>
@ -33,6 +39,7 @@ import copyToClipboard from '@/scripts/copy-to-clipboard';
import { resolve } from '@/router';
import { url } from '@/config';
import * as symbols from '@/symbols';
import * as os from '@/os';
export default defineComponent({
components: {
@ -139,6 +146,23 @@ export default defineComponent({
this.props = props;
},
menu(ev) {
os.popupMenu([{
icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab,
action: () => {
window.open(this.url, '_blank');
this.$refs.window.close();
}
}, {
icon: 'fas fa-link',
text: this.$ts.copyLink,
action: () => {
copyToClipboard(this.url);
}
}], ev.currentTarget || ev.target);
},
back() {
this.navigate(this.history.pop(), false);
},

View file

@ -284,7 +284,7 @@ export default defineComponent({
}
&.asDrawer {
padding: 12px 0;
padding: 12px 0 calc(env(safe-area-inset-bottom, 0px) + 12px) 0;
width: 100%;
> .item {

View file

@ -5,7 +5,12 @@
<MkError v-else-if="error" @retry="init()"/>
<div v-else-if="empty" key="_empty_" class="empty">
<slot name="empty"></slot>
<slot name="empty">
<div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $ts.nothing }}</div>
</div>
</slot>
</div>
<div v-else class="cxiknjgy">

View file

@ -414,6 +414,10 @@ export default defineComponent({
}
}
> .left {
min-width: 16px;
}
> .title {
flex: 1;
position: relative;
@ -421,7 +425,6 @@ export default defineComponent({
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
cursor: move;
}
}

View file

@ -1,12 +1,11 @@
<template>
<div>
<div class="_section">
<div class="_content">
<MkInput v-model="name">
<MkSpacer :content-max="700">
<div class="_formRoot">
<MkInput v-model="name" class="_formBlock">
<template #label>{{ $ts.name }}</template>
</MkInput>
<MkTextarea v-model="description">
<MkTextarea v-model="description" class="_formBlock">
<template #label>{{ $ts.description }}</template>
</MkTextarea>
@ -17,12 +16,11 @@
<MkButton @click="removeBannerImage()"><i class="fas fa-trash-alt"></i> {{ $ts._channel.removeBanner }}</MkButton>
</div>
</div>
</div>
<div class="_footer">
<div class="_formBlock">
<MkButton primary @click="save()"><i class="fas fa-save"></i> {{ channelId ? $ts.save : $ts.create }}</MkButton>
</div>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
@ -51,9 +49,11 @@ export default defineComponent({
[symbols.PAGE_INFO]: computed(() => this.channelId ? {
title: this.$ts._channel.edit,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
} : {
title: this.$ts._channel.create,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
}),
channel: null,
name: null,

View file

@ -1,6 +1,7 @@
<template>
<div v-if="channel" class="_section">
<div class="wpgynlbz _content _panel _gap" :class="{ hide: !showBanner }">
<MkSpacer :content-max="700">
<div v-if="channel">
<div class="wpgynlbz _panel _gap" :class="{ hide: !showBanner }">
<XChannelFollowButton :channel="channel" :full="true" class="subscribe"/>
<button class="_button toggle" @click="() => showBanner = !showBanner">
<template v-if="showBanner"><i class="fas fa-angle-up"></i></template>
@ -20,10 +21,11 @@
</div>
</div>
<XPostForm v-if="$i" :channel="channel" class="post-form _content _panel _gap" fixed/>
<XPostForm v-if="$i" :channel="channel" class="post-form _panel _gap" fixed/>
<XTimeline :key="channelId" class="_content _gap" src="channel" :channel="channelId" @before="before" @after="after"/>
<XTimeline :key="channelId" class="_gap" src="channel" :channel="channelId" @before="before" @after="after"/>
</div>
</MkSpacer>
</template>
<script lang="ts">
@ -55,6 +57,12 @@ export default defineComponent({
[symbols.PAGE_INFO]: computed(() => this.channel ? {
title: this.channel.name,
icon: 'fas fa-satellite-dish',
bg: 'var(--bg)',
actions: [...(this.$i && this.$i.id === this.channel.userId ? [{
icon: 'fas fa-cog',
text: this.$ts.edit,
handler: this.edit,
}] : [])],
} : null),
channel: null,
showBanner: true,
@ -79,8 +87,10 @@ export default defineComponent({
}
},
created() {
methods: {
edit() {
this.$router.push(`/channels/${this.channel.id}/edit`);
}
},
});
</script>

View file

@ -1,58 +1,63 @@
<template>
<div>
<div v-if="$i" class="_section" style="padding: 0;">
<MkTab v-model="tab" class="_content">
<option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._channel.featured }}</option>
<option value="following"><i class="fas fa-heart"></i> {{ $ts._channel.following }}</option>
<option value="owned"><i class="fas fa-edit"></i> {{ $ts._channel.owned }}</option>
</MkTab>
</div>
<div class="_section">
<MkSpacer :content-max="700">
<div v-if="tab === 'featured'" class="_content grwlizim featured">
<MkPagination v-slot="{items}" :pagination="featuredPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
<div v-if="tab === 'following'" class="_content grwlizim following">
<div v-else-if="tab === 'following'" class="_content grwlizim following">
<MkPagination v-slot="{items}" :pagination="followingPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
<div v-if="tab === 'owned'" class="_content grwlizim owned">
<div v-else-if="tab === 'owned'" class="_content grwlizim owned">
<MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
<MkPagination v-slot="{items}" :pagination="ownedPagination">
<MkChannelPreview v-for="channel in items" :key="channel.id" class="_gap" :channel="channel"/>
</MkPagination>
</div>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent } from 'vue';
import MkChannelPreview from '@/components/channel-preview.vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue';
import MkTab from '@/components/tab.vue';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
MkChannelPreview, MkPagination, MkButton, MkTab
MkChannelPreview, MkPagination, MkButton,
},
data() {
return {
[symbols.PAGE_INFO]: {
[symbols.PAGE_INFO]: computed(() => ({
title: this.$ts.channel,
icon: 'fas fa-satellite-dish',
action: {
bg: 'var(--bg)',
actions: [{
icon: 'fas fa-plus',
handler: this.create
}
},
text: this.$ts.create,
handler: this.create,
}],
tabs: [{
active: this.tab === 'featured',
title: this.$ts._channel.featured,
icon: 'fas fa-fire-alt',
onClick: () => { this.tab = 'featured'; },
}, {
active: this.tab === 'following',
title: this.$ts._channel.following,
icon: 'fas fa-heart',
onClick: () => { this.tab = 'following'; },
}, {
active: this.tab === 'owned',
title: this.$ts._channel.owned,
icon: 'fas fa-edit',
onClick: () => { this.tab = 'owned'; },
},]
})),
tab: 'featured',
featuredPagination: {
endpoint: 'channels/featured',

View file

@ -77,13 +77,6 @@
<FormSwitch v-model="defaultSideView">{{ $ts.openInSideView }}</FormSwitch>
</FormGroup>
<FormSelect v-model="chatOpenBehavior" class="_formBlock">
<template #label>{{ $ts.chatOpenBehavior }}</template>
<option value="page">{{ $ts.showInPage }}</option>
<option value="window">{{ $ts.openInWindow }}</option>
<option value="popout">{{ $ts.popout }}</option>
</FormSelect>
<FormLink to="/settings/deck" class="_formBlock">{{ $ts.deck }}</FormLink>
<FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="fas fa-code"></i></template>{{ $ts.customCss }}</FormLink>
@ -149,7 +142,6 @@ export default defineComponent({
disablePagesScript: defaultStore.makeGetterSetter('disablePagesScript'),
showFixedPostForm: defaultStore.makeGetterSetter('showFixedPostForm'),
defaultSideView: defaultStore.makeGetterSetter('defaultSideView'),
chatOpenBehavior: ColdDeviceStorage.makeGetterSetter('chatOpenBehavior'),
instanceTicker: defaultStore.makeGetterSetter('instanceTicker'),
enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'),
useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'),

View file

@ -1,4 +1,4 @@
import { Ref, ref, watch } from 'vue';
import { Ref, ref, watch, onUnmounted } from 'vue';
export function useTooltip(
elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>,
@ -18,6 +18,9 @@ export function useTooltip(
const open = () => {
close();
if (!isHovering) return;
if (elRef.value == null) return;
const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el;
if (!document.body.contains(el)) return; // openしようとしたときに既に元要素がDOMから消えている場合があるため
const showing = ref(true);
onShow(showing);
@ -69,9 +72,14 @@ export function useTooltip(
el.addEventListener('mouseleave', onMouseleave, { passive: true });
el.addEventListener('touchstart', onTouchstart, { passive: true });
el.addEventListener('touchend', onTouchend, { passive: true });
el.addEventListener('click', close, { passive: true });
}
}, {
immediate: true,
flush: 'post',
});
onUnmounted(() => {
close();
});
}

View file

@ -245,7 +245,6 @@ export class ColdDeviceStorage {
lightTheme: require('@/themes/l-light.json5') as Theme,
darkTheme: require('@/themes/d-dark.json5') as Theme,
syncDeviceDarkMode: true,
chatOpenBehavior: 'page' as 'page' | 'window' | 'popout',
plugins: [] as Plugin[],
mediaVolume: 0.5,
sound_masterVolume: 0.3,

View file

@ -3,7 +3,6 @@
*/
declare var self: ServiceWorkerGlobalScope;
import { getNoteSummary } from '@/scripts/get-note-summary';
import * as misskey from 'misskey-js';
function getUserName(user: misskey.entities.User): string {
@ -26,37 +25,37 @@ export default async function(type, data, i18n): Promise<[string, NotificationOp
switch (data.type) {
case 'mention':
return [i18n.t('_notification.youGotMention', { name: getUserName(data.user) }), {
body: getNoteSummary(data.note, i18n.locale),
body: data.note.text,
icon: data.user.avatarUrl
}];
case 'reply':
return [i18n.t('_notification.youGotReply', { name: getUserName(data.user) }), {
body: getNoteSummary(data.note, i18n.locale),
body: data.note.text,
icon: data.user.avatarUrl
}];
case 'renote':
return [i18n.t('_notification.youRenoted', { name: getUserName(data.user) }), {
body: getNoteSummary(data.note, i18n.locale),
body: data.note.text,
icon: data.user.avatarUrl
}];
case 'quote':
return [i18n.t('_notification.youGotQuote', { name: getUserName(data.user) }), {
body: getNoteSummary(data.note, i18n.locale),
body: data.note.text,
icon: data.user.avatarUrl
}];
case 'reaction':
return [`${data.reaction} ${getUserName(data.user)}`, {
body: getNoteSummary(data.note, i18n.locale),
body: data.note.text,
icon: data.user.avatarUrl
}];
case 'pollVote':
return [i18n.t('_notification.youGotPoll', { name: getUserName(data.user) }), {
body: getNoteSummary(data.note, i18n.locale),
body: data.note.text,
icon: data.user.avatarUrl
}];