From a0c63c8cc4b8035b5498abf438e4be2a01346ebf Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Sat, 16 Mar 2024 00:27:26 +0100 Subject: [PATCH 01/31] added notification dot and it seems to work well --- packages/frontend/src/ui/_common_/common.vue | 71 +++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 4fe53ae6a3..85d7b201ce 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, ref } from 'vue'; +import { defineAsyncComponent, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import { swInject } from './sw-inject.js'; import XNotification from './notification.vue'; @@ -70,6 +70,70 @@ const dev = _DEV_; const notifications = ref<Misskey.entities.Notification[]>([]); +class NotificationFavIconDot { + canvas : HTMLCanvasElement; + src : string | null = null; + ctx : CanvasRenderingContext2D | null = null; + favconImage : HTMLImageElement | null = null; + + constructor() { + this.canvas = document.createElement('canvas'); + + if (this.faviconEL == null) return; + + this.src = this.faviconEL.getAttribute('href'); + this.ctx = this.canvas.getContext('2d'); + + this.favconImage = document.createElement('img'); + this.favconImage.src = this.faviconEL.href; + + this.favconImage.onload = () => { + if (!this.favconImage) return; + + this.canvas.width = this.favconImage.width; + this.canvas.height = this.favconImage.height; + }; + } + + faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); + + _createFaviconElem() { + const newLink = document.createElement('link'); + newLink.rel = 'icon'; + newLink.href = '/favicon.ico'; + document.head.appendChild(newLink); + return newLink; + } + + _drawIcon() { + if (!this.ctx || !this.favconImage) return; + this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height); + } + + _drawDot() { + if (!this.ctx || !this.favconImage) return; + this.ctx.beginPath(); + this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI); + this.ctx.fillStyle = 'red'; + this.ctx.strokeStyle = this.ctx.fillStyle; + this.ctx.fill(); + this.ctx.stroke(); + } + + _drawFavicon() { + this.faviconEL.href = this.canvas.toDataURL('image/png'); + } + + setVisible(isVisible : boolean) { + this.ctx?.clearRect(0, 0, this.canvas.width, this.canvas.height); + this._drawIcon(); + if (isVisible) this._drawDot(); + this._drawFavicon(); + } +} + +const notificationDot = new NotificationFavIconDot(); + function onNotification(notification: Misskey.entities.Notification, isClient = false) { if (document.visibilityState === 'visible') { if (!isClient && notification.type !== 'test') { @@ -93,6 +157,11 @@ function onNotification(notification: Misskey.entities.Notification, isClient = if ($i) { const connection = useStream().useChannel('main', null, 'UI'); connection.on('notification', onNotification); + + watch(() => $i?.hasUnreadNotification, (hasAny) => { + notificationDot.setVisible(hasAny ?? false); + }); + globalEvents.on('clientNotification', notification => onNotification(notification, true)); //#region Listen message from SW From 395ea9ab9fd29ac72226e2deda9a0632d46df323 Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Sat, 16 Mar 2024 01:08:08 +0100 Subject: [PATCH 02/31] made notification dot appear if you load the page and there are already notifs --- packages/frontend/src/ui/_common_/common.vue | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 85d7b201ce..70097d89ab 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -75,6 +75,7 @@ class NotificationFavIconDot { src : string | null = null; ctx : CanvasRenderingContext2D | null = null; favconImage : HTMLImageElement | null = null; + loaded = false; constructor() { this.canvas = document.createElement('canvas'); @@ -92,6 +93,7 @@ class NotificationFavIconDot { this.canvas.width = this.favconImage.width; this.canvas.height = this.favconImage.height; + this.loaded = true; }; } @@ -120,11 +122,14 @@ class NotificationFavIconDot { this.ctx.stroke(); } - _drawFavicon() { + private _drawFavicon() { this.faviconEL.href = this.canvas.toDataURL('image/png'); } - setVisible(isVisible : boolean) { + async setVisible(isVisible : boolean) { + //Wait for it to have loaded the icon + const waiter = (done) => (this.loaded ? done() : setTimeout(() => waiter(done), 500)); + await new Promise(waiter); this.ctx?.clearRect(0, 0, this.canvas.width, this.canvas.height); this._drawIcon(); if (isVisible) this._drawDot(); @@ -158,9 +163,9 @@ if ($i) { const connection = useStream().useChannel('main', null, 'UI'); connection.on('notification', onNotification); - watch(() => $i?.hasUnreadNotification, (hasAny) => { - notificationDot.setVisible(hasAny ?? false); - }); + watch(() => $i?.hasUnreadNotification, (hasAny) => notificationDot.setVisible(hasAny ?? false)); + + if ($i.hasUnreadNotification) notificationDot.setVisible(true); globalEvents.on('clientNotification', notification => onNotification(notification, true)); From 8f300cf4601776e00ebe72b6f6fa5fb755dec6d3 Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Sat, 16 Mar 2024 01:23:02 +0100 Subject: [PATCH 03/31] added setting --- locales/en-US.yml | 1 + locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + packages/frontend/src/pages/settings/general.vue | 2 ++ .../src/pages/settings/preferences-backups.vue | 1 + packages/frontend/src/store.ts | 4 ++++ packages/frontend/src/ui/_common_/common.vue | 10 ++++------ 7 files changed, 17 insertions(+), 6 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 80e45c42d8..e22b92e2f6 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -688,6 +688,7 @@ channel: "Channels" create: "Create" notificationSetting: "Notification settings" notificationSettingDesc: "Select the types of notification to display." +enableFaviconNotificationDot: "Enable favicon notification dot" useGlobalSetting: "Use global settings" useGlobalSettingDesc: "If turned on, your account's notification settings will be used. If turned off, individual configurations can be made." other: "Other" diff --git a/locales/index.d.ts b/locales/index.d.ts index e407d2119b..422f1c8429 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2764,6 +2764,10 @@ export interface Locale extends ILocale { * 表示する通知の種別を選択してください。 */ "notificationSettingDesc": string; + /** + * ファビコン通知ドットを有効にする + */ + "enableFaviconNotificationDot": string; /** * グローバル設定を使う */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 57f52c64b2..e20907e6b7 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -687,6 +687,7 @@ channel: "チャンネル" create: "作成" notificationSetting: "通知設定" notificationSettingDesc: "表示する通知の種別を選択してください。" +enableFaviconNotificationDot: "ファビコン通知ドットを有効にする" useGlobalSetting: "グローバル設定を使う" useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使用されます。オフにすると、個別に設定できるようになります。" other: "その他" diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index 1e4e815d5d..b216622c33 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -180,6 +180,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <div class="_gaps_s"> + <MkSwitch v-model="enableFaviconNotificationDot">{{ i18n.ts.enableFaviconNotificationDot }}</MkSwitch> <MkSwitch v-model="warnMissingAltText">{{ i18n.ts.warnForMissingAltText }}</MkSwitch> <MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch> <MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch> @@ -337,6 +338,7 @@ const oneko = computed(defaultStore.makeGetterSetter('oneko')); const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages')); const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia')); const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab')); +const enableFaviconNotificationDot = computed(defaultStore.makeGetterSetter('enableFaviconNotificationDot')); const warnMissingAltText = computed(defaultStore.makeGetterSetter('warnMissingAltText')); const nsfw = computed(defaultStore.makeGetterSetter('nsfw')); const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm')); diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index f180e0b72c..86b8debe3a 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -72,6 +72,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'advancedMfm', 'loadRawImages', 'warnMissingAltText', + 'enableFaviconNotificationDot', 'imageNewTab', 'dataSaver', 'disableShowingAnimatedImages', diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 2cf17b27c5..7f6377613e 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -268,6 +268,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, + enableFaviconNotificationDot: { + where: 'device', + default: true, + }, imageNewTab: { where: 'device', default: false, diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 70097d89ab..77148835f8 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -80,8 +80,6 @@ class NotificationFavIconDot { constructor() { this.canvas = document.createElement('canvas'); - if (this.faviconEL == null) return; - this.src = this.faviconEL.getAttribute('href'); this.ctx = this.canvas.getContext('2d'); @@ -162,11 +160,11 @@ function onNotification(notification: Misskey.entities.Notification, isClient = if ($i) { const connection = useStream().useChannel('main', null, 'UI'); connection.on('notification', onNotification); - - watch(() => $i?.hasUnreadNotification, (hasAny) => notificationDot.setVisible(hasAny ?? false)); - - if ($i.hasUnreadNotification) notificationDot.setVisible(true); + watch(() => $i?.hasUnreadNotification, (hasAny) => notificationDot.setVisible((defaultStore.state.enableFaviconNotificationDot ? hasAny : false) ?? false)); + + if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) notificationDot.setVisible(true); + globalEvents.on('clientNotification', notification => onNotification(notification, true)); //#region Listen message from SW From ccf5659ac3548b671820c4fe45d6656aca1013f6 Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Sat, 16 Mar 2024 01:34:20 +0100 Subject: [PATCH 04/31] made methods private --- packages/frontend/src/ui/_common_/common.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 77148835f8..78fd2d2172 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -97,7 +97,7 @@ class NotificationFavIconDot { faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); - _createFaviconElem() { + private _createFaviconElem() { const newLink = document.createElement('link'); newLink.rel = 'icon'; newLink.href = '/favicon.ico'; @@ -105,12 +105,12 @@ class NotificationFavIconDot { return newLink; } - _drawIcon() { + private _drawIcon() { if (!this.ctx || !this.favconImage) return; this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height); } - _drawDot() { + private _drawDot() { if (!this.ctx || !this.favconImage) return; this.ctx.beginPath(); this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI); From 459e6841179979188b119be406e030e1afaf5516 Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Tue, 19 Mar 2024 22:00:28 +0100 Subject: [PATCH 05/31] fixed some of the issues with it --- packages/frontend/src/ui/_common_/common.vue | 37 ++++++++++---------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 78fd2d2172..a5e5d19a35 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -75,28 +75,29 @@ class NotificationFavIconDot { src : string | null = null; ctx : CanvasRenderingContext2D | null = null; favconImage : HTMLImageElement | null = null; - loaded = false; + faviconEL : HTMLLinkElement; + hasLoaded : Promise; constructor() { this.canvas = document.createElement('canvas'); + this.faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); this.src = this.faviconEL.getAttribute('href'); this.ctx = this.canvas.getContext('2d'); - + this.favconImage = document.createElement('img'); this.favconImage.src = this.faviconEL.href; + this.hasLoaded = new Promise((resolve, reject) => { + this.favconImage.onload = () => { + this.canvas.width = this.favconImage.width; + this.canvas.height = this.favconImage.height; - this.favconImage.onload = () => { - if (!this.favconImage) return; - - this.canvas.width = this.favconImage.width; - this.canvas.height = this.favconImage.height; - this.loaded = true; - }; + // resolve(); + setTimeout(() => resolve(), 500); + }; + }); } - faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); - private _createFaviconElem() { const newLink = document.createElement('link'); newLink.rel = 'icon'; @@ -107,6 +108,7 @@ class NotificationFavIconDot { private _drawIcon() { if (!this.ctx || !this.favconImage) return; + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height); } @@ -114,24 +116,23 @@ class NotificationFavIconDot { if (!this.ctx || !this.favconImage) return; this.ctx.beginPath(); this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI); - this.ctx.fillStyle = 'red'; - this.ctx.strokeStyle = this.ctx.fillStyle; + this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator'); + this.ctx.strokeStyle = 'white'; this.ctx.fill(); this.ctx.stroke(); } - private _drawFavicon() { + private _setFavicon() { this.faviconEL.href = this.canvas.toDataURL('image/png'); } async setVisible(isVisible : boolean) { //Wait for it to have loaded the icon - const waiter = (done) => (this.loaded ? done() : setTimeout(() => waiter(done), 500)); - await new Promise(waiter); - this.ctx?.clearRect(0, 0, this.canvas.width, this.canvas.height); + await this.hasLoaded; + console.log(this.hasLoaded); this._drawIcon(); if (isVisible) this._drawDot(); - this._drawFavicon(); + this._setFavicon(); } } From 6bc258a3e02f9492d361b146afac91055103d9db Mon Sep 17 00:00:00 2001 From: Marie <marie@kaifa.ch> Date: Fri, 29 Mar 2024 16:52:22 +0000 Subject: [PATCH 06/31] chore: automatically detect RTL on all MFM content --- .../frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index f8b5fcfedc..22a02d4f60 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -473,6 +473,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven }).flat(Infinity) as (VNode | string)[]; return h('span', { + dir: 'auto', // https://codeday.me/jp/qa/20190424/690106.html style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;', }, genEl(rootAst, props.rootScale ?? 1)); From 6bdb4a7ddcab93571911e5bcfbedef67d07bcd42 Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Fri, 29 Mar 2024 18:08:17 +0000 Subject: [PATCH 07/31] dir=auto on post form textarea, too --- packages/frontend/src/components/MkPostForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index d9e50fbb79..85b6073d9d 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only <input v-show="useCw" ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown"> <div :class="[$style.textOuter, { [$style.withCw]: useCw }]"> <div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div> - <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> + <textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text dir="auto" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/> <div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div> </div> <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> From aa11348d007d1d0a9fbf9a28234e444fde2f21ac Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Sat, 30 Mar 2024 10:54:25 +0000 Subject: [PATCH 08/31] `<bid>` all over the place --- .../global/MkMisskeyFlavoredMarkdown.ts | 49 +++++++++---------- packages/frontend/src/style.scss | 2 + 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index 22a02d4f60..df55d54269 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -335,67 +335,67 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'center': { - return [h('div', { + return [h('bdi',h('div', { style: 'text-align:center;', - }, genEl(token.children, scale))]; + }, genEl(token.children, scale)))]; } case 'url': { - return [h(MkUrl, { + return [h('bdi',h(MkUrl, { key: Math.random(), url: token.props.url, rel: 'nofollow noopener', - })]; + }))]; } case 'link': { - return [h(MkLink, { + return [h('bdi',h(MkLink, { key: Math.random(), url: token.props.url, rel: 'nofollow noopener', - }, genEl(token.children, scale, true))]; + }, genEl(token.children, scale, true)))]; } case 'mention': { - return [h(MkMention, { + return [h('bdi',h(MkMention, { key: Math.random(), host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host, username: token.props.username, - })]; + }))]; } case 'hashtag': { - return [h(MkA, { + return [h('bdi',h(MkA, { key: Math.random(), to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, style: 'color:var(--hashtag);', - }, `#${token.props.hashtag}`)]; + }, `#${token.props.hashtag}`))]; } case 'blockCode': { - return [h(MkCode, { + return [h('bdi',h(MkCode, { key: Math.random(), code: token.props.code, lang: token.props.lang ?? undefined, - })]; + }))]; } case 'inlineCode': { - return [h(MkCodeInline, { + return [h('bdi',h(MkCodeInline, { key: Math.random(), code: token.props.code, - })]; + }))]; } case 'quote': { if (!props.nowrap) { - return [h('div', { + return [h('bdi',h('div', { style: QUOTE_STYLE, - }, genEl(token.children, scale, true))]; + }, genEl(token.children, scale, true)))]; } else { - return [h('span', { + return [h('bdi',h('span', { style: QUOTE_STYLE, - }, genEl(token.children, scale, true))]; + }, genEl(token.children, scale, true)))]; } } @@ -439,17 +439,17 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'mathInline': { - return [h(MkFormula, { + return [h('bdi',h(MkFormula, { formula: token.props.formula, block: false, - })]; + }))]; } case 'mathBlock': { - return [h(MkFormula, { + return [h('bdi',h(MkFormula, { formula: token.props.formula, block: true, - })]; + }))]; } case 'search': { @@ -472,9 +472,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } }).flat(Infinity) as (VNode | string)[]; - return h('span', { - dir: 'auto', + return h('bdi', h('span', { // https://codeday.me/jp/qa/20190424/690106.html style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;', - }, genEl(rootAst, props.rootScale ?? 1)); + }, genEl(rootAst, props.rootScale ?? 1))); } diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index d876009961..8821f3cc7f 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -557,6 +557,8 @@ rt { // MFM ----------------------------- +div > bdi, p > bdi { display: block } + ._mfm_blur_ { filter: blur(6px); transition: filter 0.3s; From de80727cfc725a2f072c5d8929ad2b87c1cf49ae Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:30:14 +0200 Subject: [PATCH 09/31] Moved class to seperate file and fixed some ts warnings --- packages/frontend/src/scripts/favicon-dot.ts | 75 ++++++++++++++++++++ packages/frontend/src/ui/_common_/common.vue | 74 ++----------------- 2 files changed, 79 insertions(+), 70 deletions(-) create mode 100644 packages/frontend/src/scripts/favicon-dot.ts diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts new file mode 100644 index 0000000000..3a7887bca9 --- /dev/null +++ b/packages/frontend/src/scripts/favicon-dot.ts @@ -0,0 +1,75 @@ +class FavIconDot { + canvas : HTMLCanvasElement; + src : string | null = null; + ctx : CanvasRenderingContext2D | null = null; + favconImage : HTMLImageElement | null = null; + faviconEL : HTMLLinkElement; + hasLoaded : Promise<void>; + + constructor() { + this.canvas = document.createElement('canvas'); + this.faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); + + this.src = this.faviconEL.getAttribute('href'); + this.ctx = this.canvas.getContext('2d'); + + this.favconImage = document.createElement('img'); + this.hasLoaded = new Promise((resolve, _reject) => { + if (this.favconImage != null) { + this.favconImage.onload = () => { + this.canvas.width = (this.favconImage as HTMLImageElement).width; + this.canvas.height = (this.favconImage as HTMLImageElement).height; + // resolve(); + setTimeout(() => resolve(), 200); + }; + } + }); + this.favconImage.src = this.faviconEL.href; + } + + private _createFaviconElem() { + const newLink = document.createElement('link'); + newLink.rel = 'icon'; + newLink.href = '/favicon.ico'; + document.head.appendChild(newLink); + return newLink; + } + + private _drawIcon() { + if (!this.ctx || !this.favconImage) return; + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height); + } + + private _drawDot() { + if (!this.ctx || !this.favconImage) return; + this.ctx.beginPath(); + this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI); + this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator'); + this.ctx.strokeStyle = 'white'; + this.ctx.fill(); + this.ctx.stroke(); + } + + private _setFavicon() { + this.faviconEL.href = this.canvas.toDataURL('image/png'); + } + + async setVisible(isVisible : boolean) { + //Wait for it to have loaded the icon + await this.hasLoaded; + console.log(this.hasLoaded); + this._drawIcon(); + if (isVisible) this._drawDot(); + this._setFavicon(); + } +} + +let icon: FavIconDot = new FavIconDot(); + +export function setFavIconDot(visible: boolean) { + if (!icon) { + icon = new FavIconDot(); + } + icon.setVisible(visible); +} diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index a5e5d19a35..63b19dfb26 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -49,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { defineAsyncComponent, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; +import { setFavIconDot } from '../../scripts/favicon-dot'; import { swInject } from './sw-inject.js'; import XNotification from './notification.vue'; import { popups } from '@/os.js'; @@ -70,74 +71,6 @@ const dev = _DEV_; const notifications = ref<Misskey.entities.Notification[]>([]); -class NotificationFavIconDot { - canvas : HTMLCanvasElement; - src : string | null = null; - ctx : CanvasRenderingContext2D | null = null; - favconImage : HTMLImageElement | null = null; - faviconEL : HTMLLinkElement; - hasLoaded : Promise; - - constructor() { - this.canvas = document.createElement('canvas'); - this.faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); - - this.src = this.faviconEL.getAttribute('href'); - this.ctx = this.canvas.getContext('2d'); - - this.favconImage = document.createElement('img'); - this.favconImage.src = this.faviconEL.href; - this.hasLoaded = new Promise((resolve, reject) => { - this.favconImage.onload = () => { - this.canvas.width = this.favconImage.width; - this.canvas.height = this.favconImage.height; - - // resolve(); - setTimeout(() => resolve(), 500); - }; - }); - } - - private _createFaviconElem() { - const newLink = document.createElement('link'); - newLink.rel = 'icon'; - newLink.href = '/favicon.ico'; - document.head.appendChild(newLink); - return newLink; - } - - private _drawIcon() { - if (!this.ctx || !this.favconImage) return; - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height); - } - - private _drawDot() { - if (!this.ctx || !this.favconImage) return; - this.ctx.beginPath(); - this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI); - this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator'); - this.ctx.strokeStyle = 'white'; - this.ctx.fill(); - this.ctx.stroke(); - } - - private _setFavicon() { - this.faviconEL.href = this.canvas.toDataURL('image/png'); - } - - async setVisible(isVisible : boolean) { - //Wait for it to have loaded the icon - await this.hasLoaded; - console.log(this.hasLoaded); - this._drawIcon(); - if (isVisible) this._drawDot(); - this._setFavicon(); - } -} - -const notificationDot = new NotificationFavIconDot(); - function onNotification(notification: Misskey.entities.Notification, isClient = false) { if (document.visibilityState === 'visible') { if (!isClient && notification.type !== 'test') { @@ -162,9 +95,10 @@ if ($i) { const connection = useStream().useChannel('main', null, 'UI'); connection.on('notification', onNotification); - watch(() => $i?.hasUnreadNotification, (hasAny) => notificationDot.setVisible((defaultStore.state.enableFaviconNotificationDot ? hasAny : false) ?? false)); + //For the favicon notification dot + watch(() => $i?.hasUnreadNotification, (hasAny) => setFavIconDot((defaultStore.state.enableFaviconNotificationDot ? hasAny : false) ?? false)); - if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) notificationDot.setVisible(true); + if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) setFavIconDot(true); globalEvents.on('clientNotification', notification => onNotification(notification, true)); From 7730472c7789a2d67ca10d6b3f12e7c7301521fe Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:33:37 +0200 Subject: [PATCH 10/31] moved setting toggle under notifications category --- packages/frontend/src/pages/settings/general.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index b216622c33..c96d803d12 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -112,6 +112,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <MkSwitch v-model="useGroupedNotifications">{{ i18n.ts.useGroupedNotifications }}</MkSwitch> + <MkSwitch v-model="enableFaviconNotificationDot">{{ i18n.ts.enableFaviconNotificationDot }}</MkSwitch> + <MkRadios v-model="notificationPosition"> <template #label>{{ i18n.ts.position }}</template> <option value="leftTop"><i class="ph-arrow-up-left ph-bold ph-lg"></i> {{ i18n.ts.leftTop }}</option> @@ -180,7 +182,6 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps_m"> <div class="_gaps_s"> - <MkSwitch v-model="enableFaviconNotificationDot">{{ i18n.ts.enableFaviconNotificationDot }}</MkSwitch> <MkSwitch v-model="warnMissingAltText">{{ i18n.ts.warnForMissingAltText }}</MkSwitch> <MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch> <MkSwitch v-model="useReactionPickerForContextMenu">{{ i18n.ts.useReactionPickerForContextMenu }}</MkSwitch> From 590f7abefdadc25f540d40c6962d193d2a47368e Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Thu, 2 May 2024 17:22:30 +0200 Subject: [PATCH 11/31] removed use of settimeout --- packages/frontend/src/scripts/favicon-dot.ts | 81 ++++++++++++++------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts index 3a7887bca9..d54991f067 100644 --- a/packages/frontend/src/scripts/favicon-dot.ts +++ b/packages/frontend/src/scripts/favicon-dot.ts @@ -2,29 +2,52 @@ class FavIconDot { canvas : HTMLCanvasElement; src : string | null = null; ctx : CanvasRenderingContext2D | null = null; - favconImage : HTMLImageElement | null = null; - faviconEL : HTMLLinkElement; - hasLoaded : Promise<void>; + faviconImage : HTMLImageElement | null = null; + faviconEL : HTMLLinkElement | undefined; + hasLoaded : Promise<void> | undefined; constructor() { this.canvas = document.createElement('canvas'); - this.faviconEL = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); + } + //MUST BE CALLED BEFORE CALLING ANY OTHER FUNCTIONS + public async setup() { + const element : HTMLLinkElement = await this.getOrMakeFaviconElement(); + + this.faviconEL = element; this.src = this.faviconEL.getAttribute('href'); this.ctx = this.canvas.getContext('2d'); - - this.favconImage = document.createElement('img'); - this.hasLoaded = new Promise((resolve, _reject) => { - if (this.favconImage != null) { - this.favconImage.onload = () => { - this.canvas.width = (this.favconImage as HTMLImageElement).width; - this.canvas.height = (this.favconImage as HTMLImageElement).height; - // resolve(); - setTimeout(() => resolve(), 200); + + this.faviconImage = document.createElement('img'); + this.faviconImage.src = this.faviconEL.href; + + this.hasLoaded = new Promise((resolve, reject) => { + (this.faviconImage as HTMLImageElement).onload = () => { + this.canvas.width = (this.faviconImage as HTMLImageElement).width; + this.canvas.height = (this.faviconImage as HTMLImageElement).height; + resolve(); + }; + + (this.faviconImage as HTMLImageElement).onerror = () => { + reject('Failed to create favicon img element'); + }; + }); + } + + private async getOrMakeFaviconElement() : Promise<HTMLLinkElement> { + return new Promise((resolve, reject) => { + const favicon = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); + if (favicon === document.querySelector<HTMLLinkElement>('link[rel$=icon]')) { + favicon.onload = () => { + resolve(favicon); + }; + + favicon.onerror = () => { + reject('Failed to load favicon'); }; } + resolve(favicon); }); - this.favconImage.src = this.faviconEL.href; } private _createFaviconElem() { @@ -36,15 +59,15 @@ class FavIconDot { } private _drawIcon() { - if (!this.ctx || !this.favconImage) return; + if (!this.ctx || !this.faviconImage) return; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.ctx.drawImage(this.favconImage, 0, 0, this.favconImage.width, this.favconImage.height); + this.ctx.drawImage(this.faviconImage, 0, 0, this.faviconImage.width, this.faviconImage.height); } private _drawDot() { - if (!this.ctx || !this.favconImage) return; + if (!this.ctx || !this.faviconImage) return; this.ctx.beginPath(); - this.ctx.arc(this.favconImage.width - 10, 10, 10, 0, 2 * Math.PI); + this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI); this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator'); this.ctx.strokeStyle = 'white'; this.ctx.fill(); @@ -52,7 +75,7 @@ class FavIconDot { } private _setFavicon() { - this.faviconEL.href = this.canvas.toDataURL('image/png'); + if (this.faviconEL) this.faviconEL.href = this.canvas.toDataURL('image/png'); } async setVisible(isVisible : boolean) { @@ -65,11 +88,23 @@ class FavIconDot { } } -let icon: FavIconDot = new FavIconDot(); +let icon: FavIconDot | undefined = undefined; export function setFavIconDot(visible: boolean) { - if (!icon) { - icon = new FavIconDot(); + const setIconVisibility = async () => { + if (!icon) { + icon = new FavIconDot(); + await icon.setup(); + } + + (icon as FavIconDot).setVisible(visible); + }; + + // If document is already loaded, set visibility immediately + if (document.readyState === 'complete') { + setIconVisibility(); + } else { + // Otherwise, set visibility when window loads + window.onload = setIconVisibility; } - icon.setVisible(visible); } From 42c29697070f40c994e8661f8b88ea652b46f162 Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Thu, 2 May 2024 17:38:16 +0200 Subject: [PATCH 12/31] fixes --- packages/frontend/src/scripts/favicon-dot.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts index d54991f067..2e05171772 100644 --- a/packages/frontend/src/scripts/favicon-dot.ts +++ b/packages/frontend/src/scripts/favicon-dot.ts @@ -19,7 +19,6 @@ class FavIconDot { this.ctx = this.canvas.getContext('2d'); this.faviconImage = document.createElement('img'); - this.faviconImage.src = this.faviconEL.href; this.hasLoaded = new Promise((resolve, reject) => { (this.faviconImage as HTMLImageElement).onload = () => { @@ -32,20 +31,20 @@ class FavIconDot { reject('Failed to create favicon img element'); }; }); + + this.faviconImage.src = this.faviconEL.href; } private async getOrMakeFaviconElement() : Promise<HTMLLinkElement> { return new Promise((resolve, reject) => { const favicon = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); - if (favicon === document.querySelector<HTMLLinkElement>('link[rel$=icon]')) { - favicon.onload = () => { - resolve(favicon); - }; + favicon.onload = () => { + resolve(favicon); + }; - favicon.onerror = () => { - reject('Failed to load favicon'); - }; - } + favicon.onerror = () => { + reject('Failed to load favicon'); + }; resolve(favicon); }); } From 47d1477ac493ac88d5b97b55de39e86251ab1735 Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Fri, 3 May 2024 11:27:41 +0200 Subject: [PATCH 13/31] did thread fixes --- packages/frontend/src/scripts/favicon-dot.ts | 16 +++++++--------- packages/frontend/src/ui/_common_/common.vue | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts index 2e05171772..c40a08f427 100644 --- a/packages/frontend/src/scripts/favicon-dot.ts +++ b/packages/frontend/src/scripts/favicon-dot.ts @@ -21,15 +21,14 @@ class FavIconDot { this.faviconImage = document.createElement('img'); this.hasLoaded = new Promise((resolve, reject) => { - (this.faviconImage as HTMLImageElement).onload = () => { + (this.faviconImage as HTMLImageElement).addEventListener('load', () => { this.canvas.width = (this.faviconImage as HTMLImageElement).width; this.canvas.height = (this.faviconImage as HTMLImageElement).height; resolve(); - }; - - (this.faviconImage as HTMLImageElement).onerror = () => { + }); + (this.faviconImage as HTMLImageElement).addEventListener('error', () => { reject('Failed to create favicon img element'); - }; + }); }); this.faviconImage.src = this.faviconEL.href; @@ -38,9 +37,9 @@ class FavIconDot { private async getOrMakeFaviconElement() : Promise<HTMLLinkElement> { return new Promise((resolve, reject) => { const favicon = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); - favicon.onload = () => { + favicon.addEventListener('load', () => { resolve(favicon); - }; + }); favicon.onerror = () => { reject('Failed to load favicon'); @@ -80,7 +79,6 @@ class FavIconDot { async setVisible(isVisible : boolean) { //Wait for it to have loaded the icon await this.hasLoaded; - console.log(this.hasLoaded); this._drawIcon(); if (isVisible) this._drawDot(); this._setFavicon(); @@ -104,6 +102,6 @@ export function setFavIconDot(visible: boolean) { setIconVisibility(); } else { // Otherwise, set visibility when window loads - window.onload = setIconVisibility; + window.addEventListener('load', setIconVisibility); } } diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 63b19dfb26..bec8bb6c7c 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -96,7 +96,7 @@ if ($i) { connection.on('notification', onNotification); //For the favicon notification dot - watch(() => $i?.hasUnreadNotification, (hasAny) => setFavIconDot((defaultStore.state.enableFaviconNotificationDot ? hasAny : false) ?? false)); + watch(() => $i?.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot, (hasAny) => setFavIconDot(hasAny as boolean)); if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) setFavIconDot(true); From 975b6b3dd0f34731ad442480fc39e9a741ad76dc Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Fri, 3 May 2024 12:15:47 +0200 Subject: [PATCH 14/31] fixed querySelector that would grab favicon --- packages/frontend/src/scripts/favicon-dot.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts index c40a08f427..0229092ca7 100644 --- a/packages/frontend/src/scripts/favicon-dot.ts +++ b/packages/frontend/src/scripts/favicon-dot.ts @@ -36,7 +36,7 @@ class FavIconDot { private async getOrMakeFaviconElement() : Promise<HTMLLinkElement> { return new Promise((resolve, reject) => { - const favicon = document.querySelector<HTMLLinkElement>('link[rel$=icon]') ?? this._createFaviconElem(); + const favicon = (document.querySelector('link[rel=icon]') ?? this._createFaviconElem()) as HTMLLinkElement; favicon.addEventListener('load', () => { resolve(favicon); }); From 0117f1896c92843b7cb7525a454ba9d3d6c8cf03 Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Fri, 3 May 2024 12:20:58 +0200 Subject: [PATCH 15/31] potential firefox fix --- packages/frontend/src/scripts/favicon-dot.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts index 0229092ca7..143ad50c9a 100644 --- a/packages/frontend/src/scripts/favicon-dot.ts +++ b/packages/frontend/src/scripts/favicon-dot.ts @@ -50,8 +50,10 @@ class FavIconDot { private _createFaviconElem() { const newLink = document.createElement('link'); - newLink.rel = 'icon'; - newLink.href = '/favicon.ico'; + newLink.setAttribute('rel', 'icon'); + newLink.setAttribute('href', '/favicon.ico'); + newLink.setAttribute('type', 'image/x-icon'); + document.head.appendChild(newLink); return newLink; } From 342eda431f6e7b19742c909fced38fdef772ccea Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Mon, 6 May 2024 13:54:43 +0200 Subject: [PATCH 16/31] fixing a buch of comments --- packages/frontend/src/scripts/favicon-dot.ts | 40 ++++++++++---------- packages/frontend/src/ui/_common_/common.vue | 2 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts index 143ad50c9a..643ee1b76b 100644 --- a/packages/frontend/src/scripts/favicon-dot.ts +++ b/packages/frontend/src/scripts/favicon-dot.ts @@ -1,18 +1,20 @@ class FavIconDot { - canvas : HTMLCanvasElement; - src : string | null = null; - ctx : CanvasRenderingContext2D | null = null; - faviconImage : HTMLImageElement | null = null; - faviconEL : HTMLLinkElement | undefined; - hasLoaded : Promise<void> | undefined; + canvas: HTMLCanvasElement; + src: string | null = null; + ctx: CanvasRenderingContext2D | null = null; + faviconImage: HTMLImageElement | null = null; + faviconEL: HTMLLinkElement | undefined; + hasLoaded: Promise<void> | undefined; constructor() { this.canvas = document.createElement('canvas'); } - //MUST BE CALLED BEFORE CALLING ANY OTHER FUNCTIONS + /** + * Must be called before calling any other functions + */ public async setup() { - const element : HTMLLinkElement = await this.getOrMakeFaviconElement(); + const element: HTMLLinkElement = await this.getOrMakeFaviconElement(); this.faviconEL = element; this.src = this.faviconEL.getAttribute('href'); @@ -34,9 +36,9 @@ class FavIconDot { this.faviconImage.src = this.faviconEL.href; } - private async getOrMakeFaviconElement() : Promise<HTMLLinkElement> { + private async getOrMakeFaviconElement(): Promise<HTMLLinkElement> { return new Promise((resolve, reject) => { - const favicon = (document.querySelector('link[rel=icon]') ?? this._createFaviconElem()) as HTMLLinkElement; + const favicon = (document.querySelector('link[rel=icon]') ?? this.createFaviconElem()) as HTMLLinkElement; favicon.addEventListener('load', () => { resolve(favicon); }); @@ -48,7 +50,7 @@ class FavIconDot { }); } - private _createFaviconElem() { + private createFaviconElem() { const newLink = document.createElement('link'); newLink.setAttribute('rel', 'icon'); newLink.setAttribute('href', '/favicon.ico'); @@ -58,13 +60,13 @@ class FavIconDot { return newLink; } - private _drawIcon() { + private drawIcon() { if (!this.ctx || !this.faviconImage) return; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(this.faviconImage, 0, 0, this.faviconImage.width, this.faviconImage.height); } - private _drawDot() { + private drawDot() { if (!this.ctx || !this.faviconImage) return; this.ctx.beginPath(); this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI); @@ -74,16 +76,16 @@ class FavIconDot { this.ctx.stroke(); } - private _setFavicon() { + private setFavicon() { if (this.faviconEL) this.faviconEL.href = this.canvas.toDataURL('image/png'); } - async setVisible(isVisible : boolean) { - //Wait for it to have loaded the icon + async setVisible(isVisible: boolean) { + // Wait for it to have loaded the icon await this.hasLoaded; - this._drawIcon(); - if (isVisible) this._drawDot(); - this._setFavicon(); + this.drawIcon(); + if (isVisible) this.drawDot(); + this.setFavicon(); } } diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index bec8bb6c7c..b1fe8e54fc 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -95,7 +95,7 @@ if ($i) { const connection = useStream().useChannel('main', null, 'UI'); connection.on('notification', onNotification); - //For the favicon notification dot + // For the favicon notification dot watch(() => $i?.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot, (hasAny) => setFavIconDot(hasAny as boolean)); if ($i.hasUnreadNotification && defaultStore.state.enableFaviconNotificationDot) setFavIconDot(true); From a058c855fc27c17d84dfb1655191cabdcf844c7a Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+KevinWh0@users.noreply.github.com> Date: Mon, 6 May 2024 13:59:42 +0200 Subject: [PATCH 17/31] changed grabbing theme color for dot to match the other things in this project --- packages/frontend/src/scripts/favicon-dot.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/scripts/favicon-dot.ts b/packages/frontend/src/scripts/favicon-dot.ts index 643ee1b76b..e338f55f72 100644 --- a/packages/frontend/src/scripts/favicon-dot.ts +++ b/packages/frontend/src/scripts/favicon-dot.ts @@ -1,3 +1,5 @@ +import tinycolor from 'tinycolor2'; + class FavIconDot { canvas: HTMLCanvasElement; src: string | null = null; @@ -70,7 +72,8 @@ class FavIconDot { if (!this.ctx || !this.faviconImage) return; this.ctx.beginPath(); this.ctx.arc(this.faviconImage.width - 10, 10, 10, 0, 2 * Math.PI); - this.ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--navIndicator'); + const computedStyle = getComputedStyle(document.documentElement); + this.ctx.fillStyle = tinycolor(computedStyle.getPropertyValue('--navIndicator')).toHexString(); this.ctx.strokeStyle = 'white'; this.ctx.fill(); this.ctx.stroke(); From 3f73251df5967ad47f61f1a6541f9d5ff6563f72 Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Sun, 7 Apr 2024 17:49:20 +0100 Subject: [PATCH 18/31] allow computed `offsetMode` in `MkPagination` - #490 --- packages/frontend/src/components/MkPagination.vue | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 6f6007d432..9a324849e2 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -73,7 +73,7 @@ export type Paging<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints> */ reversed?: boolean; - offsetMode?: boolean; + offsetMode?: boolean | ComputedRef<boolean>; pageEl?: HTMLElement; }; @@ -240,10 +240,11 @@ const fetchMore = async (): Promise<void> => { if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; moreFetching.value = true; const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; + const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false; await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { ...params, limit: SECOND_FETCH_LIMIT, - ...(props.pagination.offsetMode ? { + ...(offsetMode ? { offset: offset.value, } : { untilId: Array.from(items.value.keys()).at(-1), @@ -304,10 +305,11 @@ const fetchMoreAhead = async (): Promise<void> => { if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; moreFetching.value = true; const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; + const offsetMode = props.offsetMode ? isRef(props.offsetMode) ? props.offsetMode.value : props.offsetMode : false; await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, { ...params, limit: SECOND_FETCH_LIMIT, - ...(props.pagination.offsetMode ? { + ...(offsetMode ? { offset: offset.value, } : { sinceId: Array.from(items.value.keys()).at(-1), From 9d91196344ce359fd27dabd778d4d084d1fc6c5e Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Sun, 7 Apr 2024 17:49:53 +0100 Subject: [PATCH 19/31] allow `offset` in `admin/emoji/list` - #490 also, use `skip` + `take` instead of `limit` (the TypeORM docs say so https://github.com/typeorm/typeorm/blob/master/docs/select-query-builder.md#adding-limit-expression ) --- .../backend/src/server/api/endpoints/admin/emoji/list.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index 5e21111f9f..f35a6667f4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -66,6 +66,7 @@ export const paramDef = { properties: { query: { type: 'string', nullable: true, default: null }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + offset: { type: 'integer', minimum: 1, nullable: true, default: null }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, }, @@ -91,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); //const emojis = await q.limit(ps.limit).getMany(); - emojis = await q.orderBy('length(emoji.name)', 'ASC').getMany(); + emojis = await q.orderBy('length(emoji.name)', 'ASC').addOrderBy('id', 'DESC').getMany(); const queryarry = ps.query.match(/:([\p{Letter}\p{Number}\p{Mark}_+-]*):/ug); if (queryarry) { @@ -105,9 +106,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- emoji.aliases.some(a => a.includes(queryNfc)) || emoji.category?.includes(queryNfc)); } - emojis.splice(ps.limit + 1); + emojis = emojis.slice((ps.offset ?? 0), ((ps.offset ?? 0) + ps.limit)); } else { - emojis = await q.limit(ps.limit).getMany(); + emojis = await q.take(ps.limit).skip(ps.offset ?? 0).getMany(); } return this.emojiEntityService.packDetailedMany(emojis); From a676b0ee6178ad45370864195afa09c87d865265 Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Sun, 7 Apr 2024 17:50:53 +0100 Subject: [PATCH 20/31] paginate in offset mode when querying emoji - fixes #490 since the backend sorts emojis by name length when a query is present, the normal pagination with `sinceId` / `untilId` would not work reliably `offsetMode` is better in this case, although it will produce non-stable results if custom emojis that match the query are added or removed while we paginate --- packages/frontend/src/pages/custom-emojis-manager.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index 1f9a99d4f5..9357735c82 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -98,6 +98,9 @@ const selectedEmojis = ref<string[]>([]); const pagination = { endpoint: 'admin/emoji/list' as const, limit: 30, + offsetMode: computed(() => ( + (query.value && query.value !== '') ? true : false + )), params: computed(() => ({ query: (query.value && query.value !== '') ? query.value : null, })), From 42d9da161b56d38a04fb4f25c7d063bdea880ff0 Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Tue, 14 May 2024 16:58:06 +0100 Subject: [PATCH 21/31] first basic protection - #524 --- packages/backend/src/core/NoteCreateService.ts | 8 ++++++++ packages/backend/src/core/NoteEditService.ts | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 631d7074bd..d51315f71f 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -627,6 +627,14 @@ export class NoteCreateService implements OnApplicationShutdown { userHost: user.host, }); + // should really not happen, but better safe than sorry + if (data.reply?.id === insert.id) { + throw new Error("A note can't reply to itself"); + } + if (data.renote?.id === insert.id) { + throw new Error("A note can't renote itself"); + } + if (data.uri != null) insert.uri = data.uri; if (data.url != null) insert.url = data.url; diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 72fc01ae3b..435f5f017a 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -299,6 +299,10 @@ export class NoteEditService implements OnApplicationShutdown { } if (data.renote) { + if (data.renote.id === oldnote.id) { + throw new Error("A note can't renote itself"); + } + switch (data.renote.visibility) { case 'public': // public noteは無条件にrenote可能 From 2d89b08a0840c6554f4a8af47fbd1f8ed5ea76d3 Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Wed, 15 May 2024 16:47:06 +0100 Subject: [PATCH 22/31] use the current resolver for quotes - #524 this might solve the loop problem, if the protection already in place for replies was enough --- packages/backend/src/core/activitypub/models/ApNoteService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 6d9dc86c16..ad7b3b145c 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -251,7 +251,7 @@ export class ApNoteService { > => { if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' }; try { - const res = await this.resolveNote(uri); + const res = await this.resolveNote(uri, { resolver }); if (res == null) return { status: 'permerror' }; return { status: 'ok', res }; } catch (e) { @@ -478,7 +478,7 @@ export class ApNoteService { > => { if (!/^https?:/.test(uri)) return { status: 'permerror' }; try { - const res = await this.resolveNote(uri); + const res = await this.resolveNote(uri, { resolver }); if (res == null) return { status: 'permerror' }; return { status: 'ok', res }; } catch (e) { From 189c26aa25fa995a57e682c4bb64d83fa0c1d28a Mon Sep 17 00:00:00 2001 From: Sugar <sugar@sylveon.social> Date: Thu, 30 May 2024 16:01:29 +0200 Subject: [PATCH 23/31] escape \ character in sqlLikeEscape --- packages/backend/src/misc/sql-like-escape.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts index 0c05255674..ffe61670ee 100644 --- a/packages/backend/src/misc/sql-like-escape.ts +++ b/packages/backend/src/misc/sql-like-escape.ts @@ -4,5 +4,5 @@ */ export function sqlLikeEscape(s: string) { - return s.replace(/([%_])/g, '\\$1'); + return s.replace(/([%_\\])/g, '\\$1'); } From 145c4ba132e765a07c1800282e7d7adb6921afb6 Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+ChaoticLeah@users.noreply.github.com> Date: Fri, 31 May 2024 12:19:18 +0200 Subject: [PATCH 24/31] fixed the search url --- packages/backend/src/core/MfmService.ts | 284 ++++++++++++------------ 1 file changed, 142 insertions(+), 142 deletions(-) diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index b7c9064cef..c1dd27fe99 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -480,174 +480,174 @@ export class MfmService { const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any; } = { - async bold(node) { - const el = doc.createElement('span'); - el.textContent = '**'; - await appendChildren(node.children, el); - el.textContent += '**'; - return el; - }, + async bold(node) { + const el = doc.createElement('span'); + el.textContent = '**'; + await appendChildren(node.children, el); + el.textContent += '**'; + return el; + }, - async small(node) { - const el = doc.createElement('small'); - await appendChildren(node.children, el); - return el; - }, + async small(node) { + const el = doc.createElement('small'); + await appendChildren(node.children, el); + return el; + }, - async strike(node) { - const el = doc.createElement('span'); - el.textContent = '~~'; - await appendChildren(node.children, el); - el.textContent += '~~'; - return el; - }, + async strike(node) { + const el = doc.createElement('span'); + el.textContent = '~~'; + await appendChildren(node.children, el); + el.textContent += '~~'; + return el; + }, - async italic(node) { - const el = doc.createElement('span'); - el.textContent = '*'; - await appendChildren(node.children, el); - el.textContent += '*'; - return el; - }, + async italic(node) { + const el = doc.createElement('span'); + el.textContent = '*'; + await appendChildren(node.children, el); + el.textContent += '*'; + return el; + }, - async fn(node) { - const el = doc.createElement('span'); - el.textContent = '*'; - await appendChildren(node.children, el); - el.textContent += '*'; - return el; - }, + async fn(node) { + const el = doc.createElement('span'); + el.textContent = '*'; + await appendChildren(node.children, el); + el.textContent += '*'; + return el; + }, - blockCode(node) { - const pre = doc.createElement('pre'); - const inner = doc.createElement('code'); + blockCode(node) { + const pre = doc.createElement('pre'); + const inner = doc.createElement('code'); - const nodes = node.props.code - .split(/\r\n|\r|\n/) - .map((x) => doc.createTextNode(x)); + const nodes = node.props.code + .split(/\r\n|\r|\n/) + .map((x) => doc.createTextNode(x)); - for (const x of intersperse<FIXME | 'br'>('br', nodes)) { - inner.appendChild(x === 'br' ? doc.createElement('br') : x); - } + for (const x of intersperse<FIXME | 'br'>('br', nodes)) { + inner.appendChild(x === 'br' ? doc.createElement('br') : x); + } - pre.appendChild(inner); - return pre; - }, + pre.appendChild(inner); + return pre; + }, - async center(node) { - const el = doc.createElement('div'); - await appendChildren(node.children, el); - return el; - }, + async center(node) { + const el = doc.createElement('div'); + await appendChildren(node.children, el); + return el; + }, - emojiCode(node) { - return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); - }, + emojiCode(node) { + return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); + }, - unicodeEmoji(node) { - return doc.createTextNode(node.props.emoji); - }, + unicodeEmoji(node) { + return doc.createTextNode(node.props.emoji); + }, - hashtag: (node) => { - const a = doc.createElement('a'); - a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`); - a.textContent = `#${node.props.hashtag}`; - a.setAttribute('rel', 'tag'); - a.setAttribute('class', 'hashtag'); - return a; - }, + hashtag: (node) => { + const a = doc.createElement('a'); + a.setAttribute('href', `${this.config.url}/tags/${node.props.hashtag}`); + a.textContent = `#${node.props.hashtag}`; + a.setAttribute('rel', 'tag'); + a.setAttribute('class', 'hashtag'); + return a; + }, - inlineCode(node) { - const el = doc.createElement('code'); - el.textContent = node.props.code; - return el; - }, + inlineCode(node) { + const el = doc.createElement('code'); + el.textContent = node.props.code; + return el; + }, - mathInline(node) { - const el = doc.createElement('code'); - el.textContent = node.props.formula; - return el; - }, + mathInline(node) { + const el = doc.createElement('code'); + el.textContent = node.props.formula; + return el; + }, - mathBlock(node) { - const el = doc.createElement('code'); - el.textContent = node.props.formula; - return el; - }, + mathBlock(node) { + const el = doc.createElement('code'); + el.textContent = node.props.formula; + return el; + }, - async link(node) { - const a = doc.createElement('a'); - a.setAttribute('rel', 'nofollow noopener noreferrer'); - a.setAttribute('target', '_blank'); - a.setAttribute('href', node.props.url); - await appendChildren(node.children, a); - return a; - }, + async link(node) { + const a = doc.createElement('a'); + a.setAttribute('rel', 'nofollow noopener noreferrer'); + a.setAttribute('target', '_blank'); + a.setAttribute('href', node.props.url); + await appendChildren(node.children, a); + return a; + }, - async mention(node) { - const { username, host, acct } = node.props; - const resolved = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); + async mention(node) { + const { username, host, acct } = node.props; + const resolved = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); - const el = doc.createElement('span'); - if (!resolved) { - el.textContent = acct; - } else { - el.setAttribute('class', 'h-card'); - el.setAttribute('translate', 'no'); - const a = doc.createElement('a'); - a.setAttribute('href', resolved.url ? resolved.url : resolved.uri); - a.className = 'u-url mention'; - const span = doc.createElement('span'); - span.textContent = resolved.username || username; - a.textContent = '@'; - a.appendChild(span); - el.appendChild(a); - } + const el = doc.createElement('span'); + if (!resolved) { + el.textContent = acct; + } else { + el.setAttribute('class', 'h-card'); + el.setAttribute('translate', 'no'); + const a = doc.createElement('a'); + a.setAttribute('href', resolved.url ? resolved.url : resolved.uri); + a.className = 'u-url mention'; + const span = doc.createElement('span'); + span.textContent = resolved.username || username; + a.textContent = '@'; + a.appendChild(span); + el.appendChild(a); + } - return el; - }, + return el; + }, - async quote(node) { - const el = doc.createElement('blockquote'); - await appendChildren(node.children, el); - return el; - }, + async quote(node) { + const el = doc.createElement('blockquote'); + await appendChildren(node.children, el); + return el; + }, - text(node) { - const el = doc.createElement('span'); - const nodes = node.props.text - .split(/\r\n|\r|\n/) - .map((x) => doc.createTextNode(x)); + text(node) { + const el = doc.createElement('span'); + const nodes = node.props.text + .split(/\r\n|\r|\n/) + .map((x) => doc.createTextNode(x)); - for (const x of intersperse<FIXME | 'br'>('br', nodes)) { - el.appendChild(x === 'br' ? doc.createElement('br') : x); - } + for (const x of intersperse<FIXME | 'br'>('br', nodes)) { + el.appendChild(x === 'br' ? doc.createElement('br') : x); + } - return el; - }, + return el; + }, - url(node) { - const a = doc.createElement('a'); - a.setAttribute('rel', 'nofollow noopener noreferrer'); - a.setAttribute('target', '_blank'); - a.setAttribute('href', node.props.url); - a.textContent = node.props.url.replace(/^https?:\/\//, ''); - return a; - }, + url(node) { + const a = doc.createElement('a'); + a.setAttribute('rel', 'nofollow noopener noreferrer'); + a.setAttribute('target', '_blank'); + a.setAttribute('href', node.props.url); + a.textContent = node.props.url.replace(/^https?:\/\//, ''); + return a; + }, - search: (node) => { - const a = doc.createElement('a'); - a.setAttribute('href', `https"google.com/${node.props.query}`); - a.textContent = node.props.content; - return a; - }, + search: (node) => { + const a = doc.createElement('a'); + a.setAttribute('href', `https://www.google.com/search?q=${node.props.query}`); + a.textContent = node.props.content; + return a; + }, - async plain(node) { - const el = doc.createElement('span'); - await appendChildren(node.children, el); - return el; - }, - }; + async plain(node) { + const el = doc.createElement('span'); + await appendChildren(node.children, el); + return el; + }, + }; await appendChildren(nodes, doc.body); From 1656c02536108e9566ace58809ddc86124b1bfdf Mon Sep 17 00:00:00 2001 From: KevinWh0 <45321184+ChaoticLeah@users.noreply.github.com> Date: Fri, 31 May 2024 12:21:25 +0200 Subject: [PATCH 25/31] renamed toMastoHtml to toMastoApiHtml to clear up what it does --- packages/backend/src/core/MfmService.ts | 4 ++-- packages/backend/src/server/api/mastodon/converters.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index c1dd27fe99..819c940dc1 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -459,10 +459,10 @@ export class MfmService { return `<p>${doc.body.innerHTML}</p>`; } - // the toMastoHtml function was taken from Iceshrimp and written by zotan and modified by marie to work with the current MK version + // the toMastoApiHtml function was taken from Iceshrimp and written by zotan and modified by marie to work with the current MK version @bindThis - public async toMastoHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], inline = false, quoteUri: string | null = null) { + public async toMastoApiHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], inline = false, quoteUri: string | null = null) { if (nodes == null) { return null; } diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index 326d3a1d5c..6b2c991ff2 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -110,7 +110,7 @@ export class MastoConverters { private async encodeField(f: Entity.Field): Promise<Entity.Field> { return { name: f.name, - value: await this.mfmService.toMastoHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value), + value: await this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value), verified_at: null, }; } @@ -179,7 +179,7 @@ export class MastoConverters { const files = this.driveFileEntityService.packManyByIds(edit.fileIds); const item = { account: noteUser, - content: this.mfmService.toMastoHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''), + content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''), created_at: lastDate.toISOString(), emojis: [], sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false), @@ -240,7 +240,7 @@ export class MastoConverters { }); const content = note.text !== null - ? quoteUri.then(quoteUri => this.mfmService.toMastoHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri)) + ? quoteUri.then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri)) .then(p => p ?? escapeMFM(note.text!)) : ''; From 81ed73938019848e4f44ebaedcfc62af7ab88f67 Mon Sep 17 00:00:00 2001 From: Marie <marie@kaifa.ch> Date: Fri, 31 May 2024 21:18:35 +0000 Subject: [PATCH 26/31] fix: button saying Misskey instead of Sharkey --- packages/frontend/src/pages/about-sharkey.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/about-sharkey.vue b/packages/frontend/src/pages/about-sharkey.vue index 1bfd90c8f7..f020e043c6 100644 --- a/packages/frontend/src/pages/about-sharkey.vue +++ b/packages/frontend/src/pages/about-sharkey.vue @@ -215,7 +215,7 @@ function gravity() { function iLoveMisskey() { os.post({ - initialText: 'I $[jelly ❤] #Misskey', + initialText: 'I $[jelly ❤] #Sharkey', instant: true, }); } From 92cd771e0fb47eadbd37a337004be73e9ca2b3d0 Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Mon, 3 Jun 2024 16:28:25 +0000 Subject: [PATCH 27/31] avoid `await` at top-level in setup - fixes frontend tests --- packages/frontend/src/index.html | 2 +- packages/frontend/src/pages/user/home.vue | 30 ++++++++++++----------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html index 54059bfaf4..40e3cdf2ab 100644 --- a/packages/frontend/src/index.html +++ b/packages/frontend/src/index.html @@ -22,7 +22,7 @@ style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: www.google.com xn--931a.moe launcher.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 activitypub.software secure.gravatar.com avatars.githubusercontent.com; media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000; - connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com; + connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com https://api.listenbrainz.org; frame-src *;" /> <meta property="og:site_name" content="[DEV BUILD] Misskey" /> diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 96ae4824f0..4a00b204c0 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -236,22 +236,24 @@ const moderationNote = ref(props.user.moderationNote); const editModerationNote = ref(false); const noteview = ref<string | null>(null); -let listenbrainzdata = false; +const listenbrainzdata = ref(false); if (props.user.listenbrainz) { - try { - const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - }, - }); - const data = await response.json(); - if (data.payload.listens && data.payload.listens.length !== 0) { - listenbrainzdata = true; + (async function() { + try { + const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + }, + }); + const data = await response.json(); + if (data.payload.listens && data.payload.listens.length !== 0) { + listenbrainzdata.value = true; + } + } catch (err) { + listenbrainzdata.value = false; } - } catch (err) { - listenbrainzdata = false; - } + })() } const background = computed(() => { From 082e1d1afb44cc866d75c37ab5e0f7ca8701796b Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Mon, 3 Jun 2024 16:29:19 +0000 Subject: [PATCH 28/31] allow setting separate timeout / max size for imports - fixes #479 --- .config/example.yml | 5 +++++ packages/backend/src/config.ts | 14 +++++++++++++ packages/backend/src/core/DownloadService.ts | 8 +++---- .../processors/ImportNotesProcessorService.ts | 21 +++++++++++++------ 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/.config/example.yml b/.config/example.yml index c037a280b6..c82e744ee1 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -287,5 +287,10 @@ checkActivityPubGetSignature: false # Upload or download file size limits (bytes) #maxFileSize: 262144000 +# timeout and maximum size for imports (e.g. note imports) +#import: +# downloadTimeout: 30 +# maxFileSize: 262144000 + # PID File of master process #pidFile: /tmp/misskey.pid diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index f6ce9b3cdf..5974170a6d 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -97,6 +97,12 @@ type Source = { perChannelMaxNoteCacheCount?: number; perUserNotificationsMaxCount?: number; deactivateAntennaThreshold?: number; + + import?: { + downloadTimeout: number; + maxFileSize: number; + }; + pidFile: string; }; @@ -177,6 +183,12 @@ export type Config = { perChannelMaxNoteCacheCount: number; perUserNotificationsMaxCount: number; deactivateAntennaThreshold: number; + + import: { + downloadTimeout: number; + maxFileSize: number; + } | undefined; + pidFile: string; }; @@ -284,6 +296,7 @@ export function loadConfig(): Config { perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000, perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), + import: config.import, pidFile: config.pidFile, }; } @@ -425,4 +438,5 @@ function applyEnvOverrides(config: Source) { _apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]); _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'videoThumbnailGenerator']]); _apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]); + _apply_top(['import', ['downloadTimeout', 'maxFileSize']]); } diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 21ae798f9f..83452845d4 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -35,14 +35,14 @@ export class DownloadService { } @bindThis - public async downloadUrl(url: string, path: string): Promise<{ + public async downloadUrl(url: string, path: string, options: { timeout?: number, operationTimeout?: number, maxSize?: number} = {} ): Promise<{ filename: string; }> { this.logger.info(`Downloading ${chalk.cyan(url)} to ${chalk.cyanBright(path)} ...`); - const timeout = 30 * 1000; - const operationTimeout = 60 * 1000; - const maxSize = this.config.maxFileSize ?? 262144000; + const timeout = options.timeout ?? 30 * 1000; + const operationTimeout = options.operationTimeout ?? 60 * 1000; + const maxSize = options.maxSize ?? this.config.maxFileSize ?? 262144000; const urlObj = new URL(url); let filename = urlObj.pathname.split('/').pop() ?? 'untitled'; diff --git a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts index 7cef858c51..58a0ea10ad 100644 --- a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts @@ -19,12 +19,16 @@ import { IdService } from '@/core/IdService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbNoteWithParentImportToDbJobData } from '../types.js'; +import type { Config } from '@/config.js'; @Injectable() export class ImportNotesProcessorService { private logger: Logger; constructor( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -73,6 +77,11 @@ export class ImportNotesProcessorService { } } + @bindThis + private downloadUrl(url: string, path:string): Promise<{filename: string}> { + return this.downloadService.downloadUrl(url, path, { operationTimeout: this.config.import?.downloadTimeout, maxSize: this.config.import?.maxFileSize }); + } + @bindThis private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise<any[]> { type NotesMap = { @@ -176,7 +185,7 @@ export class ImportNotesProcessorService { try { await fsp.writeFile(destPath, '', 'binary'); - await this.downloadService.downloadUrl(file.url, destPath); + await this.downloadUrl(file.url, destPath); } catch (e) { // TODO: 何度か再試行 if (e instanceof Error || typeof e === 'string') { this.logger.error(e); @@ -206,7 +215,7 @@ export class ImportNotesProcessorService { try { await fsp.writeFile(destPath, '', 'binary'); - await this.downloadService.downloadUrl(file.url, destPath); + await this.downloadUrl(file.url, destPath); } catch (e) { // TODO: 何度か再試行 if (e instanceof Error || typeof e === 'string') { this.logger.error(e); @@ -239,7 +248,7 @@ export class ImportNotesProcessorService { try { await fsp.writeFile(destPath, '', 'binary'); - await this.downloadService.downloadUrl(file.url, destPath); + await this.downloadUrl(file.url, destPath); } catch (e) { // TODO: 何度か再試行 if (e instanceof Error || typeof e === 'string') { this.logger.error(e); @@ -297,7 +306,7 @@ export class ImportNotesProcessorService { try { await fsp.writeFile(path, '', 'utf-8'); - await this.downloadService.downloadUrl(file.url, path); + await this.downloadUrl(file.url, path); } catch (e) { // TODO: 何度か再試行 if (e instanceof Error || typeof e === 'string') { this.logger.error(e); @@ -349,7 +358,7 @@ export class ImportNotesProcessorService { if (!exists) { try { - await this.downloadService.downloadUrl(file.url, filePath); + await this.downloadUrl(file.url, filePath); } catch (e) { // TODO: 何度か再試行 this.logger.error(e instanceof Error ? e : new Error(e as string)); } @@ -488,7 +497,7 @@ export class ImportNotesProcessorService { if (!exists) { try { - await this.downloadService.downloadUrl(file.url, filePath); + await this.downloadUrl(file.url, filePath); } catch (e) { // TODO: 何度か再試行 this.logger.error(e instanceof Error ? e : new Error(e as string)); } From cb43994841935c6512c530c96ee409ffbd9f00f8 Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Fri, 7 Jun 2024 11:40:44 +0100 Subject: [PATCH 29/31] only allow a single boost via hotkey - fixes #467 #468 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As usual, have to write the same thing four times ☹ The parentheses around `q` tell the hotkey/keymap code to ignore auto-repeat events (which works fine in Chrome but not in Firefox, I reported the bug https://bugzilla.mozilla.org/show_bug.cgi?id=1900397 ) In addition, I've added a guard variable that is true while calling the backend to boost/renote, and false otherwise. This way, even in Firefox we don't spam-boost. Unboosting is still *only with the mouse*, I have not added that functionality. --- packages/frontend/src/components/MkNote.vue | 8 +++++--- packages/frontend/src/components/MkNoteDetailed.vue | 8 +++++--- packages/frontend/src/components/SkNote.vue | 8 +++++--- packages/frontend/src/components/SkNoteDetailed.vue | 8 +++++--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 9a667c3118..a1d241690f 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -328,10 +328,12 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string return false; } +let renoting = false; + const keymap = { 'r': () => reply(true), 'e|a|plus': () => react(true), - 'q': () => renote(appearNote.value.visibility), + '(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } }, 'up|k|shift+tab': focusBefore, 'down|j|tab': focusAfter, 'esc': blur, @@ -436,7 +438,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }); + }).then(() => { renoting = false }); } } else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; @@ -455,7 +457,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }); + }).then(() => renoting = false); } } } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 3d15f69f73..ec3c3ea3dc 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -336,10 +336,12 @@ if ($i) { }); } +let renoting = false; + const keymap = { 'r': () => reply(true), 'e|a|plus': () => react(true), - 'q': () => renote(appearNote.value.visibility), + '(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } }, 'esc': blur, 'm|o': () => showMenu(true), 's': () => showContent.value !== showContent.value, @@ -457,7 +459,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }); + }).then(() => { renoting = false }); } else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { @@ -474,7 +476,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }); + }).then(() => { renoting = false }); } } diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index 09decad1a2..fe14b1eafd 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -329,10 +329,12 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string return false; } +let renoting = false; + const keymap = { 'r': () => reply(true), 'e|a|plus': () => react(true), - 'q': () => renote(appearNote.value.visibility), + '(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } }, 'up|k|shift+tab': focusBefore, 'down|j|tab': focusAfter, 'esc': blur, @@ -437,7 +439,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }); + }).then(() => { renoting = false }); } } else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; @@ -456,7 +458,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }); + }).then(() => { renoting = false }); } } } diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index ced7e7a176..084c46ddcd 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -345,10 +345,12 @@ if ($i) { }); } +let renoting = false; + const keymap = { 'r': () => reply(true), 'e|a|plus': () => react(true), - 'q': () => renote(appearNote.value.visibility), + '(q)': () => { if (canRenote && !renoted.value && !renoting) { renoting = true; renote(appearNote.value.visibility) } }, 'esc': blur, 'm|o': () => showMenu(true), 's': () => showContent.value !== showContent.value, @@ -466,7 +468,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }); + }).then(() => { renoting = false }); } else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { @@ -483,7 +485,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }); + }).then(() => { renoting = false }); } } From 388926775ec14e58bf9daf447c28d197f925ee60 Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Fri, 7 Jun 2024 14:47:16 +0100 Subject: [PATCH 30/31] I meant `finally`, not `then`, thanks fEmber --- packages/frontend/src/components/MkNote.vue | 4 ++-- packages/frontend/src/components/MkNoteDetailed.vue | 4 ++-- packages/frontend/src/components/SkNote.vue | 4 ++-- packages/frontend/src/components/SkNoteDetailed.vue | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index a1d241690f..556fedbc30 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -438,7 +438,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }).then(() => { renoting = false }); + }).finally(() => { renoting = false }); } } else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; @@ -457,7 +457,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }).then(() => renoting = false); + }).finally(() => renoting = false); } } } diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index ec3c3ea3dc..ca017bf63b 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -459,7 +459,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }).then(() => { renoting = false }); + }).finally(() => { renoting = false }); } else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { @@ -476,7 +476,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }).then(() => { renoting = false }); + }).finally(() => { renoting = false }); } } diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index fe14b1eafd..3291390682 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -439,7 +439,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }).then(() => { renoting = false }); + }).finally(() => { renoting = false }); } } else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; @@ -458,7 +458,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }).then(() => { renoting = false }); + }).finally(() => { renoting = false }); } } } diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 084c46ddcd..1acd177e87 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -468,7 +468,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }).then(() => { renoting = false }); + }).finally(() => { renoting = false }); } else if (!appearNote.value.channel || appearNote.value.channel.allowRenoteToExternal) { const el = renoteButton.value as HTMLElement | null | undefined; if (el) { @@ -485,7 +485,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { }).then(() => { os.toast(i18n.ts.renoted); renoted.value = true; - }).then(() => { renoting = false }); + }).finally(() => { renoting = false }); } } From 079abfd7135e88562127b06135b0eb6aabee81b0 Mon Sep 17 00:00:00 2001 From: dakkar <dakkar@thenautilus.net> Date: Fri, 7 Jun 2024 15:10:16 +0100 Subject: [PATCH 31/31] rate limit note/reply/boost creation more tightly 5/minute is the same as 300/hour on average, and still high enough that it shouldn't be a problem for most people --- packages/backend/src/server/api/endpoints/notes/create.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 95ebda2f21..cc44721133 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -30,8 +30,8 @@ export const meta = { prohibitMoved: true, limit: { - duration: ms('1hour'), - max: 300, + duration: ms('1minute'), + max: 5, }, kind: 'write:notes',