From 1b7043fa79df1cf96e381550c4e5541abd7f3570 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 7 Jan 2023 19:57:48 +0900 Subject: [PATCH 01/68] :art: --- packages/frontend/src/pages/announcements.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index b06bd30245..131f6d11ea 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -53,14 +53,14 @@ definePageMetadata({ <style lang="scss" scoped> .ruryvtyk { > .announcement { + padding: 16px; + > .header { - padding: 16px; + margin-bottom: 16px; font-weight: bold; } > .content { - padding: 0 16px; - > img { display: block; max-height: 300px; @@ -69,7 +69,7 @@ definePageMetadata({ } > .footer { - padding: 16px; + margin-top: 16px; } } } From 8709574f3d4690873cc4f66053f8139ea4855fdf Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 09:58:35 +0900 Subject: [PATCH 02/68] :art: --- .../frontend/src/components/MkContextMenu.vue | 2 +- packages/frontend/src/components/MkModal.vue | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue index 9d9abc5d09..bb4a716565 100644 --- a/packages/frontend/src/components/MkContextMenu.vue +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -74,7 +74,7 @@ function onMousedown(evt: Event) { } .fade-enter-active, .fade-leave-active { - transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); + transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); transform-origin: left top; } diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index f323de605f..83b40fd01f 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -74,8 +74,24 @@ const type = $computed<ModalTypes>(() => { return props.preferType!; } }); -let transitionName = $ref(defaultStore.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''); -let transitionDuration = $ref(defaultStore.state.animation ? 200 : 0); +let transitionName = $computed((() => + defaultStore.state.animation + ? (type === 'drawer') + ? 'modal-drawer' + : (type === 'popup') + ? 'modal-popup' + : 'modal' + : '' +)); +let transitionDuration = $computed((() => + transitionName === 'modal-popup' + ? 100 + : transitionName === 'modal' + ? 200 + : transitionName === 'modal-drawer' + ? 200 + : 0 +)); let contentClicking = false; @@ -308,12 +324,12 @@ defineExpose({ .modal-popup-enter-active, .modal-popup-leave-active { > .bg { - transition: opacity 0.2s !important; + transition: opacity 0.1s !important; } > .content { transform-origin: var(--transformOrigin); - transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important; + transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important; } } .modal-popup-enter-from, .modal-popup-leave-to { From c550dafb815374cd050904d8959e62c8150889d9 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 10:20:28 +0900 Subject: [PATCH 03/68] tweak note component --- packages/frontend/src/components/MkMenu.vue | 7 +- packages/frontend/src/components/MkNote.vue | 65 +++++++++++- .../src/components/MkNoteDetailed.vue | 65 +++++++++++- .../src/components/MkRenoteButton.vue | 99 ------------------- 4 files changed, 124 insertions(+), 112 deletions(-) delete mode 100644 packages/frontend/src/components/MkRenoteButton.vue diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index 31a3f7f1c1..abae272e8f 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -251,17 +251,18 @@ onBeforeUnmount(() => { color: #fff; &:before { - background: #d42e2e; + background: #d42e2e !important; } } } + &:active, &.active { - color: var(--fgOnAccent); + color: var(--fgOnAccent) !important; opacity: 1; &:before { - background: var(--accent); + background: var(--accent) !important; } } diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 5a97204bc5..16d8a6c269 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -75,14 +75,26 @@ <i class="ti ti-arrow-back-up"></i> <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> </button> - <MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> - <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> + <button + v-if="canRenote" + ref="renoteButton" + class="button _button" + @click="renote()" + @mousedown="renote()" + > + <i class="ti ti-repeat"></i> + <p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p> + </button> + <button v-else class="button _button" disabled> + <i class="ti ti-ban"></i> + </button> + <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()" @mousedown="react()"> <i class="ti ti-plus"></i> </button> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <i class="ti ti-minus"></i> </button> - <button ref="menuButton" class="button _button" @click="menu()"> + <button ref="menuButton" class="button _button" @click="menu()" @mousedown="menu()"> <i class="ti ti-dots"></i> </button> </footer> @@ -111,7 +123,7 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; import MkMediaList from '@/components/MkMediaList.vue'; import MkCwButton from '@/components/MkCwButton.vue'; import MkPoll from '@/components/MkPoll.vue'; -import MkRenoteButton from '@/components/MkRenoteButton.vue'; +import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkVisibility from '@/components/MkVisibility.vue'; @@ -128,6 +140,7 @@ import { i18n } from '@/i18n'; import { getNoteMenu } from '@/scripts/get-note-menu'; import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; +import { useTooltip } from '@/scripts/use-tooltip'; const props = defineProps<{ note: misskey.entities.Note; @@ -158,7 +171,7 @@ const isRenote = ( const el = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>(); -const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>(); +const renoteButton = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>(); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); @@ -175,6 +188,7 @@ const translation = ref(null); const translating = ref(false); const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null; const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); const keymap = { 'r': () => reply(true), @@ -193,6 +207,47 @@ useNoteCapture({ isDeletedRef: isDeleted, }); +useTooltip(renoteButton, async (showing) => { + const renotes = await os.api('notes/renotes', { + noteId: appearNote.id, + limit: 11, + }); + + const users = renotes.map(x => x.user); + + if (users.length < 1) return; + + os.popup(MkUsersTooltip, { + showing, + users, + count: appearNote.renoteCount, + targetElement: renoteButton.value, + }, {}, 'closed'); +}); + +function renote(viaKeyboard = false) { + pleaseLogin(); + os.popupMenu([{ + text: i18n.ts.renote, + icon: 'ti ti-repeat', + action: () => { + os.api('notes/create', { + renoteId: appearNote.id, + }); + }, + }, { + text: i18n.ts.quote, + icon: 'ti ti-quote', + action: () => { + os.post({ + renote: appearNote, + }); + }, + }], renoteButton.value, { + viaKeyboard, + }); +} + function reply(viaKeyboard = false): void { pleaseLogin(); os.post({ diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index c0e1ca7215..2d2830f08c 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -85,14 +85,26 @@ <i class="ti ti-arrow-back-up"></i> <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> </button> - <MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> - <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> + <button + v-if="canRenote" + ref="renoteButton" + class="button _button" + @click="renote()" + @mousedown="renote()" + > + <i class="ti ti-repeat"></i> + <p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p> + </button> + <button v-else class="button _button" disabled> + <i class="ti ti-ban"></i> + </button> + <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()" @mousedown="react()"> <i class="ti ti-plus"></i> </button> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <i class="ti ti-minus"></i> </button> - <button ref="menuButton" class="button _button" @click="menu()"> + <button ref="menuButton" class="button _button" @click="menu()" @mousedown="menu()"> <i class="ti ti-dots"></i> </button> </footer> @@ -121,7 +133,7 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; import MkMediaList from '@/components/MkMediaList.vue'; import MkCwButton from '@/components/MkCwButton.vue'; import MkPoll from '@/components/MkPoll.vue'; -import MkRenoteButton from '@/components/MkRenoteButton.vue'; +import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import MkVisibility from '@/components/MkVisibility.vue'; @@ -138,6 +150,7 @@ import { i18n } from '@/i18n'; import { getNoteMenu } from '@/scripts/get-note-menu'; import { useNoteCapture } from '@/scripts/use-note-capture'; import { deepClone } from '@/scripts/clone'; +import { useTooltip } from '@/scripts/use-tooltip'; const props = defineProps<{ note: misskey.entities.Note; @@ -168,7 +181,7 @@ const isRenote = ( const el = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>(); -const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>(); +const renoteButton = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>(); const reactButton = shallowRef<HTMLElement>(); let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note); @@ -182,6 +195,7 @@ const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : n const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); const conversation = ref<misskey.entities.Note[]>([]); const replies = ref<misskey.entities.Note[]>([]); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); const keymap = { 'r': () => reply(true), @@ -198,6 +212,47 @@ useNoteCapture({ isDeletedRef: isDeleted, }); +useTooltip(renoteButton, async (showing) => { + const renotes = await os.api('notes/renotes', { + noteId: appearNote.id, + limit: 11, + }); + + const users = renotes.map(x => x.user); + + if (users.length < 1) return; + + os.popup(MkUsersTooltip, { + showing, + users, + count: appearNote.renoteCount, + targetElement: renoteButton.value, + }, {}, 'closed'); +}); + +function renote(viaKeyboard = false) { + pleaseLogin(); + os.popupMenu([{ + text: i18n.ts.renote, + icon: 'ti ti-repeat', + action: () => { + os.api('notes/create', { + renoteId: appearNote.id, + }); + }, + }, { + text: i18n.ts.quote, + icon: 'ti ti-quote', + action: () => { + os.post({ + renote: appearNote, + }); + }, + }], renoteButton.value, { + viaKeyboard, + }); +} + function reply(viaKeyboard = false): void { pleaseLogin(); os.post({ diff --git a/packages/frontend/src/components/MkRenoteButton.vue b/packages/frontend/src/components/MkRenoteButton.vue deleted file mode 100644 index e84d4a3faa..0000000000 --- a/packages/frontend/src/components/MkRenoteButton.vue +++ /dev/null @@ -1,99 +0,0 @@ -<template> -<button - v-if="canRenote" - ref="buttonRef" - class="eddddedb _button canRenote" - @click="renote()" -> - <i class="ti ti-repeat"></i> - <p v-if="count > 0" class="count">{{ count }}</p> -</button> -<button v-else class="eddddedb _button"> - <i class="ti ti-ban"></i> -</button> -</template> - -<script lang="ts" setup> -import { computed, ref, shallowRef } from 'vue'; -import * as misskey from 'misskey-js'; -import XDetails from '@/components/MkUsersTooltip.vue'; -import { pleaseLogin } from '@/scripts/please-login'; -import * as os from '@/os'; -import { $i } from '@/account'; -import { useTooltip } from '@/scripts/use-tooltip'; -import { i18n } from '@/i18n'; - -const props = defineProps<{ - note: misskey.entities.Note; - count: number; -}>(); - -const buttonRef = shallowRef<HTMLElement>(); - -const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id); - -useTooltip(buttonRef, async (showing) => { - const renotes = await os.api('notes/renotes', { - noteId: props.note.id, - limit: 11, - }); - - const users = renotes.map(x => x.user); - - if (users.length < 1) return; - - os.popup(XDetails, { - showing, - users, - count: props.count, - targetElement: buttonRef.value, - }, {}, 'closed'); -}); - -const renote = (viaKeyboard = false) => { - pleaseLogin(); - os.popupMenu([{ - text: i18n.ts.renote, - icon: 'ti ti-repeat', - action: () => { - os.api('notes/create', { - renoteId: props.note.id, - }); - }, - }, { - text: i18n.ts.quote, - icon: 'ti ti-quote', - action: () => { - os.post({ - renote: props.note, - }); - }, - }], buttonRef.value, { - viaKeyboard, - }); -}; -</script> - -<style lang="scss" scoped> -.eddddedb { - display: inline-block; - height: 32px; - margin: 2px; - padding: 0 6px; - border-radius: 4px; - - &:not(.canRenote) { - cursor: default; - } - - &.renoted { - background: var(--accent); - } - - > .count { - display: inline; - margin-left: 8px; - opacity: 0.7; - } -} -</style> From b93e56d2e57a13e2657bf3e7bb7aa56985232ce0 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 10:24:30 +0900 Subject: [PATCH 04/68] :art: --- packages/frontend/src/components/MkSelect.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 4b5a14f5be..e39cc2a5ce 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -1,7 +1,7 @@ <template> <div class="vblkjoeq"> <div class="label" @click="focus"><slot name="label"></slot></div> - <div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick"> + <div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="show" @mousedown.prevent="show"> <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> <select ref="inputEl" @@ -118,7 +118,7 @@ onMounted(() => { }); }); -const onClick = (ev: MouseEvent) => { +function show(ev: MouseEvent) { focused.value = true; opening.value = true; @@ -166,7 +166,7 @@ const onClick = (ev: MouseEvent) => { }).then(() => { focused.value = false; }); -}; +} </script> <style lang="scss" scoped> @@ -285,7 +285,7 @@ const onClick = (ev: MouseEvent) => { <style lang="scss" module> .chevron { - transition: transform 0.5s ease; + transition: transform 0.1s ease-out; } .chevronOpening { From 4594fb11de74bc2a67b2c75268efe75feaacf785 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 10:32:37 +0900 Subject: [PATCH 05/68] :art: --- packages/frontend/src/components/MkModal.vue | 2 +- packages/frontend/src/components/MkNote.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index 83b40fd01f..a83545ac15 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -1,7 +1,7 @@ <template> <Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened"> <div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }"> - <div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div> + <div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div> <div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick"> <slot :max-height="maxHeight" :type="type"></slot> </div> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 16d8a6c269..3aa9e516f0 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -44,7 +44,7 @@ <div class="text"> <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i"/> + <Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i"/> <a v-if="appearNote.renote != null" class="rp">RN:</a> <div v-if="translating || translation" class="translation"> <MkLoading v-if="translating" mini/> From 49f3090edd63a201e6093e1d1efeac362bf86d55 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 10:48:44 +0900 Subject: [PATCH 06/68] tweak note componsnt --- packages/frontend/src/components/MkNote.vue | 12 +++-- .../src/components/MkNoteDetailed.vue | 15 ++++-- .../frontend/src/components/MkNoteHeader.vue | 11 +++-- .../src/components/MkSubNoteContent.vue | 2 +- .../frontend/src/components/MkVisibility.vue | 48 ------------------- 5 files changed, 29 insertions(+), 59 deletions(-) delete mode 100644 packages/frontend/src/components/MkVisibility.vue diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 3aa9e516f0..5be8df5285 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -13,7 +13,7 @@ <div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div> <div v-if="appearNote._featuredId_" class="info"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div> <div v-if="isRenote" class="renote"> - <MkAvatar class="avatar" :user="note.user"/> + <MkAvatar v-once class="avatar" :user="note.user"/> <i class="ti ti-repeat"></i> <I18n :src="i18n.ts.renotedBy" tag="span"> <template #user> @@ -27,11 +27,16 @@ <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> <MkTime :time="note.createdAt"/> </button> - <MkVisibility :note="note"/> + <span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]"> + <i v-if="note.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> + <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> </div> </div> <article class="article" @contextmenu.stop="onContextmenu"> - <MkAvatar class="avatar" :user="appearNote.user"/> + <MkAvatar v-once class="avatar" :user="appearNote.user"/> <div class="main"> <MkNoteHeader class="header" :note="appearNote" :mini="true"/> <MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/> @@ -126,7 +131,6 @@ import MkPoll from '@/components/MkPoll.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; -import MkVisibility from '@/components/MkVisibility.vue'; import { pleaseLogin } from '@/scripts/please-login'; import { focusPrev, focusNext } from '@/scripts/focus'; import { checkWordMute } from '@/scripts/check-word-mute'; diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 2d2830f08c..d803253b2f 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -25,7 +25,12 @@ <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> <MkTime :time="note.createdAt"/> </button> - <MkVisibility :note="note"/> + <span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]"> + <i v-if="note.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> + <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> </div> </div> <article class="article" @contextmenu.stop="onContextmenu"> @@ -38,7 +43,12 @@ </MkA> <span v-if="appearNote.user.isBot" class="is-bot">bot</span> <div class="info"> - <MkVisibility :note="appearNote"/> + <span v-if="appearNote.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[appearNote.visibility]"> + <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock-open"></i> + <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="appearNote.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> </div> </div> <div class="username"><MkAcct :user="appearNote.user"/></div> @@ -136,7 +146,6 @@ import MkPoll from '@/components/MkPoll.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; -import MkVisibility from '@/components/MkVisibility.vue'; import { pleaseLogin } from '@/scripts/please-login'; import { checkWordMute } from '@/scripts/check-word-mute'; import { userPage } from '@/filters/user'; diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 333c3ddbd9..61d07f85e5 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -1,6 +1,6 @@ <template> <header class="kkwtjztg"> - <MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)"> + <MkA v-once v-user-preview="note.user.id" class="name" :to="userPage(note.user)"> <MkUserName :user="note.user"/> </MkA> <div v-if="note.user.isBot" class="is-bot">bot</div> @@ -9,7 +9,12 @@ <MkA class="created-at" :to="notePage(note)"> <MkTime :time="note.createdAt"/> </MkA> - <MkVisibility :note="note"/> + <span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]"> + <i v-if="note.visibility === 'home'" class="ti ti-home"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> + <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> + </span> + <span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> </div> </header> </template> @@ -17,7 +22,7 @@ <script lang="ts" setup> import { } from 'vue'; import * as misskey from 'misskey-js'; -import MkVisibility from '@/components/MkVisibility.vue'; +import { i18n } from '@/i18n'; import { notePage } from '@/filters/note'; import { userPage } from '@/filters/user'; diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 55b04e6686..79eb9ccf45 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -4,7 +4,7 @@ <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/> + <Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i"/> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <details v-if="note.files.length > 0"> diff --git a/packages/frontend/src/components/MkVisibility.vue b/packages/frontend/src/components/MkVisibility.vue deleted file mode 100644 index 2becb69d5a..0000000000 --- a/packages/frontend/src/components/MkVisibility.vue +++ /dev/null @@ -1,48 +0,0 @@ -<template> -<span v-if="note.visibility !== 'public'" :class="$style.visibility" :title="i18n.ts._visibility[note.visibility]"> - <i v-if="note.visibility === 'home'" class="ti ti-home"></i> - <i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> - <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> -</span> -<span v-if="note.localOnly" :class="$style.localOnly" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> -</template> - -<script lang="ts" setup> -import { ref } from 'vue'; -import XDetails from '@/components/MkUsersTooltip.vue'; -import * as os from '@/os'; -import { useTooltip } from '@/scripts/use-tooltip'; -import { i18n } from '@/i18n'; - -const props = defineProps<{ - note: { - visibility: string; - localOnly?: boolean; - visibleUserIds?: string[]; - }, -}>(); - -const specified = $shallowRef<HTMLElement>(); - -if (props.note.visibility === 'specified') { - useTooltip($$(specified), async (showing) => { - const users = await os.api('users/show', { - userIds: props.note.visibleUserIds, - limit: 10, - }); - - os.popup(XDetails, { - showing, - users, - count: props.note.visibleUserIds.length, - targetElement: specified, - }, {}, 'closed'); - }); -} -</script> - -<style lang="scss" module> -.visibility, .localOnly { - margin-left: 0.5em; -} -</style> From 2e2ed1385fbae6c102156a08fce2defb642e52c5 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 10:54:45 +0900 Subject: [PATCH 07/68] delete pollVote notification --- CHANGELOG.md | 1 + locales/ja-JP.yml | 2 - packages/backend/src/core/PollService.ts | 7 -- .../entities/NotificationEntityService.ts | 2 +- .../src/models/entities/Notification.ts | 10 +-- .../server/api/endpoints/notes/polls/vote.ts | 7 -- .../src/components/MkNotification.vue | 12 ---- .../sw/src/scripts/create-notification.ts | 68 ++++++++----------- 8 files changed, 37 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78caef3252..e437881129 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ You should also include the user name that made the change. #### For users - ノートのウォッチ機能が削除されました +- アンケートに投票された際に通知が作成されなくなりました - 新たに動的なPagesを作ることはできなくなりました - 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。 - AiScriptが0.12.2にアップデートされました diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b49d872a0b..3445e58356 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1550,7 +1550,6 @@ _notification: youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" youRenoted: "{name}がRenoteしました" - youGotPoll: "{name}が投票しました" youGotMessagingMessageFromUser: "{name}からのチャットがあります" youGotMessagingMessageFromGroup: "{name}のチャットがあります" youWereFollowed: "フォローされました" @@ -1569,7 +1568,6 @@ _notification: renote: "Renote" quote: "引用" reaction: "リアクション" - pollVote: "アンケートに投票された" pollEnded: "アンケートが終了" receiveFollowRequest: "フォロー申請を受け取った" followRequestAccepted: "フォローが受理された" diff --git a/packages/backend/src/core/PollService.ts b/packages/backend/src/core/PollService.ts index 3cc9b0cc9b..abc598ab76 100644 --- a/packages/backend/src/core/PollService.ts +++ b/packages/backend/src/core/PollService.ts @@ -92,13 +92,6 @@ export class PollService { choice: choice, userId: user.id, }); - - // Notify - this.createNotificationService.createNotification(note.userId, 'pollVote', { - notifierId: user.id, - noteId: note.id, - choice: choice, - }); } @bindThis diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 208d653877..a1c2c9cffb 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -98,7 +98,7 @@ export class NotificationEntityService implements OnModuleInit { }), reaction: notification.reaction, } : {}), - ...(notification.type === 'pollVote' ? { + ...(notification.type === 'pollVote' ? { // TODO: そのうち消す note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, { detail: true, _hint_: options._hintForEachNotes_, diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts index 53a7dda43a..6679cdb809 100644 --- a/packages/backend/src/models/entities/Notification.ts +++ b/packages/backend/src/models/entities/Notification.ts @@ -55,11 +55,11 @@ export class Notification { * 通知の種類。 * follow - フォローされた * mention - 投稿で自分が言及された - * reply - (自分または自分がWatchしている)投稿が返信された - * renote - (自分または自分がWatchしている)投稿がRenoteされた - * quote - (自分または自分がWatchしている)投稿が引用Renoteされた - * reaction - (自分または自分がWatchしている)投稿にリアクションされた - * pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された + * reply - 投稿に返信された + * renote - 投稿がRenoteされた + * quote - 投稿が引用Renoteされた + * reaction - 投稿にリアクションされた + * pollVote - 投稿のアンケートに投票された (廃止) * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された diff --git a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts index 793d7c5408..d583dfb936 100644 --- a/packages/backend/src/server/api/endpoints/notes/polls/vote.ts +++ b/packages/backend/src/server/api/endpoints/notes/polls/vote.ts @@ -162,13 +162,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { userId: me.id, }); - // Notify - this.createNotificationService.createNotification(note.userId, 'pollVote', { - notifierId: me.id, - noteId: note.id, - choice: ps.choice, - }); - // リモート投票の場合リプライ送信 if (note.userHost != null) { const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser; diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index a21a9e12a1..2b9f9a2bb8 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -13,7 +13,6 @@ <i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i> <i v-else-if="notification.type === 'mention'" class="ti ti-at"></i> <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> - <i v-else-if="notification.type === 'pollVote'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <XReactionIcon @@ -51,11 +50,6 @@ <MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> </MkA> - <MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> - <i class="ti ti-quote"></i> - <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> - <i class="ti ti-quote"></i> - </MkA> <MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> <i class="ti ti-quote"></i> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> @@ -239,12 +233,6 @@ useTooltip(reactionRef, (showing) => { pointer-events: none; } - &.pollVote { - padding: 3px; - background: #88a6b7; - pointer-events: none; - } - &.pollEnded { padding: 3px; background: #88a6b7; diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts index a324851fe2..4b006e0f5a 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -51,8 +51,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data actions: userDetail.isFollowing ? [] : [ { action: 'follow', - title: t('_notification._actions.followBack') - } + title: t('_notification._actions.followBack'), + }, ], }]; } @@ -66,8 +66,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data actions: [ { action: 'reply', - title: t('_notification._actions.reply') - } + title: t('_notification._actions.reply'), + }, ], }]; @@ -80,8 +80,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data actions: [ { action: 'reply', - title: t('_notification._actions.reply') - } + title: t('_notification._actions.reply'), + }, ], }]; @@ -94,8 +94,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data actions: [ { action: 'showUser', - title: getUserName(data.body.user) - } + title: getUserName(data.body.user), + }, ], }]; @@ -108,14 +108,14 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data actions: [ { action: 'reply', - title: t('_notification._actions.reply') + title: t('_notification._actions.reply'), }, ...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [ - { - action: 'renote', - title: t('_notification._actions.renote') - } - ] : []) + { + action: 'renote', + title: t('_notification._actions.renote'), + }, + ] : []), ], }]; @@ -141,7 +141,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.png`; badge = `${origin}/proxy/${dummy}?${url.query({ url: u.href, - badge: '1' + badge: '1', })}`; } } @@ -162,20 +162,12 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data actions: [ { action: 'showUser', - title: getUserName(data.body.user) - } + title: getUserName(data.body.user), + }, ], }]; } - case 'pollVote': - return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), { - body: data.body.note.text || '', - icon: data.body.user.avatarUrl, - badge: iconUrl('poll-h'), - data, - }]; - case 'pollEnded': return [t('_notification.pollEnded'), { body: data.body.note.text || '', @@ -192,12 +184,12 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data actions: [ { action: 'accept', - title: t('accept') + title: t('accept'), }, { action: 'reject', - title: t('reject') - } + title: t('reject'), + }, ], }]; @@ -217,21 +209,21 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data actions: [ { action: 'accept', - title: t('accept') + title: t('accept'), }, { action: 'reject', - title: t('reject') - } + title: t('reject'), + }, ], }]; case 'app': - return [data.body.header || data.body.body, { - body: data.body.header && data.body.body, - icon: data.body.icon, - data - }]; + return [data.body.header || data.body.body, { + body: data.body.header && data.body.body, + icon: data.body.icon, + data, + }]; default: return null; @@ -279,7 +271,7 @@ export async function createEmptyNotification() { silent: true, badge: iconUrl('null'), tag: 'read_notification', - } + }, ); res(); @@ -288,7 +280,7 @@ export async function createEmptyNotification() { for (const n of [ ...(await self.registration.getNotifications({ tag: 'user_visible_auto_notification' })), - ...(await self.registration.getNotifications({ tag: 'read_notification' })) + ...(await self.registration.getNotifications({ tag: 'read_notification' })), ] ) { n.close(); From 91356b1805f255ecaf5efc3e169d36280ddaf9d4 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 11:12:11 +0900 Subject: [PATCH 08/68] tweak --- packages/frontend/src/components/MkNote.vue | 5 ++--- packages/frontend/src/components/MkNoteDetailed.vue | 5 ++--- packages/frontend/src/components/MkSelect.vue | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 5be8df5285..86f176a693 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -84,7 +84,6 @@ v-if="canRenote" ref="renoteButton" class="button _button" - @click="renote()" @mousedown="renote()" > <i class="ti ti-repeat"></i> @@ -93,13 +92,13 @@ <button v-else class="button _button" disabled> <i class="ti ti-ban"></i> </button> - <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()" @mousedown="react()"> + <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()"> <i class="ti ti-plus"></i> </button> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <i class="ti ti-minus"></i> </button> - <button ref="menuButton" class="button _button" @click="menu()" @mousedown="menu()"> + <button ref="menuButton" class="button _button" @mousedown="menu()"> <i class="ti ti-dots"></i> </button> </footer> diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index d803253b2f..7c57a64b09 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -99,7 +99,6 @@ v-if="canRenote" ref="renoteButton" class="button _button" - @click="renote()" @mousedown="renote()" > <i class="ti ti-repeat"></i> @@ -108,13 +107,13 @@ <button v-else class="button _button" disabled> <i class="ti ti-ban"></i> </button> - <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()" @mousedown="react()"> + <button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()"> <i class="ti ti-plus"></i> </button> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <i class="ti ti-minus"></i> </button> - <button ref="menuButton" class="button _button" @click="menu()" @mousedown="menu()"> + <button ref="menuButton" class="button _button" @mousedown="menu()"> <i class="ti ti-dots"></i> </button> </footer> diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index e39cc2a5ce..c0ab781b06 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -1,7 +1,7 @@ <template> <div class="vblkjoeq"> <div class="label" @click="focus"><slot name="label"></slot></div> - <div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="show" @mousedown.prevent="show"> + <div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show"> <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div> <select ref="inputEl" From ecbefce2aaad4ab53861d50d93c4d339ad2597e4 Mon Sep 17 00:00:00 2001 From: Soni L <EnderMoneyMod@gmail.com> Date: Sat, 7 Jan 2023 23:15:54 -0300 Subject: [PATCH 09/68] Support remote objects in search (#9479) * Support remote objects in search Closes #9428 * Use account instead of localStorage * Use useRouter instead of mainRouter Co-authored-by: Chaos <chaoticryptidz@owo.monster> --- packages/frontend/src/pages/search.vue | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue index c080b763bb..7918f9f577 100644 --- a/packages/frontend/src/pages/search.vue +++ b/packages/frontend/src/pages/search.vue @@ -12,12 +12,37 @@ import { computed } from 'vue'; import XNotes from '@/components/MkNotes.vue'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import * as os from '@/os'; +import { useRouter } from '@/router'; +import { $i } from '@/account'; + +const router = useRouter(); const props = defineProps<{ query: string; channel?: string; }>(); +const query = props.query; + +if ($i != null) { + if (query.startsWith('https://') || (query.startsWith('@') && !query.includes(' '))) { + const promise = os.api('ap/show', { + uri: props.query, + }); + + os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); + + const res = await promise; + + if (res.type === 'User') { + router.replace(`/@${res.object.username}@${res.object.host}`); + } else if (res.type === 'Note') { + router.replace(`/notes/${res.object.id}`); + } + } +} + const pagination = { endpoint: 'notes/search' as const, limit: 10, From 49a95c34bf1cdead34b87a82f4a0ca311e6c08bd Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 11:16:26 +0900 Subject: [PATCH 10/68] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e437881129..c172a766e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ You should also include the user name that made the change. - Client: Improve RSS widget @tamaina - Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz - Client: OpenSearch support @SoniEx2 @chaoticryptidz +- Client: Support remote objects in search @SoniEx2 - Client: add user list widget @syuilo - Client: add heatmap of daily active users to about page @syuilo - Client: introduce fluent emoji @syuilo From a3e282bc75ec11757018e88d246ce9a205b8bfac Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 11:16:36 +0900 Subject: [PATCH 11/68] New Crowdin updates (#9478) * New translations ja-JP.yml (Thai) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (Italian) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (English) * New translations ja-JP.yml (German) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Slovak) --- locales/de-DE.yml | 4 ++++ locales/en-US.yml | 4 ++++ locales/it-IT.yml | 2 +- locales/ko-KR.yml | 4 ++++ locales/sk-SK.yml | 4 ++++ locales/th-TH.yml | 12 ++++++++++++ locales/zh-TW.yml | 14 ++++++++++++++ 7 files changed, 43 insertions(+), 1 deletion(-) diff --git a/locales/de-DE.yml b/locales/de-DE.yml index db6bd9ab05..3ab2e6abc3 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -920,6 +920,10 @@ like: "Gefällt mir" unlike: "\"Gefällt mir\" entfernen" numberOfLikes: "\"Gefällt mir\"-Anzahl" show: "Anzeigen" +neverShow: "Nicht wieder anzeigen" +remindMeLater: "Vielleicht später" +didYouLikeMisskey: "Gefällt dir Misskey?" +pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!" _sensitiveMediaDetection: description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht." sensitivity: "Erkennungssensitivität" diff --git a/locales/en-US.yml b/locales/en-US.yml index e2a7b32be8..5da7032eda 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -920,6 +920,10 @@ like: "Like" unlike: "Unlike" numberOfLikes: "Likes" show: "Show" +neverShow: "Don't show again" +remindMeLater: "Maybe later" +didYouLikeMisskey: "Have you taken a liking to Misskey?" +pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!" _sensitiveMediaDetection: description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server." sensitivity: "Detection sensitivity" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 3fba19985e..10e8c98177 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -28,7 +28,7 @@ timeline: "Timeline" noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo." login: "Accedi" loggingIn: "Accesso in corso..." -logout: "Esci" +logout: "Uscita" signup: "Iscriviti" uploading: "Caricamento..." save: "Salva" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index d3a4a40b49..522f1f4aa7 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -920,6 +920,10 @@ like: "좋아요!" unlike: "좋아요 취소" numberOfLikes: "좋아요 수" show: "표시" +neverShow: "다시 보지 않기" +remindMeLater: "나중에 알림" +didYouLikeMisskey: "Misskey가 마음에 드시나요?" +pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!" _sensitiveMediaDetection: description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다." sensitivity: "탐지 민감도" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 3abfd8609d..35afc91918 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -913,6 +913,10 @@ tools: "Nástroje" cannotLoad: "Nedá sa načítať." like: "Páči sa mi" show: "Zobraziť" +neverShow: "Nabudúce nezobrazovať" +remindMeLater: "Pripomenúť neskôr" +didYouLikeMisskey: "Páči sa vám Misskey?" +pleaseDonate: "Misskey je bezplatný softvér, ktorý používa {host}. Prosím, prispejte, aby sme ho mohli ďalej rozvíjať!" _sensitiveMediaDetection: description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera." sensitivity: "Citlivosť detekcie" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 58deeff6f1..5a3400feb1 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -917,6 +917,8 @@ tools: "เครื่องมือ" cannotLoad: "ไม่สามารถโหลดได้" numberOfProfileView: "มุมมองโปรไฟล์" like: "ชื่นชอบ" +unlike: "ไม่ชอบ" +numberOfLikes: "จำนวนไลค์" show: "แสดงผล" _sensitiveMediaDetection: description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" @@ -1317,6 +1319,7 @@ _widgets: jobQueue: "คิวงาน" serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์" aiscript: "AiScript คอนโซล" + aiscriptApp: "AiScript แอพ" aichan: "เอไอ" userList: "รายชื่อผู้ใช้" _userList: @@ -1423,7 +1426,16 @@ _timelines: social: "โซเชี่ยล" global: "ทั่วโลก" _play: + new: "สร้างการเล่น" + edit: "แก้ไขเล่น" + created: "สร้างการเล่นแล้ว" + updated: "แก้ไขการเล่นแล้ว" + deleted: "ลบการเล่นแล้ว" + pageSetting: "ตั้งค่าการเล่น" + editThisPage: "แก้ไข Play นี้" viewSource: "ดูต้นฉบับ" + my: "มาย เพลย์" + liked: "ไลค์ เพลย์" featured: "เป็นที่นิยม" title: "หัวข้อ" script: "สคริปต์" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 661325d506..de12ef1fbf 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -918,7 +918,11 @@ cannotLoad: "無法載入" numberOfProfileView: "個人檔案檢視次數" like: "讚" unlike: "收回讚" +numberOfLikes: "讚數" show: "檢視" +neverShow: "不再顯示" +remindMeLater: "以後再說" +didYouLikeMisskey: "您是否喜愛Misskey呢?" _sensitiveMediaDetection: description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。" sensitivity: "檢測敏感度" @@ -1318,6 +1322,7 @@ _widgets: jobQueue: "佇列" serverMetric: "服務器指標 " aiscript: "AiScript控制台" + aiscriptApp: "AiScript App" aichan: "小藍" userList: "使用者列表" _userList: @@ -1424,7 +1429,16 @@ _timelines: social: "社群" global: "公開" _play: + new: "新增Play" + edit: "編輯Play" + created: "已新增Play" + updated: "已更新Play" + deleted: "已刪除Play" + pageSetting: "Play設定" + editThisPage: "編輯這個Play" viewSource: "檢視原始碼" + my: "自己的Play" + liked: "按了讚的Play" featured: "人氣" title: "標題" script: "腳本" From f20d7cba74d044700388da61f3d959b455f3f9f5 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 11:17:02 +0900 Subject: [PATCH 12/68] 13.0.0-beta.28 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3d429ec63..185e338e63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0-beta.27", + "version": "13.0.0-beta.28", "codename": "indigo", "repository": { "type": "git", From 244ea9593a799348d2c36b6846df652ba3a3f1bc Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 11:30:40 +0900 Subject: [PATCH 13/68] tweak components --- .../src/components/MkNotification.vue | 96 ++++++++++--------- packages/frontend/src/pages/about.emojis.vue | 2 +- .../frontend/src/pages/about.federation.vue | 2 +- packages/frontend/src/pages/admin/users.vue | 2 +- 4 files changed, 53 insertions(+), 49 deletions(-) diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 2b9f9a2bb8..d5638813b4 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -1,6 +1,6 @@ <template> <div ref="elRef" class="qglefbjs" :class="notification.type"> - <div class="head"> + <div v-once class="head"> <MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/> <MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/> <img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/> @@ -31,37 +31,39 @@ <span v-else>{{ notification.header }}</span> <MkTime v-if="withTime" :time="notification.createdAt" class="time"/> </header> - <MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> - <i class="ti ti-quote"></i> - <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> - <i class="ti ti-quote"></i> - </MkA> - <MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> - <i class="ti ti-quote"></i> - <Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/> - <i class="ti ti-quote"></i> - </MkA> - <MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> - <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> - </MkA> - <MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> - <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> - </MkA> - <MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> - <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> - </MkA> - <MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> - <i class="ti ti-quote"></i> - <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> - <i class="ti ti-quote"></i> - </MkA> - <span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> - <span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> - <span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span> - <span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span> - <span v-if="notification.type === 'app'" class="text"> - <Mfm :text="notification.body" :nowrap="!full"/> - </span> + <div v-once class="content"> + <MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> + <i class="ti ti-quote"></i> + <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> + <i class="ti ti-quote"></i> + </MkA> + <MkA v-else-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)"> + <i class="ti ti-quote"></i> + <Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/> + <i class="ti ti-quote"></i> + </MkA> + <MkA v-else-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> + <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> + </MkA> + <MkA v-else-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> + <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> + </MkA> + <MkA v-else-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> + <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> + </MkA> + <MkA v-else-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> + <i class="ti ti-quote"></i> + <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/> + <i class="ti ti-quote"></i> + </MkA> + <span v-else-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span> + <span v-else-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> + <span v-else-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span> + <span v-else-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span> + <span v-else-if="notification.type === 'app'" class="text"> + <Mfm :text="notification.body" :nowrap="!full"/> + </span> + </div> </div> </div> </template> @@ -263,23 +265,25 @@ useTooltip(reactionRef, (showing) => { } } - > .text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + > .content { + > .text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; - > i { - vertical-align: super; - font-size: 50%; - opacity: 0.5; - } + > i { + vertical-align: super; + font-size: 50%; + opacity: 0.5; + } - > i:first-child { - margin-right: 4px; - } + > i:first-child { + margin-right: 4px; + } - > i:last-child { - margin-left: 4px; + > i:last-child { + margin-left: 4px; + } } } } diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index fef5cfff6a..669b6f1264 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -22,7 +22,7 @@ <MkFolder v-for="category in customEmojiCategories" :key="category" class="emojis"> <template #header>{{ category || $ts.other }}</template> <div class="zuvgdzyt"> - <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/> + <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" v-once :key="emoji.name" class="emoji" :emoji="emoji"/> </div> </MkFolder> </div> diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index 101ea2297b..e530bb49dd 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -36,7 +36,7 @@ <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> <div class="dqokceoi"> - <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`"> + <MkA v-for="instance in items" v-once :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`"> <MkInstanceCardMini :instance="instance"/> </MkA> </div> diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index babe76e4ec..c198dfdb46 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -41,7 +41,7 @@ </div> <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users"> - <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`"> + <MkA v-for="user in items" v-once :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`"> <MkUserCardMini :user="user"/> </MkA> </MkPagination> From 192add376c8350bc9edff54c21be6796162877dd Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 11:47:16 +0900 Subject: [PATCH 14/68] fix MkModal animation --- packages/frontend/src/components/MkModal.vue | 30 +++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index a83545ac15..492c281e98 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -63,6 +63,7 @@ let transformOrigin = $ref('center'); let showing = $ref(true); let content = $shallowRef<HTMLElement>(); const zIndex = os.claimZIndex(props.zPriority); +let useSendAnime = $ref(false); const type = $computed<ModalTypes>(() => { if (props.preferType === 'auto') { if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { @@ -76,29 +77,32 @@ const type = $computed<ModalTypes>(() => { }); let transitionName = $computed((() => defaultStore.state.animation - ? (type === 'drawer') - ? 'modal-drawer' - : (type === 'popup') - ? 'modal-popup' - : 'modal' + ? useSendAnime + ? 'send' + : type === 'drawer' + ? 'modal-drawer' + : type === 'popup' + ? 'modal-popup' + : 'modal' : '' )); let transitionDuration = $computed((() => - transitionName === 'modal-popup' - ? 100 - : transitionName === 'modal' - ? 200 - : transitionName === 'modal-drawer' + transitionName === 'send' + ? 400 + : transitionName === 'modal-popup' + ? 100 + : transitionName === 'modal' ? 200 - : 0 + : transitionName === 'modal-drawer' + ? 200 + : 0 )); let contentClicking = false; function close(opts: { useSendAnimation?: boolean } = {}) { if (opts.useSendAnimation) { - transitionName = 'send'; - transitionDuration = 400; + useSendAnime = true; } // eslint-disable-next-line vue/no-mutating-props From 8b1fdb5a3b12a9a04edf5b1d4b4fe37ffa0feb5d Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 11:55:37 +0900 Subject: [PATCH 15/68] enhance(client): add theme --- CHANGELOG.md | 1 + packages/frontend/src/scripts/theme.ts | 1 + .../frontend/src/themes/l-botanical.json5 | 29 +++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 packages/frontend/src/themes/l-botanical.json5 diff --git a/CHANGELOG.md b/CHANGELOG.md index c172a766e1..76caefd453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ You should also include the user name that made the change. - Client: add user list widget @syuilo - Client: add heatmap of daily active users to about page @syuilo - Client: introduce fluent emoji @syuilo +- Client: add new theme @syuilo - Client: show fireworks when visit user who today is birthday @syuilo - Client: show bot warning on screen when logged in as bot account @syuilo - Client: improve overall performance of client @syuilo diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index 42cb00265d..28284c7bcf 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -24,6 +24,7 @@ export const getBuiltinThemes = () => Promise.all( 'l-coffee', 'l-apricot', 'l-rainy', + 'l-botanical', 'l-vivid', 'l-cherry', 'l-sushi', diff --git a/packages/frontend/src/themes/l-botanical.json5 b/packages/frontend/src/themes/l-botanical.json5 new file mode 100644 index 0000000000..2ea9a7d6c9 --- /dev/null +++ b/packages/frontend/src/themes/l-botanical.json5 @@ -0,0 +1,29 @@ +{ + id: '1100673c-f902-4ccd-93aa-7cb88be56178', + + name: 'Mi Botanical Light', + author: 'ThinaticSystem', + + base: 'light', + + props: { + accent: '#77b58c', + bg: 'e2deda', + fg: '#3d3d3d', + fgHighlighted: '#6bc9a0', + divider: '#cfcfcf', + panel: '@X14', + panelHeaderBg: '@panel', + panelHeaderDivider: '@divider', + header: ':alpha<0.7<@panel', + navBg: '@X14', + renote: '#229e92', + mention: '#da6d35', + mentionMe: '#d44c4c', + hashtag: '#4cb8d4', + link: '@accent', + buttonGradateB: ':hue<-70<@accent', + success: '#86b300', + X14: '#ebe7e5' + }, +} From 01652b72b3274d4d55f29c026df49260cb459ca9 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 11:57:34 +0900 Subject: [PATCH 16/68] :art: --- packages/frontend/src/widgets/calendar.vue | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/widgets/calendar.vue b/packages/frontend/src/widgets/calendar.vue index 99bd36e2fc..1bd431259a 100644 --- a/packages/frontend/src/widgets/calendar.vue +++ b/packages/frontend/src/widgets/calendar.vue @@ -11,19 +11,19 @@ </div> <div class="info"> <div> - <p>{{ i18n.ts.today }}: <b>{{ dayP.toFixed(1) }}%</b></p> + <p>{{ i18n.ts.today }}<b>{{ dayP.toFixed(1) }}%</b></p> <div class="meter"> <div class="val" :style="{ width: `${dayP}%` }"></div> </div> </div> <div> - <p>{{ i18n.ts.thisMonth }}: <b>{{ monthP.toFixed(1) }}%</b></p> + <p>{{ i18n.ts.thisMonth }}<b>{{ monthP.toFixed(1) }}%</b></p> <div class="meter"> <div class="val" :style="{ width: `${monthP}%` }"></div> </div> </div> <div> - <p>{{ i18n.ts.thisYear }}: <b>{{ yearP.toFixed(1) }}%</b></p> + <p>{{ i18n.ts.thisYear }}<b>{{ yearP.toFixed(1) }}%</b></p> <div class="meter"> <div class="val" :style="{ width: `${yearP}%` }"></div> </div> @@ -168,13 +168,14 @@ defineExpose<WidgetComponentExpose>({ } > p { + display: flex; margin: 0 0 2px 0; font-size: 0.75em; line-height: 18px; opacity: 0.8; > b { - margin-left: 2px; + margin-left: auto; } } From 132e45dff456d610fee60aa7e77f8eb422ac45ff Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 11:58:00 +0900 Subject: [PATCH 17/68] 13.0.0-beta.29 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 185e338e63..65b8cf510c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0-beta.28", + "version": "13.0.0-beta.29", "codename": "indigo", "repository": { "type": "git", From 4ffbbbe6d8c178f37b4294aa492f4214106c2dec Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 13:10:01 +0900 Subject: [PATCH 18/68] :art: --- .../frontend/src/pages/settings/accounts.vue | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue index c66cc12925..c2d511e744 100644 --- a/packages/frontend/src/pages/settings/accounts.vue +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -1,18 +1,20 @@ <template> -<div class="_gaps_m"> +<div class=""> <FormSuspense :p="init"> - <MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton> + <div class="_gaps"> + <MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton> - <div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)"> - <div class="avatar"> - <MkAvatar :user="account" class="avatar"/> - </div> - <div class="body"> - <div class="name"> - <MkUserName :user="account"/> + <div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)"> + <div class="avatar"> + <MkAvatar :user="account" class="avatar"/> </div> - <div class="acct"> - <MkAcct :user="account"/> + <div class="body"> + <div class="name"> + <MkUserName :user="account"/> + </div> + <div class="acct"> + <MkAcct :user="account"/> + </div> </div> </div> </div> From 5320f2301718e3e3b8f69f979c5dccefffe7689d Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 14:17:56 +0900 Subject: [PATCH 19/68] enhance(client): improve user activity page --- packages/frontend/src/components/MkFolder.vue | 10 +- packages/frontend/src/components/MkSignin.vue | 4 +- .../src/pages/user/activity.following.vue | 174 ++++++++++++++++++ .../src/pages/user/activity.notes.vue | 174 ++++++++++++++++++ .../frontend/src/pages/user/activity.pv.vue | 78 +++----- packages/frontend/src/pages/user/activity.vue | 14 +- 6 files changed, 386 insertions(+), 68 deletions(-) create mode 100644 packages/frontend/src/pages/user/activity.following.vue create mode 100644 packages/frontend/src/pages/user/activity.notes.vue diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index dc10c7d3f3..aa2d9aac25 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -1,7 +1,7 @@ <template> <div class="ssazuxis"> <header class="_button" :style="{ background: bg }" @click="showBody = !showBody"> - <div class="title"><slot name="header"></slot></div> + <div class="title"><div><slot name="header"></slot></div></div> <div class="divider"></div> <button class="_button"> <template v-if="showBody"><i class="ti ti-chevron-up"></i></template> @@ -127,14 +127,6 @@ export default defineComponent({ place-content: center; margin: 0; padding: 12px 16px 12px 0; - - > i { - margin-right: 6px; - } - - &:empty { - display: none; - } } > .divider { diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 56e6f938f8..d70ae13ffd 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -14,7 +14,7 @@ <template #prefix><i class="ti ti-lock"></i></template> <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> </MkInput> - <MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> + <MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> </div> <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> <div v-if="user && user.securityKeys" class="twofa-group tap-group"> @@ -36,7 +36,7 @@ <template #label>{{ i18n.ts.token }}</template> <template #prefix><i class="ti ti-123"></i></template> </MkInput> - <MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> + <MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> </div> </div> </div> diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue new file mode 100644 index 0000000000..b7a51d5b69 --- /dev/null +++ b/packages/frontend/src/pages/user/activity.following.vue @@ -0,0 +1,174 @@ +<template> +<div> + <MkLoading v-if="fetching"/> + <div v-show="!fetching" :class="$style.root" class="_panel"> + <canvas ref="chartEl"></canvas> + <MkChartLegend ref="legendEl" style="margin-top: 8px;"/> + </div> +</div> +</template> + +<script lang="ts" setup> +import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; +import { Chart, ChartDataset } from 'chart.js'; +import tinycolor from 'tinycolor2'; +import * as misskey from 'misskey-js'; +import gradient from 'chartjs-plugin-gradient'; +import { satisfies } from 'compare-versions'; +import * as os from '@/os'; +import { defaultStore } from '@/store'; +import { useChartTooltip } from '@/scripts/use-chart-tooltip'; +import { chartVLine } from '@/scripts/chart-vline'; +import { alpha } from '@/scripts/color'; +import { initChart } from '@/scripts/init-chart'; +import { chartLegend } from '@/scripts/chart-legend'; +import MkChartLegend from '@/components/MkChartLegend.vue'; + +initChart(); + +const props = defineProps<{ + user: misskey.entities.User; +}>(); + +const chartEl = $shallowRef<HTMLCanvasElement>(null); +let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); +const now = new Date(); +let chartInstance: Chart = null; +const chartLimit = 50; +let fetching = $ref(true); + +const { handler: externalTooltipHandler } = useChartTooltip(); + +async function renderChart() { + if (chartInstance) { + chartInstance.destroy(); + } + + const getDate = (ago: number) => { + const y = now.getFullYear(); + const m = now.getMonth(); + const d = now.getDate(); + + return new Date(y, m, d - ago); + }; + + const format = (arr) => { + return arr.map((v, i) => ({ + x: getDate(i).getTime(), + y: v, + })); + }; + + const raw = await os.api('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' }); + + const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + + const colorFollowLocal = '#008FFB'; + const colorFollowRemote = '#008FFB88'; + const colorFollowedLocal = '#2ecc71'; + const colorFollowedRemote = '#2ecc7188'; + + function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { + return Object.assign({ + label: label, + data: data, + parsing: false, + pointRadius: 0, + borderWidth: 0, + borderJoinStyle: 'round', + borderRadius: 4, + barPercentage: 0.9, + fill: true, + } satisfies ChartDataset, extra); + } + + chartInstance = new Chart(chartEl, { + type: 'bar', + data: { + datasets: [ + makeDataset('Follow (local)', format(raw.local.followings.inc).slice().reverse(), { backgroundColor: colorFollowLocal }), + makeDataset('Follow (remote)', format(raw.remote.followings.inc).slice().reverse(), { backgroundColor: colorFollowRemote }), + makeDataset('Followed (local)', format(raw.local.followers.inc).slice().reverse(), { backgroundColor: colorFollowedLocal }), + makeDataset('Followed (remote)', format(raw.remote.followers.inc).slice().reverse(), { backgroundColor: colorFollowedRemote }), + ], + }, + options: { + aspectRatio: 3, + layout: { + padding: { + left: 0, + right: 8, + top: 0, + bottom: 0, + }, + }, + scales: { + x: { + type: 'time', + offset: true, + stacked: true, + time: { + stepSize: 1, + unit: 'day', + displayFormats: { + day: 'M/d', + month: 'Y/M', + }, + }, + grid: { + display: false, + }, + ticks: { + display: true, + maxRotation: 0, + autoSkipPadding: 8, + }, + }, + y: { + position: 'left', + stacked: true, + suggestedMax: 10, + grid: { + display: true, + }, + ticks: { + display: true, + //mirror: true, + }, + }, + }, + interaction: { + intersect: false, + mode: 'index', + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: false, + mode: 'index', + animation: { + duration: 0, + }, + external: externalTooltipHandler, + }, + gradient, + }, + }, + plugins: [chartVLine(vLineColor), chartLegend(legendEl)], + }); + + fetching = false; +} + +onMounted(async () => { + renderChart(); +}); +</script> + +<style lang="scss" module> +.root { + padding: 20px; +} +</style> diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue new file mode 100644 index 0000000000..f2103b152c --- /dev/null +++ b/packages/frontend/src/pages/user/activity.notes.vue @@ -0,0 +1,174 @@ +<template> +<div> + <MkLoading v-if="fetching"/> + <div v-show="!fetching" :class="$style.root" class="_panel"> + <canvas ref="chartEl"></canvas> + <MkChartLegend ref="legendEl" style="margin-top: 8px;"/> + </div> +</div> +</template> + +<script lang="ts" setup> +import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; +import { Chart, ChartDataset } from 'chart.js'; +import tinycolor from 'tinycolor2'; +import * as misskey from 'misskey-js'; +import gradient from 'chartjs-plugin-gradient'; +import { satisfies } from 'compare-versions'; +import * as os from '@/os'; +import { defaultStore } from '@/store'; +import { useChartTooltip } from '@/scripts/use-chart-tooltip'; +import { chartVLine } from '@/scripts/chart-vline'; +import { alpha } from '@/scripts/color'; +import { initChart } from '@/scripts/init-chart'; +import { chartLegend } from '@/scripts/chart-legend'; +import MkChartLegend from '@/components/MkChartLegend.vue'; + +initChart(); + +const props = defineProps<{ + user: misskey.entities.User; +}>(); + +const chartEl = $shallowRef<HTMLCanvasElement>(null); +let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); +const now = new Date(); +let chartInstance: Chart = null; +const chartLimit = 50; +let fetching = $ref(true); + +const { handler: externalTooltipHandler } = useChartTooltip(); + +async function renderChart() { + if (chartInstance) { + chartInstance.destroy(); + } + + const getDate = (ago: number) => { + const y = now.getFullYear(); + const m = now.getMonth(); + const d = now.getDate(); + + return new Date(y, m, d - ago); + }; + + const format = (arr) => { + return arr.map((v, i) => ({ + x: getDate(i).getTime(), + y: v, + })); + }; + + const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' }); + + const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; + + const colorNormal = '#008FFB'; + const colorReply = '#FEB019'; + const colorRenote = '#00E396'; + const colorFile = '#e300db'; + + function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { + return Object.assign({ + label: label, + data: data, + parsing: false, + pointRadius: 0, + borderWidth: 0, + borderJoinStyle: 'round', + borderRadius: 4, + barPercentage: 0.9, + fill: true, + } satisfies ChartDataset, extra); + } + + chartInstance = new Chart(chartEl, { + type: 'bar', + data: { + datasets: [ + makeDataset('Normal', format(raw.diffs.normal).slice().reverse(), { backgroundColor: colorNormal }), + makeDataset('Reply', format(raw.diffs.reply).slice().reverse(), { backgroundColor: colorReply }), + makeDataset('Renote', format(raw.diffs.renote).slice().reverse(), { backgroundColor: colorRenote }), + makeDataset('File', format(raw.diffs.withFile).slice().reverse(), { backgroundColor: colorFile }), + ], + }, + options: { + aspectRatio: 3, + layout: { + padding: { + left: 0, + right: 8, + top: 0, + bottom: 0, + }, + }, + scales: { + x: { + type: 'time', + offset: true, + stacked: true, + time: { + stepSize: 1, + unit: 'day', + displayFormats: { + day: 'M/d', + month: 'Y/M', + }, + }, + grid: { + display: false, + }, + ticks: { + display: true, + maxRotation: 0, + autoSkipPadding: 8, + }, + }, + y: { + position: 'left', + stacked: true, + suggestedMax: 10, + grid: { + display: true, + }, + ticks: { + display: true, + //mirror: true, + }, + }, + }, + interaction: { + intersect: false, + mode: 'index', + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: false, + mode: 'index', + animation: { + duration: 0, + }, + external: externalTooltipHandler, + }, + gradient, + }, + }, + plugins: [chartVLine(vLineColor), chartLegend(legendEl)], + }); + + fetching = false; +} + +onMounted(async () => { + renderChart(); +}); +</script> + +<style lang="scss" module> +.root { + padding: 20px; +} +</style> diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue index d74b641dac..4be6978da0 100644 --- a/packages/frontend/src/pages/user/activity.pv.vue +++ b/packages/frontend/src/pages/user/activity.pv.vue @@ -10,7 +10,7 @@ <script lang="ts" setup> import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; -import { Chart } from 'chart.js'; +import { Chart, ChartDataset } from 'chart.js'; import tinycolor from 'tinycolor2'; import * as misskey from 'misskey-js'; import gradient from 'chartjs-plugin-gradient'; @@ -67,65 +67,33 @@ async function renderChart() { const colorUser2 = '#3498db88'; const colorVisitor2 = '#2ecc7188'; + function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset { + return Object.assign({ + label: label, + data: data, + parsing: false, + pointRadius: 0, + borderWidth: 0, + borderJoinStyle: 'round', + borderRadius: 4, + barPercentage: 0.7, + categoryPercentage: 0.7, + fill: true, + } satisfies ChartDataset, extra); + } + chartInstance = new Chart(chartEl, { type: 'bar', data: { - datasets: [{ - parsing: false, - label: 'UPV (user)', - data: format(raw.upv.user).slice().reverse(), - pointRadius: 0, - borderWidth: 0, - borderJoinStyle: 'round', - borderRadius: 4, - backgroundColor: colorUser, - barPercentage: 0.7, - categoryPercentage: 0.7, - fill: true, - stack: 'u', - }, { - parsing: false, - label: 'UPV (visitor)', - data: format(raw.upv.visitor).slice().reverse(), - pointRadius: 0, - borderWidth: 0, - borderJoinStyle: 'round', - borderRadius: 4, - backgroundColor: colorVisitor, - barPercentage: 0.7, - categoryPercentage: 0.7, - fill: true, - stack: 'u', - }, { - parsing: false, - label: 'NPV (user)', - data: format(raw.pv.user).slice().reverse(), - pointRadius: 0, - borderWidth: 0, - borderJoinStyle: 'round', - borderRadius: 4, - backgroundColor: colorUser2, - barPercentage: 0.7, - categoryPercentage: 0.7, - fill: true, - stack: 'n', - }, { - parsing: false, - label: 'NPV (visitor)', - data: format(raw.pv.visitor).slice().reverse(), - pointRadius: 0, - borderWidth: 0, - borderJoinStyle: 'round', - borderRadius: 4, - backgroundColor: colorVisitor2, - barPercentage: 0.7, - categoryPercentage: 0.7, - fill: true, - stack: 'n', - }], + datasets: [ + makeDataset('UPV (user)', format(raw.upv.user).slice().reverse(), { backgroundColor: colorUser, stack: 'u' }), + makeDataset('UPV (visitor)', format(raw.upv.visitor).slice().reverse(), { backgroundColor: colorVisitor, stack: 'u' }), + makeDataset('NPV (user)', format(raw.pv.user).slice().reverse(), { backgroundColor: colorUser2, stack: 'n' }), + makeDataset('UPV (visitor)', format(raw.pv.visitor).slice().reverse(), { backgroundColor: colorVisitor2, stack: 'n' }), + ], }, options: { - aspectRatio: 2.5, + aspectRatio: 3, layout: { padding: { left: 0, diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue index 3def414674..e74b82fb27 100644 --- a/packages/frontend/src/pages/user/activity.vue +++ b/packages/frontend/src/pages/user/activity.vue @@ -2,11 +2,19 @@ <MkSpacer :content-max="700"> <div class="_gaps"> <MkFolder class="item"> - <template #header>Heatmap</template> + <template #header><i class="ti ti-activity"></i> Heatmap</template> <XHeatmap :user="user" :src="'notes'"/> </MkFolder> <MkFolder class="item"> - <template #header>PV</template> + <template #header><i class="ti ti-pencil"></i> Notes</template> + <XNotes :user="user"/> + </MkFolder> + <MkFolder class="item"> + <template #header><i class="ti ti-users"></i> Following</template> + <XFollowing :user="user"/> + </MkFolder> + <MkFolder class="item"> + <template #header><i class="ti ti-eye"></i> PV</template> <XPv :user="user"/> </MkFolder> </div> @@ -18,6 +26,8 @@ import { computed } from 'vue'; import * as misskey from 'misskey-js'; import XHeatmap from './activity.heatmap.vue'; import XPv from './activity.pv.vue'; +import XNotes from './activity.notes.vue'; +import XFollowing from './activity.following.vue'; import MkFolder from '@/components/MkFolder.vue'; const props = defineProps<{ From 25f4ee70306fff43cc78fbbbb036e33a64813f7e Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 14:19:32 +0900 Subject: [PATCH 20/68] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76caefd453..611d5d986d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ You should also include the user name that made the change. - Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz - Client: OpenSearch support @SoniEx2 @chaoticryptidz - Client: Support remote objects in search @SoniEx2 +- Client: user activity page @syuilo - Client: add user list widget @syuilo - Client: add heatmap of daily active users to about page @syuilo - Client: introduce fluent emoji @syuilo From 1cfdd4c41a92d8734b13e6fcc12176c8d0bec7b2 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 14:22:04 +0900 Subject: [PATCH 21/68] refactor --- packages/frontend/src/components/MkNotification.vue | 4 ++-- packages/frontend/src/components/MkPlusOneEffect.vue | 4 ++-- packages/frontend/src/components/MkReactionTooltip.vue | 4 ++-- .../frontend/src/components/MkReactionsViewer.details.vue | 4 ++-- .../frontend/src/components/MkReactionsViewer.reaction.vue | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index d5638813b4..7f21bb298a 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -15,7 +15,7 @@ <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> - <XReactionIcon + <MkReactionIcon v-else-if="notification.type === 'reaction'" ref="reactionRef" :reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" @@ -71,7 +71,7 @@ <script lang="ts" setup> import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue'; import * as misskey from 'misskey-js'; -import XReactionIcon from '@/components/MkReactionIcon.vue'; +import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import XReactionTooltip from '@/components/MkReactionTooltip.vue'; import { getNoteSummary } from '@/scripts/get-note-summary'; diff --git a/packages/frontend/src/components/MkPlusOneEffect.vue b/packages/frontend/src/components/MkPlusOneEffect.vue index 3e4f2059fd..af3a6c0e41 100644 --- a/packages/frontend/src/components/MkPlusOneEffect.vue +++ b/packages/frontend/src/components/MkPlusOneEffect.vue @@ -1,7 +1,7 @@ <template> <div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> <span class="text" :class="{ up }"> - <XReactionIcon class="icon" :reaction="reaction"/> + <MkReactionIcon class="icon" :reaction="reaction"/> </span> </div> </template> @@ -9,7 +9,7 @@ <script lang="ts" setup> import { onMounted } from 'vue'; import * as os from '@/os'; -import XReactionIcon from '@/components/MkReactionIcon.vue'; +import MkReactionIcon from '@/components/MkReactionIcon.vue'; const props = withDefaults(defineProps<{ reaction: string; diff --git a/packages/frontend/src/components/MkReactionTooltip.vue b/packages/frontend/src/components/MkReactionTooltip.vue index 34ebc4da2d..3b3c2c1a1d 100644 --- a/packages/frontend/src/components/MkReactionTooltip.vue +++ b/packages/frontend/src/components/MkReactionTooltip.vue @@ -1,7 +1,7 @@ <template> <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> <div class="beeadbfb"> - <XReactionIcon :reaction="reaction" class="icon" :no-style="true"/> + <MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/> <div class="name">{{ reaction.replace('@.', '') }}</div> </div> </MkTooltip> @@ -10,7 +10,7 @@ <script lang="ts" setup> import { } from 'vue'; import MkTooltip from './MkTooltip.vue'; -import XReactionIcon from '@/components/MkReactionIcon.vue'; +import MkReactionIcon from '@/components/MkReactionIcon.vue'; defineProps<{ showing: boolean; diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index aae647b6af..6ec227b0ed 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -2,7 +2,7 @@ <MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> <div class="bqxuuuey"> <div class="reaction"> - <XReactionIcon :reaction="reaction" class="icon" :no-style="true"/> + <MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/> <div class="name">{{ getReactionName(reaction) }}</div> </div> <div class="users"> @@ -19,7 +19,7 @@ <script lang="ts" setup> import { } from 'vue'; import MkTooltip from './MkTooltip.vue'; -import XReactionIcon from '@/components/MkReactionIcon.vue'; +import MkReactionIcon from '@/components/MkReactionIcon.vue'; import { getEmojiName } from '@/scripts/emojilist'; defineProps<{ diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index e63192a629..1d5c402765 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -6,7 +6,7 @@ :class="{ reacted: note.myReaction == reaction, canToggle }" @click="toggleReaction()" > - <XReactionIcon class="icon" :reaction="reaction"/> + <MkReactionIcon class="icon" :reaction="reaction"/> <span class="count">{{ count }}</span> </button> </template> @@ -15,7 +15,7 @@ import { computed, onMounted, ref, shallowRef, watch } from 'vue'; import * as misskey from 'misskey-js'; import XDetails from '@/components/MkReactionsViewer.details.vue'; -import XReactionIcon from '@/components/MkReactionIcon.vue'; +import MkReactionIcon from '@/components/MkReactionIcon.vue'; import * as os from '@/os'; import { useTooltip } from '@/scripts/use-tooltip'; import { $i } from '@/account'; From dcca2350ddff11e3b5f0d38784b437504c055af1 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 14:28:14 +0900 Subject: [PATCH 22/68] :art: --- .../src/components/MkReactionsViewer.reaction.vue | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 1d5c402765..321ce7ab2c 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -1,6 +1,6 @@ <template> <button - ref="buttonRef" + ref="buttonEl" v-ripple="canToggle" class="hkzvhatu _button" :class="{ reacted: note.myReaction == reaction, canToggle }" @@ -28,7 +28,7 @@ const props = defineProps<{ note: misskey.entities.Note; }>(); -const buttonRef = shallowRef<HTMLElement>(); +const buttonEl = shallowRef<HTMLElement>(); const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); @@ -58,9 +58,9 @@ const toggleReaction = () => { const anime = () => { if (document.hidden) return; - const rect = buttonRef.value.getBoundingClientRect(); - const x = rect.left + (buttonRef.value.offsetWidth / 2); - const y = rect.top + (buttonRef.value.offsetHeight / 2); + const rect = buttonEl.value.getBoundingClientRect(); + const x = rect.left + 16; + const y = rect.top + (buttonEl.value.offsetHeight / 2); os.popup(MkPlusOneEffect, { reaction: props.reaction, x, y }, {}, 'end'); }; @@ -72,7 +72,7 @@ onMounted(() => { if (!props.isInitial) anime(); }); -useTooltip(buttonRef, async (showing) => { +useTooltip(buttonEl, async (showing) => { const reactions = await os.apiGet('notes/reactions', { noteId: props.note.id, type: props.reaction, @@ -87,7 +87,7 @@ useTooltip(buttonRef, async (showing) => { reaction: props.reaction, users, count: props.count, - targetElement: buttonRef.value, + targetElement: buttonEl.value, }, {}, 'closed'); }, 100); </script> From 0d7ee20a77d731ce93aa1cde8388aece53377fd0 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 14:29:22 +0900 Subject: [PATCH 23/68] :art: --- packages/frontend/src/pages/flash/flash-index.vue | 2 +- packages/frontend/src/pages/pages.vue | 2 +- packages/frontend/src/ui/visitor/b.vue | 2 +- packages/frontend/src/ui/visitor/header.vue | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue index fb377be579..7a1080d3f0 100644 --- a/packages/frontend/src/pages/flash/flash-index.vue +++ b/packages/frontend/src/pages/flash/flash-index.vue @@ -69,7 +69,7 @@ const headerActions = $computed(() => [{ const headerTabs = $computed(() => [{ key: 'featured', title: i18n.ts._play.featured, - icon: 'fas fa-fire-alt', + icon: 'ti ti-flare', }, { key: 'my', title: i18n.ts._play.my, diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue index b077180df8..af5f631caf 100644 --- a/packages/frontend/src/pages/pages.vue +++ b/packages/frontend/src/pages/pages.vue @@ -63,7 +63,7 @@ const headerActions = $computed(() => [{ const headerTabs = $computed(() => [{ key: 'featured', title: i18n.ts._pages.featured, - icon: 'fas fa-fire-alt', + icon: 'ti ti-flare', }, { key: 'my', title: i18n.ts._pages.my, diff --git a/packages/frontend/src/ui/visitor/b.vue b/packages/frontend/src/ui/visitor/b.vue index c8b32bb98f..9a2320da88 100644 --- a/packages/frontend/src/ui/visitor/b.vue +++ b/packages/frontend/src/ui/visitor/b.vue @@ -34,7 +34,7 @@ <div v-if="showMenu" class="menu"> <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA> <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA> - <MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA> + <MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA> <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA> <div class="action"> <button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button> diff --git a/packages/frontend/src/ui/visitor/header.vue b/packages/frontend/src/ui/visitor/header.vue index 7300b12a75..07a40c0378 100644 --- a/packages/frontend/src/ui/visitor/header.vue +++ b/packages/frontend/src/ui/visitor/header.vue @@ -4,7 +4,7 @@ <div class="content"> <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA> <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA> - <MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA> + <MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA> <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA> <div v-if="info" class="page active link"> <div class="title"> From 1f6a41cea7b1015029e3bf04528e4edb0de76af4 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 14:30:00 +0900 Subject: [PATCH 24/68] 13.0.0-beta.30 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65b8cf510c..46e96d8716 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0-beta.29", + "version": "13.0.0-beta.30", "codename": "indigo", "repository": { "type": "git", From 7c2d2676f77310c3a4269c532f5e1d17262a5608 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 16:17:42 +0900 Subject: [PATCH 25/68] refactor --- .../components/{MkPlusOneEffect.vue => MkReactionEffect.vue} | 0 .../frontend/src/components/MkReactionsViewer.reaction.vue | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/frontend/src/components/{MkPlusOneEffect.vue => MkReactionEffect.vue} (100%) diff --git a/packages/frontend/src/components/MkPlusOneEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue similarity index 100% rename from packages/frontend/src/components/MkPlusOneEffect.vue rename to packages/frontend/src/components/MkReactionEffect.vue diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index 321ce7ab2c..cc6e87e1c4 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -19,7 +19,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue'; import * as os from '@/os'; import { useTooltip } from '@/scripts/use-tooltip'; import { $i } from '@/account'; -import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; +import MkReactionEffect from '@/components/MkReactionEffect.vue'; const props = defineProps<{ reaction: string; @@ -61,7 +61,7 @@ const anime = () => { const rect = buttonEl.value.getBoundingClientRect(); const x = rect.left + 16; const y = rect.top + (buttonEl.value.offsetHeight / 2); - os.popup(MkPlusOneEffect, { reaction: props.reaction, x, y }, {}, 'end'); + os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end'); }; watch(() => props.count, (newCount, oldCount) => { From b442c38f4148d0825189c646cfc249ec4a4c212e Mon Sep 17 00:00:00 2001 From: tamaina <tamaina@hotmail.co.jp> Date: Sun, 8 Jan 2023 16:47:57 +0900 Subject: [PATCH 26/68] enhance: Push Notification badges to Tabler Icons (#9406) * enhance: Push Notification badges to Tabler Icons * add receiveFollowRequest icon --- .../assets/notification-badges/LICENSE | 5 --- .../backend/assets/notification-badges/at.png | Bin 1752 -> 0 bytes .../assets/notification-badges/check.png | Bin 577 -> 0 bytes .../clipboard-check-solid.png | Bin 1402 -> 0 bytes .../assets/notification-badges/clock.png | Bin 1131 -> 0 bytes .../assets/notification-badges/comments.png | Bin 1134 -> 0 bytes .../notification-badges/id-card-alt.png | Bin 844 -> 0 bytes .../assets/notification-badges/plus.png | Bin 507 -> 0 bytes .../assets/notification-badges/poll-h.png | Bin 689 -> 0 bytes .../notification-badges/quote-right.png | Bin 772 -> 0 bytes .../assets/notification-badges/reply.png | Bin 930 -> 0 bytes .../assets/notification-badges/retweet.png | Bin 798 -> 0 bytes .../assets/notification-badges/satellite.png | Bin 1743 -> 0 bytes .../assets/notification-badges/user-plus.png | Bin 991 -> 0 bytes packages/backend/assets/tabler-badges/LICENSE | 24 +++++++++++++ .../backend/assets/tabler-badges/antenna.png | Bin 0 -> 516 bytes .../assets/tabler-badges/arrow-back-up.png | Bin 0 -> 952 bytes packages/backend/assets/tabler-badges/at.png | Bin 0 -> 2909 bytes .../assets/tabler-badges/chart-arrows.png | Bin 0 -> 829 bytes .../assets/tabler-badges/circle-check.png | Bin 0 -> 2307 bytes .../backend/assets/tabler-badges/messages.png | Bin 0 -> 1056 bytes .../null.png | Bin .../backend/assets/tabler-badges/plus.png | Bin 0 -> 414 bytes .../backend/assets/tabler-badges/quote.png | Bin 0 -> 1011 bytes .../backend/assets/tabler-badges/repeat.png | Bin 0 -> 1206 bytes .../assets/tabler-badges/user-plus.png | Bin 0 -> 1431 bytes .../backend/assets/tabler-badges/users.png | Bin 0 -> 1911 bytes .../sw/src/scripts/create-notification.ts | 32 +++++++++++------- packages/sw/src/types.ts | 15 ++++++++ 29 files changed, 59 insertions(+), 17 deletions(-) delete mode 100644 packages/backend/assets/notification-badges/LICENSE delete mode 100644 packages/backend/assets/notification-badges/at.png delete mode 100644 packages/backend/assets/notification-badges/check.png delete mode 100644 packages/backend/assets/notification-badges/clipboard-check-solid.png delete mode 100644 packages/backend/assets/notification-badges/clock.png delete mode 100644 packages/backend/assets/notification-badges/comments.png delete mode 100644 packages/backend/assets/notification-badges/id-card-alt.png delete mode 100644 packages/backend/assets/notification-badges/plus.png delete mode 100644 packages/backend/assets/notification-badges/poll-h.png delete mode 100644 packages/backend/assets/notification-badges/quote-right.png delete mode 100644 packages/backend/assets/notification-badges/reply.png delete mode 100644 packages/backend/assets/notification-badges/retweet.png delete mode 100644 packages/backend/assets/notification-badges/satellite.png delete mode 100644 packages/backend/assets/notification-badges/user-plus.png create mode 100644 packages/backend/assets/tabler-badges/LICENSE create mode 100644 packages/backend/assets/tabler-badges/antenna.png create mode 100644 packages/backend/assets/tabler-badges/arrow-back-up.png create mode 100644 packages/backend/assets/tabler-badges/at.png create mode 100644 packages/backend/assets/tabler-badges/chart-arrows.png create mode 100644 packages/backend/assets/tabler-badges/circle-check.png create mode 100644 packages/backend/assets/tabler-badges/messages.png rename packages/backend/assets/{notification-badges => tabler-badges}/null.png (100%) create mode 100644 packages/backend/assets/tabler-badges/plus.png create mode 100644 packages/backend/assets/tabler-badges/quote.png create mode 100644 packages/backend/assets/tabler-badges/repeat.png create mode 100644 packages/backend/assets/tabler-badges/user-plus.png create mode 100644 packages/backend/assets/tabler-badges/users.png diff --git a/packages/backend/assets/notification-badges/LICENSE b/packages/backend/assets/notification-badges/LICENSE deleted file mode 100644 index 841c4c682b..0000000000 --- a/packages/backend/assets/notification-badges/LICENSE +++ /dev/null @@ -1,5 +0,0 @@ -Font Awesome Icons -------------------------- - -Ⓒ Font Awesome -CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/) diff --git a/packages/backend/assets/notification-badges/at.png b/packages/backend/assets/notification-badges/at.png deleted file mode 100644 index d1492856de2b2f837a24a439eaa13631283aa87a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1752 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 diff --git a/packages/backend/assets/notification-badges/check.png b/packages/backend/assets/notification-badges/check.png deleted file mode 100644 index baeb76babfa3f9f8dff025f7a007a0be3eeb69b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 577 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VB+v}aSW-L^Y%`>FSDaa>qq5< zLK7NVv>7=q9Gz4{n1CD>l@O=O8~4~xeA;eYT=uPQf68I$xj*?Bojg>5#(+S*=%l_$ z^VZA1U%OoMiNZ^jmzzGx^gh>mxZtUFoo=0-{<h0+@8mx+c2ckJJkQblSKUt8q{zF* zZAW1DkDd?yx{sfKw)w&){94d{etYBh8A0cN)M)>5eE&n-^LgU-nP)nHjQquVCX)j{ z{&d`vl=5fukM5W3iyzzb?Yyeek?&HY{772xe#Z~DBj!r`E?)WQeChI6rsdb>*ED24 zmf!Lx|8(WkR=>4zOE#~3Jio5u;Po*5YbVz|&aZ6|`ncW7Y2}Y->4hnO*7gKg?W=Cd zeC&OC{VZL6i>j8DkL^pWUaSyY*0=s#`+M6%Up_8<8n0X>x92*??hgI0@(Yf0?C#Xx z`k(PmbxZ8g^W42Ve7Xh8#O+;=cW;qO-2O56PQinF%5UuIy7Kv+NiFpGBXa8;$J0m4 zC)BrW`WSq|-09~JO_Ms7NA*Hd)=VG6TaNWQ{Lydu^YGL$f5rL(ASLpfa{h>XV%L?n zZ~c>a8YKJxEW8UWtPc`?2oipDyR{=<Txnly$B#1noNM=aT7EFgZrLMy?(|nnuU*A* d9FQ2ycq{w-4nxDepeZ{+0-mmZF6*2UngEjs688WA diff --git a/packages/backend/assets/notification-badges/clipboard-check-solid.png b/packages/backend/assets/notification-badges/clipboard-check-solid.png deleted file mode 100644 index d8cdfa9da4562d7a247ab7c734933be48bc68fc6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1402 zcmV-=1%>*FP)<h;3K|Lk000e1NJLTq003YB003YJ1^@s6;+S_h000F+Nkl<ZcwX(D zfpOD75JgD`(1FuIr~p#|p#qW)LIsct2o>N|03Bc|ARXWipaa;SIn5YH((PZJbbFfJ znHvZ4Wu5-py*tU4a`vpPcrM+&l=e;<e%_?+ALJG7hqRrvKdl_rStA{V*&n1`CF675 zz7xUc1SO}f`ui3DY{j9kO5;z>d@KL)t*XPBoqGpxlQJI_V2$fEELpdobvw@hreuCI zmEaWsGX9rw*y+nyOv*1k&Iw)tV8Z=axb}DQei)8ig+~C|?0-lZjqJWiEobhzw<_=m z02A-05hr2ztu!n(kK;=I@g+mxeb06|XtVy_BLFNpak4cOV<uudY(ErOl45D<COiW8 zIWnAJ*ui8v7MQ#@0|Hdf3pegdM?3-;GjJz;&P}8kzA%jS#cBa;<!E0?!w`V%n{whb zmY|v;WDLO6<Ojfmw7s-@X{g9aF|`7^YcxX{H>q<y8*4H+H;0t$6BYuPlgu#%0G56@ zSY-T?F?M|?Hwn+tPb3BUgsPy!3ILh^T^b-P9XMcQ-R@+^+msM<4S!?Dsj1A~?X3Xj z9A~vaF&`X~80ObVhU30!=BFpeW&u#6%(YNz>*&kh765Zxr>SjRsr_SE%q^4u+m{8v z*oji918Mbr=()fGVC+PxRpUzadl3MCOZ9f8??WXuB~FF~fbq<sJf*||KvlDpr<6DV z1OSx-l|@uiQ{t!q6<W$uN*n+JfXac&A}XmVaa4c`E#)aC4gdi_<v?W-mDH3tDnNyn z@{|$>fB>L!pt6WcYDydxph8P|N{ItN08lwlSwtl@C5{SEp`|>f!~q}xs2r#)qLP{t zudo8}A$sX=(ovpL;uQga@wd`;((rT1Z~9T5QsR{WfZ^BD@KwU&C<TG?loGE1z>xVJ zh!PMePbqQZ0CMJ+f<Spni5mu>HNOM|%2P_*C;-;{4gzj@y)rCmDNiYJg8;1NcfMP` z2TOTMi5mkj)qEVT<$g2DQ%c+rfD-281kLim5Xw_Z+z5bD=3@@P-9T(mo>Jl&0F=HI zP}_W1M0rYy3j^56`{B8657KU=jXj`4lZ774ZwA1f^z`%w=dJu7Zhf_c7xS9{&}V*U zx&?wK^W6g&Gv5NioB6H*Xv}9Icr@QF0LFX`1h3}11OT(nrQwy0zB$3O`7QzAgO4eX z2|I=W^UY5tN%V1sYdy)*BoMW<r^IRRmfKWO0S8U*2Q+|y>oTvU;k>~bbDQt70?;X# zmV4Hl)l}_vOPKE#Ky3(1neQ3^I=4^5h&F3W=$16!J%FVlz<kUdm_wEdYF?$p=?E;7 z@ae%XQ3)`=#$(kePbqOh0CQJDE%Tcgf{v?Uj!LL$escgyS3+&`n*>k_f(P@P1uzwY z7xSA2V1=+rs~x<02x?(Ni3_iShRk9nW^d*<UjZEy4Fr$ouLeMFIPz-#ssIc@@NE9- z0CW&=N6SIlRvJD`@iO7|&12NON{JVmgoh?)e5P>WVVRTZ1LY|tt_+|7?;pEJc}j@` zfGP(lPbqN#2mmSvDvPM3ro>SJDzucRlsEtc0F?ukMO0E#;-~->TFO&OoOb_PUUB_B zqTd0^Q%aon7k|WOIQ0(lD|`)89Tos39%!NNU}Hl1i^vuLxE_Nlh<NJ2B7evYyS3aw zzII?cIY3;srNUAH*a0SCRKbSY+j_<3n^3F(U?qm48h_5#8^%d@>`VdB8DV{_i#H=J z{n^V#822r=Up?-x1OU9xrL;HFwksc&U^E&&9J-f=RnMgO2d_sLaN2p@od5s;07*qo IM6N<$g3e8FJOBUy diff --git a/packages/backend/assets/notification-badges/clock.png b/packages/backend/assets/notification-badges/clock.png deleted file mode 100644 index 9323f8f3070dd4ef538f7df33d54975c449d7cf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1131 zcmV-x1eE)UP)<h;3K|Lk000e1NJLTq003YB003YJ1^@s6;+S_h000CsNkl<Zc%1EB z(UGhm5bO{Pfe;LV5H<uuxC{tkLokFFf+1`OhF}Q3546tRLq!&u2JNc;ySi$+br?`w z;KmJ4C;&Bp$HWG}&%|b;o~TlbD+v{>i3Y$w051RofH8pm^gqn>KX0r+!CuK7!p-*O z9EeIVE^1V;W#ff&4c}r18~mkE19;;6EBz>+DEzL_;56P<qSPr42%&;CUfS5BY>{~o zbB$-e;`f;498p7da*fT`G^Q+|k^fb{#z#Dx&cP4LHGigw=2k>hu#=XwIjPviu`LcN z*w#(jl+=8Ipi8P@_gkdRkm3Nr$+<@P_1>4sL#S}=EB})N5|*Q@*8DxbzA^O(q_!0X z*fw|Tc30EiNQKb(nivYn!I?~Sp0;(-Kz7nq2eej#I!BzaVrJ-gjzUuFh=`q01zX#s z2XqwDdiwy)PDs|%oC6pUVLnihFFiR2@b&tjB^X4`0gSL~rXbV=;!6l%MBs$n5(qCP zfWvoz0#EHPA%GDF=Yp0%aA^UwzQQZ;UHv5lFv9aLVCnly48Ut$&=CkNHGqCkG+XOg zS^y(<$`EV$DJcN6C7St3*HK98UE}7OfU)~2Er1cW)BFQXAnYmuXqRB-`d?ZA#wD1! z{+AYja~|jkgk2?o!6o<(KyW;YK+x3!czZ^m2?Sj&0KF8e2?Sj&fMzMSArN%60N!>+ z8v;R>4uE|TR$>5VDYnD_*vIr1KtmuXb^rkPdF8hf^dJzlN9PWrCm27K&@2V!5Wu%@ z8*73wtOmd)&czr9A1d&aq_;B!(gzxR*dK9v2Zmrg>QfZ+)3BMrFS<G)ea89EoW^o% z?y>Q<cpp+HK6{rK_6IUc#y$v_dhDG`1Y@hVH;8Xq(<M^Rflrr+99q#&4v6<X(3HBJ zrzmsigw|;MJYGfWi%+cy=@JWx$rr*(QtDHno1Zi=35A@&kdo9U*66JNGjc9NOPZ}g zE+DaEs`o_og2u(<-WRPNNWb{>`l3ohU|uW^aCaOEGAn<x>HLIJ%FvQe*sX{8)A+rR z4&@U7z<Ee7cOKfuQUroaqdwq2fcq|p$)H_aMIgKs>O;;ikkbw+E5CY%QDR4wl2JbF z%Km;0GfjZsPV@wW$&EeYJ+GiXz|F|D75G=s1Ov;3O_0ML$nCME1UI*vW8iL0*V!Us z-QPkkudt+K=o7ghuzu7@iXuPcaCb((=8OHh62mv8FKhk`rZ<jLlHx~vmbg-P)FI)7 zdjtmZ=A6wZNX?(2A#c(d9B|?aH#Vf-y^qZo2qxPQ<i3?X|8Nj>$=O4VJfAOj$ml0* zeV~y?5XBk)3C25Ozy*x|L<L)4B^PlWZ0md}<6Y3OMs!GXrQfFS;DO&jdW0L^Pygcy x8*<j&j5HK%FwI1>;LpSc*68^~+#5Hx@gEPy`5mNRh?f8W002ovPDHLkV1iQt4c7nw diff --git a/packages/backend/assets/notification-badges/comments.png b/packages/backend/assets/notification-badges/comments.png deleted file mode 100644 index bc8a1c35b4423bc08af88c3c72d4d29c6e81c6bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1134 zcmV-!1d;oRP)<h;3K|Lk000e1NJLTq003YB003YJ1^@s6;+S_h000CvNkl<Zc%1E> z>yfK4428cED1j1E0wtsbN=OHkuq9B!mOu%VKnawv_lMzz&0?N@2%|G|e(X#(v5$o; z%ZdUDg+ifFC=?2XLZMJ7!XX24-~l{=V)#3YHlhTPVT64F4bTD|&;vg!?1$HW17Da$ z(#2W9D0qbtdyCL}*#R|XQFTEEWEjyq+zY<OZ_J|K5(UOR_^|Z7Y=P3}WF?`%OgOga zHdfwd9vK<N;$sWE^@(`+ks%HW;`sQp{PLhhP-K{mF1C<o*mKd1I>W^8u?2L+!l^o* zk#kL#v&Pb$ME>v~P4P#*+)3pgIWpq<TRsV?`TRAC3sWoNixQHZ8`<2T;2y5$liv=s zJB<w4sRiGBo|o_(D&mU}ll^%K&!Ho}2(cM$Lw;@?VCt;T3V9S)&-1useoo5$$hl|a zV`K0Wa_L~uDKU6R`f@bzR2V!OsvQkH71|b{!d&b)kCb!+{>!;im#L>{aky7>0wyet z#f(n2Q2q2mXaw|^b7_gCvr@;2)b^e8KQ<OKI+Yv#ujRlwq7g7*X`B;rPqbLBX#NYe zfC)>YEehHju5T`dxk`~$D7F@zjzo*ilJFxm5%-(6lW=$N{WdLvMnZ4SML^lf_q07# z|4|E=xCHjhAHrOCiNzdKAz<<nSXuOOl`UAj_SEyheE6B+)<s)$NreCbV$$X!O2?nG zxF6g#G1p_aOdp=X>WXp!V#J^=q2?<@?`#X;^FyW5=6ZbBNiA;3wE$s4u*3c}SKk?e z)vM1=fIvRkW4FQ(USRR+GbioS@sjwi!_FmI-B8L2x{l?XeaN}bW=wO^wf?mWlh73+ zv-?1*XMu+0z7jFYbN(F4Qmhf`ST8+~HSb>=aZXyUr^oo%wQed^<Z%LNd=Y!fhHV8@ zvBm%Y1RUA<r;~gU|J=|If^HFBiovy`czY(fWL;0fpQ0P~HEt@A<7yjNx7<<mz1^vv zyt`((8gM_n-;wrZ4`f*Vlr_Bl;5fN<7-%&N+Eq)iC&v0J%z`$U#h)6$*duNn!`LgX z!rWIJ8B2yNtDjQzxAo6rY@sIkrZ4w3`IvYhr-j-QS1Tp1gt1d&gM8sfRz8FXg=uTd zD~UeRR{kOmpPh9p{PiNemGu|Aqn!p5?bPh=tb0X@v3_YWc_@krJu~8`FG90+qMSn% z#1|zLTZFitPu!slW~uN?1X8>R`dk3<NVyQ;2SJ<f5=!C=b?c`9((YGUIn>E3GSV%N z6ulXLq9nbip>ruvC{)B3&LwNN@`Qtg%m0zhFrK3!bo4fvXHXKS#X^s<-Z?+McOk?d z&bFe&Z(^^*oKXt4r4mZE8^>ir)2$dU{=Yc(gmk<M0bZQ5&}<mb(pZwZmb77dsZ;P? zq+;NMa6g|oj$`_>jS7WAp-?Ck3WY+UP}~Ur0DhV)Yg-p@t^fc407*qoM6N<$f)7F> A<^TWy diff --git a/packages/backend/assets/notification-badges/id-card-alt.png b/packages/backend/assets/notification-badges/id-card-alt.png deleted file mode 100644 index 67e1410e34cb2a6ec1a17ebd89dc15fdfff87205..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 844 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U>5dtaSW-L^LFmT-qi*IEz4QF z7PL%p;M8msy42_mV`?-CRoLg;bL9{)Y>V9d+f#Y&BIh&jCdjn$vP-lj0@0cPrV7Rk zhCTHcv~#juf)1G1G8o8B*nR6qY{kQf$GR=dQ!MJ9oDYqAVP732_jhvK^sot*`Ahg0 z?A_9IW+TIi2!<3L1|u;B$*YVu5BzUNG00vrPg<wCj_1LfU%xiwx65WvlzJ)daEp0H zcB$<pp)(mg2`nFG&bCN?n#q{+`oHzUBVXmM#Tvpn3;OHcGFmizznRa-;=h;G;qn2q z1-1==EsWd_aSWvy5nh(u3H1!&PV49AHSp}M+r?<~<=0EL6Bk}DmsW6IKR?%D_Y1Zy z-Ff&Z1vlnDSK`?1lP`0BSfbRx|L5#1rqecu-*2eBr`Nz(SCh8(WATl}X4|<IG|Nl; zo65dUQg2@=1M@feuihdP{(o=GOYr`>UN(M#P=Nb1#wCB8SgU{OpJq7K;kG`IV}<%^ zhFvSn@;?g(gqty%6x!Z9!ns0wHp8nGV)?<`AlA8xb<VmA^sX{`#L8ag)p^<T{)-vY zt5a$XitCQg_SmcAXue_DjMw(rAxy7M>NVIcx(Kv%H=BU$few?`jOq*ea+oCw-z<K? z_^y0GzTkn59hWajeVRSrmqGbYq|eU9A6Zl4|DG{x@Z|lo_r7M2(w`W`v(0h345^Ex zFX-)Jd<&;$WH;<Dxo}|q|LZ@hJ09rY-n~9|_J>(Vj8%3uzCZSMW?gmLGpir%Ra<^= zzuuW#o%x;h1>3O;CzP2Z4EG3Ep0{gYyKY|Z<iwFR+x{S5<F?0(i|4OQINyJ%?2V9# zzNqOGR)wc8<|pjq*08os{_;UXUV|&4WzGMrkcRCZi&-xmj6a+n#KM0@+G30BY#jzc zkgGN-S4&-CENTxkUBUX~+}Z32&I`m7+-6tR#PY6Sk+`%>J?#pUPv(V^=lK(3=B=Nw z+U|yBPRzmm%2G?}`}UjpeqSv=p_KK80ncHBlF$6IQhO)62ZpQ%<`M=^S3j3^P6<r_ DXhU;v diff --git a/packages/backend/assets/notification-badges/plus.png b/packages/backend/assets/notification-badges/plus.png deleted file mode 100644 index 05362c122bd2162d5c5ed95c7e8f2d40d9e7eff4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 507 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U_9jM;uumf=k48txwjnz94=}) zs_syken99-qgNoOR-$(Rr;elQ4faWPGaJgLKZxi3$NX%X#N?k=5^iRl6I47e-Pou6 zNKo*6$B&ycEB~4bZ%*=?9?`d}{L9|Tb^F8TM)mFYW&Gxw+V$;mzVX~W#ag!W|K-Xp z&!2lj<}YI$koI{0aeJUL^8p?PV}>&f2Bb1JR6Q?j*?dNZ-A*X8R_3A3<-Z<B9)I^= zx$jq&RKiak<Ivc;r_2BE$gEN^;@?m)4M;ob&&^$Rkvn~M)|W#!mr3m{*tXJ5e-2dW zGlO^f%w_q`@0oSxrFuPmtDN+cQ)6$%w4I^%{`pHL{PdqMBx^6Yw?(k3|9@!pm;W)> z^}k6y;5YrX=UQ~E@<Drv(kp>iPLS>mnCG9pu;2H8Vsv};7j_L$APM%n<N;})&VBy> zkN+1fjC!p+QHb5P@4}<G?n*1=+&w0#JpIaT{mt&>^TzTwz({5AboFyt=akR{03trk AK>z>% diff --git a/packages/backend/assets/notification-badges/poll-h.png b/packages/backend/assets/notification-badges/poll-h.png deleted file mode 100644 index 3b7ded66597679a8d19ef9eccc09834fa7a4f222..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 689 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V5;zRaSW-L^LDOd;o|@S7wIMi zjwVGN7Uu|#!~~8;Ale|nbWngr(bA)R&8e-?w)K<d*<W4%D%SY#a^d4$bA%Fl439}9 z|2wbGV9xNK!Hhv(O#ZmuJ;psuJ6xv!)}Qvcdo{mR_xg>d_RpCf1U?AZyfd->s6^7i ze~XveomrFj=hXjW`qq#4U5vKhA;xk4sP@ikhTpqoOF!t{J9(G+Lw5bLh4)$enGWuj z&~lL7HQVIXfxAlr+25)4S{0aQUTNIzxtQ;cuT<%Syh|&X%O|_Z?wD;7*=)|B|6`f8 ztNOL%X;n9ua_w+!;}6=%GHcDj?35o0-UivHnL<{Gzhmmsv~J+~dMMVxFqJ)EVcZUG zjb(2bKuYSQe;u8E;)7$2V%ewVUJ(p`+|KgzU4~nL;fBekKh)oMq;6FDAJwFm{Xy4g z564W^3n{#D%7?or{8Z<ay7?e!2ZN!WLl3vWW2PfO=4J5>Zf)WVkKPVAD!Wl9DyHLX z#G&##k&9329AFE;>b(?Rxt+?}7Cl>jXPMRmG0D0`&YVD>UO(|cSkykp+LtkMh4>35 zweYeBI#;+$7$<967l^%L-PLe%s%%9skNv~#3z^gI^n7f+vEZVR*8lU@Ls)MxVE6+w zEM?c(X?s3y|MBFfLOK5d{=(BHUktW;H$I5vu|2_iV|H-+{JYkBzv}CJ_<Y3ePM<8_ z!{axPO_#5GS$S;lfrV)w`Gxj<o?m!0?#SW;irPD6tt*{BmG1>57axB+#(+&H7xe92 SI188*89ZJ6T-G@yGywp^oij%O diff --git a/packages/backend/assets/notification-badges/quote-right.png b/packages/backend/assets/notification-badges/quote-right.png deleted file mode 100644 index 0fa483765481dd43d88a470c45614c6a782c748b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 772 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U^?yT;uumf=k46Y!rKl4ZuT5a z2^?(-9Bl#|i9iyF0yx?dIGQH>n~-+t<BqN0N~){%@0xG^f9Yj)+|fdj$Z3;QJV7Ym z&2#dS-Ai0AP5-QO`+w=wl7I7ex4rG&d-UGf(*8A!F4rS3EoMGx-#_o;UFF@(F4q@) zjaH3%H}#F6L9A^}^}pOaYxy_f3cK`VPwbi9{WNfSsjqCup4m6Q-Qd1@^>Oi&&r|;B z94N8168IRwuuMBD>3u}!x4U;mbtiB)^i0jTAH$Gxh_#4u!bYLDv$-35b~xxU7=<mW zEoPj+DSm?0;h5K!e4xlfrkxBYfEv%TI;h1ch%-oPU)%>2Y2kjtq>!w7%UqmcW<ev+ zs5LIX-@VKF=+@f7ny`yw`iH4H441EdR=wjc*6`A*T;btHhLG!PnJ3=jb|~e2^*`&8 zP(bY7y))MyW10|K%pPPa-tcBu&LLY_OVb}KWB;=#XR%yv6*O&nd8k9`U`e7&Z^ABv zMQ#SM5`oGy^m<kZ`iQ$-;aJ9vU70}aa<57J`GLynj=PR|Eh-1H7AwRCt6sbr4b=2H z&~wYT-kAQT7pp3-A37O)g=5j`Z@j_YdC@N!mu0Wh*qQUtw&BIEH5xycCYPUE>-jyl zxkm4AN8K;SlHN}{r7AD}^7a>5Z1qNJTfNPz--iXVxZQ02mfZfjux!4?N1Mg2K=ZUe zy-GU2SpH(@@8$bS_t<%}T^6mJQt_{=?`2oj<k{b~<$c1h1U~w^Oa8K`;>|~E)R(Q^ z`Xojvl405ErB7nt`t3;E73DS0OO^XT$<_&bR{mX?<}-c!lai1P0zT8HKUnnD#z$m^ z9{<6}jmxL2285rzym9}N-<Ai(3}P=k?mQa3K_F!M*Nn(5`<Q+N*w=g1v#xsm*|Xwe uRpO=E$-DC6jonVjK3HC+3Qm9wKX|7khxaGNYvce^H-o3EpUXO@geCwhN@g?w diff --git a/packages/backend/assets/notification-badges/reply.png b/packages/backend/assets/notification-badges/reply.png deleted file mode 100644 index 77021f71a7af666a33db652aaaf997508200c769..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 930 zcmV;T16}-yP)<h;3K|Lk000e1NJLTq003YB003YJ1^@s6;+S_h000ANNkl<Zc%1E> zO_i)L5QVP<Bp{(BL;@0!fCD771SGTsB(#J`K*G!-@HVd}ou5i7!2Qm-yIEA~hDs%Y z9)J)+2qA>%(gBzNya8AMF94%vKzE{pOZd+Ry%giR1rN9x|0(&0X0_-pjsdX2pZuTw zZ-_7x!jgZ82r?l&@iah?4#0x=<kJ8Fe4P#rC`?GFLjwvSk$f7E$bh&`hXx1|+h)=L zAvyq0$R(ZzM3CF*(0~j&04vHQp9XwEs?(tX2^h#Owlp9D=}w0RL{OsBp#cHp9H`}J zg1e#|;Wl_?*JD?|$JP}Ybt9B)Go@wsSQoe(wGqy+UN$B5sL|=v)(ftb4ru(zF{roL zYHEWsW!}f!1T{OI7Nz=nOv=^aRkgvDt3D^8p=fnFXHn6o;!K^6wk%wg5aWuSP7S@` zI;kv}{Cq9ZS>KVoI=s8yaFaQhp2;tw;F81zx17ch18f6<URhsFpaJFOEk{cO0bX4j zJUi&VfdKEI^)CYeck-tl;2q@MH9r~%@apoGqQeB+K%iIF;xhnnhjH%)0==rfJ_QHd zpq0}cVgOM8)swp@>Z?!Qij`3x;Q`y!0-9qC09=Q}Q&Tx5eSEIjAsuhM^}8N$Pv!u3 z;5EU`ss(P>X%&+jGo~a~g<x|S;0!v<(N6p!lQXEZznnYlW6&(C*%$hA#b8S^K<89= zkqF*Y3bqEd--lYI0<X~_)#QOXYsFhK=pJP|Bn{@s7-MCn11|Zcq!z{#Q?82r5Y<v3 zSWO1VkEf^sfc)dDjr4e`j7xInw1CCQ3;^V=I$OX%Hu<(z;8*&=CdsEjv<>}6?m=EF zjF2;6tsA-{^{TTKI>;IDtQ`A4QV#N3LTbaA{ZU5_u~(ffA$1bmSC0EX48j7nO7Z>0 zjQr$RU4VUr9OTVv(S1f@f1_6aKjG_;UTE)U1SAYFwSx{}f-5&0T#GiLz2pP=1~gNC z&Wb^v0j-tP(=j4F`5W4Ns!v7E+k%|^0;KK(IQxZ2Jx<~57s4Vw`4*i0BBUP1sh;~> ziIfW_{x38^Vmlx%yu!pRz#CY4#EDse+4Tq$Q~wvG7%xp|{a?YlUxbkQ&*T^6Tm5J9 z3*$532`0Zd2A^9^(ccawenz-bbKKgo&nJWsLI{cG7r>uR1s%&lUjP6A07*qoM6N<$ Ef-jw?4FCWD diff --git a/packages/backend/assets/notification-badges/retweet.png b/packages/backend/assets/notification-badges/retweet.png deleted file mode 100644 index dc6106048107a8459bd6efc921835d9bf2e25d9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 798 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V0!B5;uumf=k4s=jh_qzTKid4 z6c!3;Efkv2z^oF(Ikf@IpK#%~@^<6e%HJDjr)=h(lb-hfV9qz2DgBceS_GUp6p;x@ z-iB+{g7-a`AMhp2xq7wKHQt3e;lTcf!A^?X4lQR%R(TlQBslAXrlPUo!{r_sj9(by zCfe#||A>w@3_8?5@xr4S2lYD*)ZKqXaqj#zRpavZ8*?6Ji@lLuwXBZivrD4jK9@r$ zH>`L4y2+w6qD_Bdf#JK_3`W&M(>*Fgq7E-)nZolwSS3aH0Z^UO`z^~BG1(lR?(u@* z&c8O@NLLma#hOkQ{xhs|7SzwL)tskXD|&Xdx1@&gGbXFV$(28N5B2k(e9XAFrTUb^ zOhfSkQL~wc=Eu#IVr+P)B^|_e>P+IghRPoeaSRI6KZJ%eY>+=w{lj4n149K@)z+2F zsY^N<gs(MMD&7&RP!DtP(f9A-V3^i?lS>7{;heEUsioTa$F|LGT@9V5*Ic(uJUdN9 zTtT(HL23a*y2b%Xu7WnEISbL44Z??Xr%QkPU+XQp;N{XqVT`)}?tl61!jaV}!Z>%+ z^e!pZEfMz%|Hf4Mt!daBcTy-zxZv2ovhS*#hD=h9>)l^DY~`%5KdH&<rtKi~FJ$_Z z#w_6t-@{+bV(De@{h@q_z1Re(z(MXzOjF1K$zNx4!745|iJ#;6*;D@Fu>6D{jxn<t z6k<L_af)r-a@wUg<beOfU5VAv-P>Dq9x@j(#$C-{bJyEx+lR)-+!7oA#_rzIvgRRk zDThT@%bSPHUpatcrRSrXJbyUEGR0YMj_@?#mS5q0pyTh{@|~(P+W24amsMO-n4>wJ z<?{!1lked+-okFHT9O_zKU`iie@DXVS?4w$mHE{@HNfPLzq@whLurMn2R`uLoP6K| z_Xf4bhvHBETD%v&t=`cgDC_hga@#cY6#41qA12wRZPvS3xb1=Ip25@A&t;ucLK6TV C7*n4B diff --git a/packages/backend/assets/notification-badges/satellite.png b/packages/backend/assets/notification-badges/satellite.png deleted file mode 100644 index 0e1831e8a04a1c92fbcba2d48ee62a94c4ad0a3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1743 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 diff --git a/packages/backend/assets/notification-badges/user-plus.png b/packages/backend/assets/notification-badges/user-plus.png deleted file mode 100644 index 9d376d04d666f856fc927a82cf78ddadc4629e8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 991 zcmV<510ei~P)<h;3K|Lk000e1NJLTq003kF003kN1^@s6aN?Cz000A~Nkl<Zc%1E> zYq6_95QJL-5|Ds|NH`LZ5C=$z1SCWP5|DrdB;@{BwnF6sS=gQJ3Cvg3Rrzs}Gc(P; zcr1Wn7=~dOhG7_nVHidM;11vcpaakY7$@}ro&ehEZ%R&x=cfSB;9dOJ4t3LajRbfj z_L{J!N8a$y#SOrKG)Bxd0{F|9#yPknU8`7=bz=b(a09ZW5uf9Wu|ZBV%#<(Ok>3oj zdxrdeia*GY8Q)|B{X_w`zF?a1f;%7#xY-Wd46jBwSM?O&cU7OQq?AlpJDOUDwGy}~ zC)<FfR&q&7@Qc5uQY>r5FxMU5bjfu|3x46(^c2inEl=<Zzot$wZ?$Mx46f;t1N#7e z;mwd4nihmhlBC9Z4>5o=snFbI4T5=Bfic_!V_2jM42+>(Fz?Xmej^yeB9Fkp7-|Lc zZUSS-Zb%&%7(<f9P-{pL7#Ks9VBTsC0d!7%CC3jyxgi?I5$QsxHbh&umE!`HYD4w} zc!t&i{D2_^bc`kA6pJ+K4FTM$lx;{YoTKL_3<0<UeC%EEH@Fpe6QC|IrY`XJ8VF9) z8P`Z!#C26UZVV4m9g)K(IZf#hKW+>e<K2?zC4(kI0uQ7a#Mws7CCUvlUBQU7&6{mP zHbXqA?oJuuT3+g@E!%|bh5+wp4Tl3k_S1Juq1f0aPz;$<!L^zv+@S1D8mG_x(dY)U zXKg)f6X=F$<4CKO&?RroSfU2sj29HF>@`s#d{x~ctiaul-^|f9pOyx8#H&KBA-<V^ zZXz=e>WOrVxB_mm*?#;VKRffA)<|~&stoZa9Jxu(o3m$Ow`M|!dboq7Jd)@JDhw&$ zl1q6i<nyS0OrO_UTZ~c~Lp4Og2&Pi(CPXu&fbM5Nj;cr8`7nwh74)oNB{bmpz@Oca z3g_vfwlw0HDOnBCvQ0)#Q}%XW*$g@BnDI^B&N^j#J|xunGivYbYwN~{5ec54(za&_ zLuG6cQ=;*v>D+3gtC)xq);he(s7fZH0lyGh)JE1a2?O|r&?5R})X*v1l~l$QG#1S} z`mjXZNvK^3>dK<-By`1<`8Rb@6^&$A3F^wC4(w)#GDFzOYZVQ4;#x(6ow!!fU?;9s zG}wu26%BUcT1A7MxK`1)(hUuvlHT5F6T4FQzuKsZMsiyS=<1@sI>}BjzNKQ%crDv2 zv8C$mJh@*8sZAXenK7q^8|cqI`<lNhyiQOz!!QiPFbu;m48t%^$3I+7t|l<5&T9Yw N002ovPDHLkV1n>o#FPL4 diff --git a/packages/backend/assets/tabler-badges/LICENSE b/packages/backend/assets/tabler-badges/LICENSE new file mode 100644 index 0000000000..cab2551f67 --- /dev/null +++ b/packages/backend/assets/tabler-badges/LICENSE @@ -0,0 +1,24 @@ +Tabler Icons +https://github.com/tabler/tabler-icons/blob/master/LICENSE +==== +MIT License + +Copyright (c) 2020-2022 Paweł Kuna + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/backend/assets/tabler-badges/antenna.png b/packages/backend/assets/tabler-badges/antenna.png new file mode 100644 index 0000000000000000000000000000000000000000..013c7f4e61cec955ccd453551b9c056ed9ed901b GIT binary patch literal 516 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgI14-?iy0WWg+Z8+Vb&Z81_s7$ zo-U3d6}R5bcI-Q3z~gFr*GOpZfjbF#eMh!BBr)FIDB0yaalwMX+ec3JAKkuGC@JIf zoKv2U-CCR$255+Qnf(kZ)$V=udATvax%p`?(?6>|K6@DOSv&gX>b$Kg8Z8$5{AcYw z^u@2nc?+~u@V9?*ldhX)e|7UTvz-h@6&8QqN0*sr*W16_;<iRPNRPigZQWA~CW|az z5r#vD(-<`P`M+I0Q5VJ6@Oc9#!==M)3=#a&j1O$)@HBh_3T$ee$#APWMM^2?cUj|| zZ$}v{_@%!d_`x#w&x?)=S%0JYwf|ld-Kx&WqyBQsvxRpj>u$fx!m!efVY+zjejh#S z>W1wC`3wE|7r$dSS{}^#eX`~A%bC~tP2S0C=E}WV$RKuFE1}^^>)MxWY4J0=<D=HF zDU=nK^EpgBdiRFhtZxYipMISoH;X;Mu}Qg1FEq4EQ*6O2mHT%zf^uh-dP=M@x|l0^ zJaN5L1fR=<>9#)ZL1$jGvM4YxayT$B2{bUEF#{M{gIrjCurthh>?K~%cbaWMQ*X{o z4$&M{AIBAS#?MWfn%{1@EO_!`TN+nuki)udZl8DiUdh}Aj8+CuS3j3^P6<r_*}l#y literal 0 HcmV?d00001 diff --git a/packages/backend/assets/tabler-badges/arrow-back-up.png b/packages/backend/assets/tabler-badges/arrow-back-up.png new file mode 100644 index 0000000000000000000000000000000000000000..a253384c72bce2068c2b4518b7403b7dcce4cdaf GIT binary patch literal 952 zcmV;p14sOcP)<h;3K|Lk000e1NJLTq003YB003YJ1ONa4NRhv@00009a7bBm000XU z000XU0RWnu7ytkRYDq*vRCwC$TD@))K@k4V4g^FjK#G8xA{F!`5NL^j8jz9)2|Xmx zp(H#2X^<lso&#PWHU(4!u=0xn0mo-%_RiMXJ{yahU2esEBkP=4zMJ`Gc6WXZC=?2X zLZMJ7{(A%@-l9N~yn3fTbAN_}{otJmWAwU$3O<um=pFnrfz`i`9kdXVv{3iU1XlkB zc1iJHXp$V!yRtt9MC`NrO?tJA7h2fDgjow7v40$^@09`l?6Wj{386IhBU840Sj6e< z^ZPyA^h5o0_V+Oxf9@&!xBO6F*;n?huz$kT16kxBu+Qi24yJ`R<af=#qU_|2yu` zXOlRaNlG}Kpr9yyjucYakK~9lp92b_Y*NQZyrUH45oT~IQ6h<{f0Yu1ZuafTdo1Cy zNd}8@29!;{pZ>s5M4IH?21ytP<_laj!Iaq#e1i!JlPKa{NR!N9Uw_2|2@~8K5P7v> zp0{S!LXbU#HU@@JC@8(2;3vMJ{V8K{<Z2POLnlcNn^4K_g<;$150PzjA3x*!9rqZ) zCajvZVCf(`#GJrEHi;?R#$#-nyLQvWe=W$Eke*6x<C<6vc9M3UT3lHtv#l*$%HAgU zQyG~pnJbt~h-oNcy@w_?XC=E2hNaVc^nZzTI*5&U+SjW2MnNXv=fPgsHC`AKIr*9C zmt7{X50)i-;KB<L&*xE1(8Sjo#%0=NF1*0^nC}at8L7>}#aaNR)td>DIEWVI%GO~! z(yRzi9PP`A4VAC2WLANj)*>r<H$f0T<!#417QMhn|Av48{QW62Z3sKme8`^IxofWn zCP6vq7!aBYp(Ne5w>RK$WI+0e$(s;LdO>=~$BX!$-9_AOg7lcx`Zn6@aM?k3bdXi; z9b{=HNdDA-)m*TbN(|;L5F&$<xmr&$SCvZUY$Hmi*Zb1xC3`DC(uYw&^UPL3BOh|3 zmXy!)#((-^AIHPgqW+WVlm>ZHKiYQBCE6-;6y?<cZI$_a@n7g1e4{nCR`RHRy{}zt zv9%~~K-tW8nj<uKU)xFGY`5#%?e*2Xu|J9d?NIuH`w*GYxCx=|5z`^oa3P=S4_?G= zso{z2kk7?7TBmA@$E=H(mLIdaILIsyx?QCb7Jq7BKbiCThvN!`LZMJ76bgkxp-?FP a62Ae_OM*7Fm(cV80000<MNUMnLSTY&8N&wv literal 0 HcmV?d00001 diff --git a/packages/backend/assets/tabler-badges/at.png b/packages/backend/assets/tabler-badges/at.png new file mode 100644 index 0000000000000000000000000000000000000000..cbf8df4925a5211183e17e6d4f64e46d75300509 GIT binary patch literal 2909 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 Hu0mjfQIC$? literal 0 HcmV?d00001 diff --git a/packages/backend/assets/tabler-badges/chart-arrows.png b/packages/backend/assets/tabler-badges/chart-arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..b2b8a2d99367e27fc9eeb5a7ccd964a3a150b32b GIT binary patch literal 829 zcmV-D1H$}?P)<h;3K|Lk000e1NJLTq003YB003YJ1ONa4NRhv@00009a7bBm000XU z000XU0RWnu7ytkQ?@2^KRCwC$oXt+cKoG~rf+tPn;MF(a?g7xF@l_?^5qup?jDeCE z5rH~wk<X@ScmFd}YIh?vP3Z2g`!(~QU5T31q$breU2s0zas$r$-&zS&Zjii#E6(xz z2o$RiDL4LMmqzr)dBj(wgm7AJ%l$F$6w#V<#8)~9$K`%dkMFeI9yH_!%xT4W&R0V< zfR}B#3%;7tLwSU_4_?zac@NR%bIf;0ct$!dH)4p0Ce$yB5I^XKf4C1Y;XC>O5iOHe z7?eY>0@!t-#TFoKg^~y<!1MrMss_;Ip_^7aI_O9x5bOX_gnrWc%ac|pJe(~7(kk{6 zgw+YcV=;tmfX-%>l+Tx3(jXHdEY=FPE%4Wo?vt^wc4>tyfROIkI_dCzoqWr6c&5(` zgaWN_43I*Ed`C0Sct!ZytNadOMo;;6K^8!Da*Xhutj<9>fVD}^Lipjo*Srx~?G`9t z8PpV@=|Z-HxJuJ#Saupc`-Q9)rbH2AuC-AR;%_NqZXjpOp>!lMb2=XYr03*1Gy^$@ z2BqiZxs)Ze2{!<Mo@3?Iqx2lGtl|UURxnuDa_K^zO^=ioT?VnYo`aeUT&GcQJtuQI zcF?BX7BavBgjx_?O$l#32Nhy+H|9KzFjSDw$DH;ugONJDyF=rl=STvmxPr0(3eI@7 zTR<_=Q&WI{--V0^F=84Gt)|hlU&tg(3DJE>62{!VS0KI*N!FZJ*oTC6Xb#^#Mn6(? zDf=hY2kZy3a_Uj~k&&zdlTECIEtqg+CoQ7%BQtRqr5~xt>upi}$S}Zx(vJ*7Oep=x zFvy3~kIcfXqGq>Fpj%KsvfXL`a6L6Qdx$vy)|)-N<(HZQG+oGc5Nl4OA$l4;`-Q9) zrUY{HxA%S^+ZHf0r!fGK{XljAp#4C_03;3psXFy|099E<;vk%wuqAgCQAt{qI}EBM z?q=&efzWfsf=*I_2*rceQlTK#qwZ3{IIDj)(4;0csjBH0Uo$Sgqc*{V00000NkvXX Hu0mjfq%C$X literal 0 HcmV?d00001 diff --git a/packages/backend/assets/tabler-badges/circle-check.png b/packages/backend/assets/tabler-badges/circle-check.png new file mode 100644 index 0000000000000000000000000000000000000000..6464d5133cc838edc4f68f9a98e711b0fae44fe5 GIT binary patch literal 2307 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 literal 0 HcmV?d00001 diff --git a/packages/backend/assets/tabler-badges/messages.png b/packages/backend/assets/tabler-badges/messages.png new file mode 100644 index 0000000000000000000000000000000000000000..fa5072ebba50da94e0ca42d23f5756579ec610ab GIT binary patch literal 1056 zcmV+*1mF9KP)<h;3K|Lk000e1NJLTq003YB003YJ1ONa4NRhv@00009a7bBm000XU z000XU0RWnu7ytkR(n&-?RCwC$o6SxeF%W<!8)y!R#3If-K{)`YRwZrKWBYDR;4zBS zQ+bG1eT@QA1W7oN9|y+XpKNyRjd$0x@v649IV8$>KL3pEnOOi=Tye#fOlcXewH6QB z#$~oqYE3=wg%DjhFjTv-%vL?>pTQOUg<F^ys<$ZZ63*!nxbS@#!UA4kg^#()r7f{r zI$<kh>c4>-(bNJThRvF(Oi1M2a)T|9A@GGe#75?Zdw)F&bNn2JEfH8Az*#yMJ@!IW zR|4WZY>lY@7pVqK)U`w|eLyh=E%E$8@r<nhUqb1>;}#7A^w8WTt<MUkMvUk*A(I3A z!+n7S-~bzo0Qlz=j<G4f(iY~UgkzMHlsm!ii2%mN8<KU*m=0i)0`SAVvwY>&#?Q#D z4S;3@9DoCG01m(bH~<IW035*o1EA-rv=l(?<MdK~0Sf`tK2ATplX^A-<oh`9;U}DI zcxNmF2%LmhuvGXs`FEoY1k+c_N&ssg=R)D*Wbll?{>T8ByVOT(tQ3HbkCXp$2fbL3 zNw-RJjg<h<^Kr@m%sIBAHP$WzpyT6|Pa$)zMXu%}^hLCxAb_4C*+mSnhtI%}bC(H6 z`UE}-$%O&7Plc=*h0yHIjJemDQ1uf^Jt_*YZ86F=vS{)R3<fxuX!|{M;HfCUc5~=S zLJWcV{Z%={w2ANl-_TrC8Ufjcl(&OiRtoZE5ym(%ldG=;fc}OQOsrFcC>Lf8lx0dF za)4@q?n9jKpV5G(5o!g<KZfgs3j1OWE|mtn1_1R%XnW=R$p?YJb_-CxaD)rRC^P~9 zaz?=l0G;d~)hO&7z>95^S85ct2Ki_Kf@GPl;wYjs3Ofbh3ZLLH@LqZt)EkBLJ)}l} zK>bm?w~n+Cf>D?wL~swe&ifxNz%@#H)>9-k3Xi(rl<tvU>QtFK6;cS=Q;*iaGcK7j zP+;-f2~fcN+UzFRN`G~J{w4r4I&H=)nJ>8G#HFaA$ej7Mr;f=1)*DL0lJ^3D{lu1T zdHSRrAbF@Vlfe2tJSNlta1^W0jHM9I;0y>-h-Cmz(1Ca2yQRS-ZjuCIIlvu!73X+X z*>TQ7uPI>~eHwt*ndD}=st~$Zz>w}O02zSHKgprWzDjZmgs+mQ0MdbnFSsraZG4$W z4j_x0l(iqkuT$j%1TmAou-}%x(3S_V?)^Qmwx|ID_4{_KuLfXo*wbqDvzPDe+=u1? zESoU*RSu{qPI%D_^|LO(c~j=T+6nuB*O&&WpLK)3ZJPSU0}+-Y=!Sx<wn!~-#T8dv ap)aqtud1s^8Kjp00000<MNUMnLSTaM_2>Hl literal 0 HcmV?d00001 diff --git a/packages/backend/assets/notification-badges/null.png b/packages/backend/assets/tabler-badges/null.png similarity index 100% rename from packages/backend/assets/notification-badges/null.png rename to packages/backend/assets/tabler-badges/null.png diff --git a/packages/backend/assets/tabler-badges/plus.png b/packages/backend/assets/tabler-badges/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..f13a86f4cdfad66e7b19b22d411d86335cf21e3e GIT binary patch literal 414 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgI14-?iy0WWg+Z8+Vb&Z81_s6e zPZ!6Kid%1IU*u{s5NY$jC-#K<3!81F<NDAGtyR~3Z$$>5QYqLmW%IuO?NW=M)SO%P zH%Ey>u|>d%qxPa#ZrIFUfA7Zpe0OL4)jS!a>Zjdro~np{^^8v9J~+L8s#{j+V%=np zbypUP)lJ_0YuVbxsto0C^Pl{&`S1I?>YCg}Bjue&+k?I^aJ^+rh-=uy9>4^2Co=f3 zo?$h^?jI`h3{M_i-u1C>#;26-9)~49TTfdQ>n6@A{`<vmm7K#;+l9wFCav6Obk68R zXmri<sOm#~0;<8a=PhQ>lvNEbjknz*nJ)Vx(J}Gc%vXtA2hxx2JR;$9D&#EljQuS_ zKLlsbcN5+*QT2ny!~=(A{<9-H0K}dl+7Q}(nrTMdkF2_vg-qT1&+700&auqgp@QXd nbigjjlONO+KmnroCE1oiRCd9k^K*jqK^i?>{an^LB{Ts5&U~dD literal 0 HcmV?d00001 diff --git a/packages/backend/assets/tabler-badges/quote.png b/packages/backend/assets/tabler-badges/quote.png new file mode 100644 index 0000000000000000000000000000000000000000..e0fc6f3fb42884f7a7a1f94b5706a4b2a2ea368b GIT binary patch literal 1011 zcmV<P0}T9$P)<h;3K|Lk000e1NJLTq003YB003YJ1ONa4NRhv@00009a7bBm000XU z000XU0RWnu7ytkRrAb6VRCwC$n!RolK@i8su>(Q|5)^0<k*JUm2tf*J=;#nnK!rpD z67U2_G*osdsHp>@B1qJP!iX23L@042knHdwh~u-F-8<Xo^VzpM{^yd2nbp}+y!V@( z+1<I>xe%$SsHmu@sHmu@sHprGQpjj|bG&4RikW?i+$%4Rr{`xP<z<>1)5`rmg<a=} zhJS^g(I?uV=9sp@zb@0#1l1^x_wb2ksY*N4r4C=?oB6fP!#f<^wL2Wa@DI_dMegtw zFh0Enwuc$sD&LV*jpBHV*>F{cwKBXm@Lr2QtI?f!j(6lCUX3m5nXY5&SmjI6-B^y; z|2qkAS{w8*y(>e$LvaM_zd#=u6FB>=8}+XB2{?|(fTJAN;!&eRTdBcsE7r#-Ugvlo zHvqvA88FAto?`R4MT?A_AA0o6^IsPEmw3M>|2=L1f+IAbkqJy2U}MC9={b-YXtJNe zXXK!nC^0|MZ}JRCI3NSuJeO^NKSvEv1JnRD;6R4a251Hv`(p+UTq+?+!qN;taB@qh z(+ogx1XALw;(ZVqRgB`~a->XCjN%9nl@*V}_&CKVj<Brq#-SWCSuu(uHeuPJAT?Pr ziX$#9US{RvOM))p55<8OcikUAC=R&1?w10fIKUSi{z({$12YTbxg2I-yyAt#+Wk() zMFt=^A_Gp-ufAewMGeIfB^(!H@!cr?EU4|HIHGjpM(pv_=4(w*ltpnwIqUscu4%qk z1Z8m)N3?`qMIP%Zei996FF2wYw4RvyTi%{SaYQ@dcFb=^{ZBgU{b~rs5#>p>m}2w& zUa;{{98a0$>)z05*R0$J-YXZy5yXwF^gFh0!^gkx#G^Q#Q*OL=p1zLw?YY;+Lvg%Q zYvY;gw%QX69YKE3IcLA2I9`4)u~M_Kj?q*5Ha$qZ*?sKfHx$Ry?=w+UI7JWXi;WaX zwKY1D897iK@8KKVNxDfd=$oD9_R!%xu<sNvWllSaBg!~m+ZuCs#AjjD-sV&+YuZs9 z(eOim1w6&cvWS`VKkQSt;ZM&I8L-0ezbO@+z^bj&HHU`|iX$qs*Ha2oJukkaGY(!u zaX<#Nqs_NTcWh0em>Wm9gkIXI52Akw3#*uQj@AqPz?dIrs!bgFZJR#OT{>a?T6B)r zD9#jNbd$aHmLAcCNLf*IaG#zdevp-U6%Ueu!X9$Osf771O|@209C3AYKXFu4R8&+{ hR8&+{R8$VG`~!+s)<=HFD3t&J002ovPDHLkV1i|W+1>yE literal 0 HcmV?d00001 diff --git a/packages/backend/assets/tabler-badges/repeat.png b/packages/backend/assets/tabler-badges/repeat.png new file mode 100644 index 0000000000000000000000000000000000000000..ab548043f73bed11cce7cdad74a8f7bf0ab2fbf3 GIT binary patch literal 1206 zcmV;n1WEgeP)<h;3K|Lk000e1NJLTq003YB003YJ1ONa4NRhv@00009a7bBm000XU z000XU0RWnu7ytkSXh}ptRCwC$Tib3EK@4@aTd4)2zLd)k079Z%A1c%fyns}RcP<|Q zv_De#11NuxO8p1Q3kAd<(8mS{ZIVj8sM&0=XE)K_Qgvs>vpXnbMU4XS`q&=ZV^07^ zYqUmdv^1;=lC{f0ivG1ueA4}SB(j5JpZEYbVFP|4ZQwhAG5f@CKo5MR9_+##e&X5% zpoW4ULl@};-?d=T9th<77oaZm-xJiuR}<!Kfsh8!15a3C!4?R87djaL2&`ZYgkTfz zhGO?(j${#pc@#Hrp12h(+0G>5oPf|xL3qdop`w+>k>f%pgtl%gR4`Av3}qKN(yZ7E zKA#nwNRpp`DokmmY2>;D2N@9BI<_-up7eeRUcwrDhHXs$EzPu#8Q~}*d@{8;D!QjH z-++%PBb%VEsF7ymA<);(!CS0XNO%uA`09${i-WQtbnvw<3xNFo8vGEN=or_2K=jl) z79Eg3e31C}VMl187~z!wI70jN(1>xCCS8asrCNR$BD9n6H6^|mOZp|=#N<Bj65{(| zuJkZEn+c<yY#62geoHX)6XI{dEBu#fV~MOOcg!UHuf_CFj1S>8OaCUjl<?%wXC&km z_!D7Be-GfXVCITeh>2pZC40Oi{lAFS==5$GKONU@f$VZx@n}h3`Ves-V*Mp-L76(x zG*w$Hn-Qjc{ksg}GaM-^qF~-0u=NFmJ8BhS&O^OYYlQ$*!+u#GKwAVly)MMoRw2ey z$R-M)g`e;P(A3y61%R!!XchoNbJrYztoFLA$2Uy@U>R_j1#tR^Nosax8o)3?rDtbF z0MP1MWdM-g+>T8ElHs+H4?+N-83WKX4q$zd;Ewb5g^C7{EHQGoRg@WM3FAr%R5W2Z z04QvtG9*%^FjkIAptwsI+QPW@8l{Llq_svVM<V5SEmi=ofz8bbAn2m)F^5++0PdiR zb}~qCHG2T0dzf4$1`7Z;qld`}U>f#g02)wLc}{i*xx?f_#P6<nl0Rs7{e%g509o4a z&#pK&F+Fc3(OUkb)AyBTS7&k|2eu;5BTK&rg-OA%{yoR#ykK6lk{Yf&1F0LOr(H&( z9_F~#QIjLaR{&4oFfrWv4VPthK(_XS9_B1lf$hL@Vm3++v?infZ0!fbz9kXpbPrl@ z!yEV}p64fKqY|Madv+Dj5k~)ghC#O%dc2<q)vNjSPvXy{W>@^wNvNwM2J4d`b<JZv zF}pH<)q}*}fvX^O%_BLxGJnM;5dWUc_AMF!2~V#3U1)zywi>Aobs;V%>V_BEe9@6w z6~3=<Pp*hf9N7FX#{KK6XYSL&=rKzeZD7T@i8ar2xF{qT({cXFPGpDhlp&E-MZpZA z@LIC`@KRzA9x&CRuDa&WR?M~Gk@jMarByb(oNoUQ_xl%(Hhmeauxq$)t=S4(hk9dH zFg<$GZUxH#m}@PvTVb!D*_k#7Ma|B%^$o240W*O0KOygzT^9d{f3!wxv`V!80VRT4 U7IWCp`v3p{07*qoM6N<$g1CPtg#Z8m literal 0 HcmV?d00001 diff --git a/packages/backend/assets/tabler-badges/user-plus.png b/packages/backend/assets/tabler-badges/user-plus.png new file mode 100644 index 0000000000000000000000000000000000000000..2ae96f0b7379d9896a6570c91b42c1c08f63b20b GIT binary patch literal 1431 zcmV;I1!($-P)<h;3K|Lk000e1NJLTq003YB003YJ1ONa4NRhv@00009a7bBm000XU z000XU0RWnu7ytkTNl8RORCwC$TFXupK@`0`LlB}yh(y_t7>&5`2NXX*qI-p)-{B*1 z$3tBygL`3PAfn%)A0QwJxIu-5;4AW&8Sky?Vfr;)kM61(vHJ$->FFx&sk&8l>ec}$ zuHq`L|Nim<_K#g<=)j-!CvAF_0&bR|8&0Bi<6RR-shvZHJJ$_Y;2tc%dsv4pv~|3j zhevP?x_n?>dxj0Ubsi>R73wYQBTT^s2DRL7ZBGuJg$n#cH*0A7Xa`Vb_f@>3&ou_( zH@txUL%?n1{s??U_p0b<wPg=9zTJe;_JQEB*srjbPjkLuHNP)r*jVt~yHA7m4LY<J zbG{a^Xix0n@3&YI+(O{3&)u&j220W>4vs@8yX^&v{Tb$d*!cthZWngYcKuHW5fD_W z;@9HL{kzPupj!v5FP2~&uERw*jW&pP<M@QyJ^F4P?zn<L9p4Y1S&KJa+Q*+&xDB1L z_dD_FivLEluj*JsznhUjtyYORL@hdt<bhV(4n`aJC?O_9tMo~ji}V<ko_5g(<jy63 z*$TS<l(l9@LW_Dnh{quaRQd-78QA7XW#5(kkozwMbd6KJ*$@P(Vaj$NQ2TZvvuVaZ z{auD$VN24tz4&Ih<O&?KI}XT=q0sQ979GQ<9m2Bo##k(juG$SEa^hhifC$J3MwUIv z-dhU5r&{hyfzNdWyb1uIzZ24NOm9s^O24u>3Ox4nJ`e`fmN%3D+zj*p)$;DJW-)om ziNLKNx<E1f2UlI$D5kdtu|#7Erdrk{h+WCAzm#>fcx*}9I&^@3??9jQJVI~v!S_)4 zO}6}bsqWLa$I+Ub*IWUdHVS|oz6^`-1#J=UGR;}}gwU=+2yyqf{5?h;?=nG19>g~d z4q|e3fi+VVjYiKxwmwLjMkkF;qXg>&<Gsz0`;1RAtlhNaJcul0OG*pbvcf_}B~HP& z5P)y^Bm;n~RN{^jl`sH+x8jO6nTwZ9BXb<Ub_l?BHUO>Y^sE$}o{Q1x^zmZ^fc}Q& ziSmZV2msz<(+OM3Ipr<ovxN2!o-I(xo)~~0e|6SAgW#L`jl@m;Xu_uc7>p!t>NntQ zq%EUdN45u)IZ`>cH3_CwCSVvtgQr!7G0K-)q*cZu?cd-$J?J909{e<bWF<^p24e4M zaHK|NLO-!J9dp0IdA38g6UqG5@JaX$Esn$^^tE<saWRtM4RW5kqG@s@PV5$QO77CX zp7%LVEn@YQ+W9F2ain>5j>LQ6xpI2F5=*Z~Igd^sXmO<A0Y^>pfa6xe0f#dH4G3p& zNQ#e`e!&czIFrt2oP<#LknbE$pC4Pc(F%ThKIM?l!5|LzL-3e&9=AMe?3i`UKV}`0 zIA*mxjkcm|jeDJgZmSDf>u|~<HLyk{WF}Z$v4;gRXEzsafKDGl0G@)i831)PkV**y zoBFhA^OV5U^+U!0B<q8=1*WbaGLYD6QipVXkjP~9L%F9G)Ad0jQ`ZmWp4^qy2Z>Bx zKa^v7T~!~HmwqUx0}fezkjQlOLpdBV$?Ah-Lq}ad<YmvsiS$9DY1E;9NJbyjCtS$- zT<M2Y^+BS`x_(GrA0#p-`XTjny0}NJ>WB37L2^{kf__L}A0*SW7W6||4)tUX{fz5} zvgm{4de^XiD4RY=rgo$Hp{)8K834NaAq4{WNn6|@Gs1NALrMU$=!0bHSJw}<QCh=z z+N1zb04M+)2>^z2lNRbf8UPIBCau+PA~$Kd{=9RO*6W9yrN0j9hg`+iPU?po<?oK_ l&m%YKy#C@UuHyPvuD=)4f|M;X+^_%u002ovPDHLkV1lkst@r={ literal 0 HcmV?d00001 diff --git a/packages/backend/assets/tabler-badges/users.png b/packages/backend/assets/tabler-badges/users.png new file mode 100644 index 0000000000000000000000000000000000000000..7862963327a034ab20da73c84ca18aea46080375 GIT binary patch literal 1911 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 literal 0 HcmV?d00001 diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts index 4b006e0f5a..6744687fcc 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -3,14 +3,21 @@ */ import { swLang } from '@/scripts/lang'; import { cli } from '@/scripts/operations'; -import { pushNotificationDataMap } from '@/types'; +import { badgeNames, pushNotificationDataMap } from '@/types'; import getUserName from '@/scripts/get-user-name'; import { I18n } from '@/scripts/i18n'; import { getAccountFromId } from '@/scripts/get-account-from-id'; import { char2fileName } from '@/scripts/twemoji-base'; import * as url from '@/scripts/url'; -const iconUrl = (name: string) => `/static-assets/notification-badges/${name}.png`; +const iconUrl = (name: badgeNames) => `/static-assets/tabler-badges/${name}.png`; +/* How to add a new badge: + * 1. Find the icon and download png from https://tabler-icons.io/ + * 2. vips resize ~/Downloads/icon-name.png vipswork.png 0.4; vips scRGB2BW vipswork.png ~/icon-name.png"[compression=9,strip]"; rm vipswork.png; + * 3. mv ~/icon-name.png ~/misskey/packages/backend/assets/tabler-badges/ + * 4. Add 'icon-name' to badgeNames + * 5. Add `badge: iconUrl('icon-name'),` + */ export async function createNotification<K extends keyof pushNotificationDataMap>(data: pushNotificationDataMap[K]) { const n = await composeNotification(data); @@ -75,7 +82,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), { body: data.body.note.text || '', icon: data.body.user.avatarUrl, - badge: iconUrl('reply'), + badge: iconUrl('arrow-back-up'), data, actions: [ { @@ -89,7 +96,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), { body: data.body.note.text || '', icon: data.body.user.avatarUrl, - badge: iconUrl('retweet'), + badge: iconUrl('repeat'), data, actions: [ { @@ -103,7 +110,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), { body: data.body.note.text || '', icon: data.body.user.avatarUrl, - badge: iconUrl('quote-right'), + badge: iconUrl('quote'), data, actions: [ { @@ -171,7 +178,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data case 'pollEnded': return [t('_notification.pollEnded'), { body: data.body.note.text || '', - badge: iconUrl('clipboard-check-solid'), + badge: iconUrl('chart-arrows'), + tag: `poll:${data.body.note.id}`, data, }]; @@ -179,7 +187,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data return [t('_notification.youReceivedFollowRequest'), { body: getUserName(data.body.user), icon: data.body.user.avatarUrl, - badge: iconUrl('clock'), + badge: iconUrl('user-plus'), data, actions: [ { @@ -197,14 +205,14 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data return [t('_notification.yourFollowRequestAccepted'), { body: getUserName(data.body.user), icon: data.body.user.avatarUrl, - badge: iconUrl('check'), + badge: iconUrl('circle-check'), data, }]; case 'groupInvited': return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), { body: data.body.invitation.group.name, - badge: iconUrl('id-card-alt'), + badge: iconUrl('users'), data, actions: [ { @@ -232,7 +240,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data if (data.body.groupId === null) { return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), { icon: data.body.user.avatarUrl, - badge: iconUrl('comments'), + badge: iconUrl('messages'), tag: `messaging:user:${data.body.userId}`, data, renotify: true, @@ -240,7 +248,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data } return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), { icon: data.body.user.avatarUrl, - badge: iconUrl('comments'), + badge: iconUrl('messages'), tag: `messaging:group:${data.body.groupId}`, data, renotify: true, @@ -249,7 +257,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), { body: `${getUserName(data.body.note.user)}: ${data.body.note.text || ''}`, icon: data.body.note.user.avatarUrl, - badge: iconUrl('satellite'), + badge: iconUrl('antenna'), tag: `antenna:${data.body.antenna.id}`, data, renotify: true, diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts index 2e23de8e1d..3b35de4079 100644 --- a/packages/sw/src/types.ts +++ b/packages/sw/src/types.ts @@ -36,3 +36,18 @@ export type pushNotificationData<K extends keyof pushNotificationDataSourceMap> export type pushNotificationDataMap = { [K in keyof pushNotificationDataSourceMap]: pushNotificationData<K>; }; + +export type badgeNames = + 'null' + | 'antenna' + | 'arrow-back-up' + | 'at' + | 'chart-arrows' + | 'circle-check' + | 'messages' + | 'plus' + | 'quote' + | 'repeat' + | 'user-plus' + | 'users' + ; From fceeb1b10882add478fee620111f7015bf5f4af7 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 17:38:33 +0900 Subject: [PATCH 27/68] :art: --- packages/frontend/src/components/MkReactionEffect.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkReactionEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue index af3a6c0e41..ff2d18ed80 100644 --- a/packages/frontend/src/components/MkReactionEffect.vue +++ b/packages/frontend/src/components/MkReactionEffect.vue @@ -23,7 +23,7 @@ const emit = defineEmits<{ }>(); let up = $ref(false); -const zIndex = os.claimZIndex('veryLow'); +const zIndex = os.claimZIndex('middle'); const angle = (90 - (Math.random() * 180)) + 'deg'; onMounted(() => { From 27c2ca50488680595c114dfae6f8de2ec3c48b32 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 17:41:09 +0900 Subject: [PATCH 28/68] =?UTF-8?q?feat(client):=20=F0=9F=8D=AA=F0=9F=91=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + locales/ja-JP.yml | 1 + packages/frontend/assets/cookie.png | Bin 0 -> 38365 bytes .../frontend/src/components/MkClickerGame.vue | 70 ++++++++++++++++++ .../src/components/MkPlusOneEffect.vue | 69 +++++++++++++++++ .../frontend/src/directives/click-anime.ts | 3 + packages/frontend/src/pages/clicker.vue | 24 ++++++ packages/frontend/src/router.ts | 4 + packages/frontend/src/scripts/clicker-game.ts | 46 ++++++++++++ packages/frontend/src/ui/_common_/common.ts | 5 ++ packages/frontend/src/widgets/clicker.vue | 44 +++++++++++ packages/frontend/src/widgets/index.ts | 2 + 12 files changed, 269 insertions(+) create mode 100644 packages/frontend/assets/cookie.png create mode 100644 packages/frontend/src/components/MkClickerGame.vue create mode 100644 packages/frontend/src/components/MkPlusOneEffect.vue create mode 100644 packages/frontend/src/pages/clicker.vue create mode 100644 packages/frontend/src/scripts/clicker-game.ts create mode 100644 packages/frontend/src/widgets/clicker.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index 611d5d986d..1a911f63b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ You should also include the user name that made the change. - Client: show bot warning on screen when logged in as bot account @syuilo - Client: improve overall performance of client @syuilo - Client: ui tweaks @syuilo +- Client: clicker game @syuilo ### Bugfixes - Server: 引用内の文章がnyaizeされてしまう問題を修正 @kabo2468 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 3445e58356..e42f9babe1 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1361,6 +1361,7 @@ _widgets: userList: "ユーザーリスト" _userList: chooseList: "リストを選択" + clicker: "クリッカー" _cw: hide: "隠す" diff --git a/packages/frontend/assets/cookie.png b/packages/frontend/assets/cookie.png new file mode 100644 index 0000000000000000000000000000000000000000..4a7f04061333e8f22c5a732f1dea9d692c0f59f8 GIT binary patch literal 38365 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 literal 0 HcmV?d00001 diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue new file mode 100644 index 0000000000..6ae202cb63 --- /dev/null +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -0,0 +1,70 @@ +<template> +<div> + <div v-if="game.ready" :class="$style.game"> + <div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div> + <button v-click-anime class="_button" :class="$style.button" @click="onClick"> + <img src="/client-assets/cookie.png" :class="$style.img"> + </button> + </div> + <div v-else> + <MkLoading/> + </div> +</div> +</template> + +<script lang="ts" setup> +import { computed, defineAsyncComponent, onMounted, onUnmounted } from 'vue'; +import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue'; +import * as os from '@/os'; +import { useInterval } from '@/scripts/use-interval'; +import * as game from '@/scripts/clicker-game'; +import number from '@/filters/number'; + +defineProps<{ +}>(); + +const saveData = game.saveData; +const cookies = computed(() => saveData.value?.cookies); + +function onClick(ev: MouseEvent) { + saveData.value!.cookies++; + saveData.value!.clicked++; + + const x = ev.clientX; + const y = ev.clientY; + os.popup(MkPlusOneEffect, { x, y }, {}, 'end'); +} + +useInterval(game.save, 1000 * 5, { + immediate: false, + afterMounted: true, +}); + +onMounted(async () => { + await game.load(); +}); + +onUnmounted(() => { + game.save(); +}); +</script> + +<style lang="scss" module> +.game { + padding: 16px; + text-align: center; +} + +.count { + font-size: 1.3em; + margin-bottom: 6px; +} + +.button { + +} + +.img { + max-width: 90px; +} +</style> diff --git a/packages/frontend/src/components/MkPlusOneEffect.vue b/packages/frontend/src/components/MkPlusOneEffect.vue new file mode 100644 index 0000000000..6a09669a68 --- /dev/null +++ b/packages/frontend/src/components/MkPlusOneEffect.vue @@ -0,0 +1,69 @@ +<template> +<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }"> + <span class="text" :class="{ up }">+1</span> +</div> +</template> + +<script lang="ts" setup> +import { onMounted } from 'vue'; +import * as os from '@/os'; + +const props = withDefaults(defineProps<{ + x: number; + y: number; +}>(), { +}); + +const emit = defineEmits<{ + (ev: 'end'): void; +}>(); + +let up = $ref(false); +const zIndex = os.claimZIndex('middle'); +const angle = (45 - (Math.random() * 90)) + 'deg'; + +onMounted(() => { + window.setTimeout(() => { + up = true; + }, 10); + + window.setTimeout(() => { + emit('end'); + }, 1100); +}); +</script> + +<style lang="scss" module> +.root { + pointer-events: none; + position: fixed; + width: 128px; + height: 128px; + + &:global { + > .text { + display: block; + height: 1em; + text-align: center; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + color: #fff; + text-shadow: 0 0 6px #000; + font-size: 18px; + font-weight: bold; + transform: translateY(0px); + transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5); + will-change: opacity, transform; + + &.up { + opacity: 0; + transform: translateY(-50px) rotateZ(v-bind(angle)); + } + } + } +} +</style> diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts index 83ec08543f..3d070177bd 100644 --- a/packages/frontend/src/directives/click-anime.ts +++ b/packages/frontend/src/directives/click-anime.ts @@ -12,6 +12,9 @@ export default { target.classList.add('_anime_bounce_standBy'); el.addEventListener('mousedown', () => { + target.classList.remove('_anime_bounce_ready'); + target.classList.remove('_anime_bounce'); + target.classList.add('_anime_bounce_standBy'); target.classList.add('_anime_bounce_ready'); diff --git a/packages/frontend/src/pages/clicker.vue b/packages/frontend/src/pages/clicker.vue new file mode 100644 index 0000000000..082a303e6f --- /dev/null +++ b/packages/frontend/src/pages/clicker.vue @@ -0,0 +1,24 @@ +<template> +<MkStickyContainer> + <template #header><MkPageHeader/></template> + <MkSpacer :content-max="800"> + <MkClickerGame/> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import MkClickerGame from '@/components/MkClickerGame.vue'; +import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; + +definePageMetadata({ + title: '🍪👈', + icon: 'ti ti-cookie', +}); +</script> + +<style lang="scss" module> + +</style> diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 63c753de22..bfa4a3ceab 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -460,6 +460,10 @@ export const routes = [{ path: '/timeline/antenna/:antennaId', component: page(() => import('./pages/antenna-timeline.vue')), loginRequired: true, +}, { + path: '/clicker', + component: page(() => import('./pages/clicker.vue')), + loginRequired: true, }, { name: 'index', path: '/', diff --git a/packages/frontend/src/scripts/clicker-game.ts b/packages/frontend/src/scripts/clicker-game.ts new file mode 100644 index 0000000000..77206cc8e2 --- /dev/null +++ b/packages/frontend/src/scripts/clicker-game.ts @@ -0,0 +1,46 @@ +import { ref, computed } from 'vue'; +import * as os from '@/os'; + +type SaveData = { + gameVersion: number; + cookies: number; + clicked: number; +}; + +export const saveData = ref<SaveData>(); +export const ready = computed(() => saveData.value != null); + +let prev = ''; + +export async function load() { + try { + saveData.value = await os.api('i/registry/get', { + scope: ['clickerGame'], + key: 'saveData', + }); + } catch (err) { + if (err.code === 'NO_SUCH_KEY') { + saveData.value = { + gameVersion: 1, + cookies: 0, + clicked: 0, + }; + save(); + return; + } + throw err; + } +} + +export async function save() { + const current = JSON.stringify(saveData.value); + if (current === prev) return; + + await os.api('i/registry/set', { + scope: ['clickerGame'], + key: 'saveData', + value: saveData.value, + }); + + prev = current; +} diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index dfdf324bcf..c3b22cd9e1 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -41,6 +41,11 @@ export function openInstanceMenu(ev: MouseEvent) { to: '/api-console', text: 'API Console', icon: 'ti ti-terminal-2', + }, { + type: 'link', + to: '/clicker', + text: '🍪👈', + icon: 'ti ti-cookie', }], }, null, { type: 'parent', diff --git a/packages/frontend/src/widgets/clicker.vue b/packages/frontend/src/widgets/clicker.vue new file mode 100644 index 0000000000..77d1777e97 --- /dev/null +++ b/packages/frontend/src/widgets/clicker.vue @@ -0,0 +1,44 @@ +<template> +<MkContainer :show-header="widgetProps.showHeader" class="mkw-clicker"> + <template #header><i class="ti ti-cookie"></i>Clicker</template> + <MkClickerGame/> +</MkContainer> +</template> + +<script lang="ts" setup> +import { onMounted, onUnmounted, Ref, ref, watch } from 'vue'; +import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget'; +import { GetFormResultType } from '@/scripts/form'; +import { $i } from '@/account'; +import MkContainer from '@/components/MkContainer.vue'; +import MkClickerGame from '@/components/MkClickerGame.vue'; + +const name = 'clicker'; + +const widgetPropsDef = { + showHeader: { + type: 'boolean' as const, + default: true, + }, +}; + +type WidgetProps = GetFormResultType<typeof widgetPropsDef>; + +// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない +//const props = defineProps<WidgetComponentProps<WidgetProps>>(); +//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>(); +const props = defineProps<{ widget?: Widget<WidgetProps>; }>(); +const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>(); + +const { widgetProps, configure } = useWidgetPropsManager(name, + widgetPropsDef, + props, + emit, +); + +defineExpose<WidgetComponentExpose>({ + name, + configure, + id: props.widget ? props.widget.id : null, +}); +</script> diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts index 3966649da4..eba4abd2f7 100644 --- a/packages/frontend/src/widgets/index.ts +++ b/packages/frontend/src/widgets/index.ts @@ -25,6 +25,7 @@ export default function(app: App) { app.component('MkwAiscriptApp', defineAsyncComponent(() => import('./aiscript-app.vue'))); app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue'))); app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue'))); + app.component('MkwClicker', defineAsyncComponent(() => import('./clicker.vue'))); } export const widgets = [ @@ -52,4 +53,5 @@ export const widgets = [ 'aiscriptApp', 'aichan', 'userList', + 'clicker', ]; From 416dcf884db0be69693a078d0a87116399028f63 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 17:42:51 +0900 Subject: [PATCH 29/68] :art: --- packages/frontend/src/pages/user/activity.notes.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue index f2103b152c..8763997f8e 100644 --- a/packages/frontend/src/pages/user/activity.notes.vue +++ b/packages/frontend/src/pages/user/activity.notes.vue @@ -86,10 +86,10 @@ async function renderChart() { type: 'bar', data: { datasets: [ - makeDataset('Normal', format(raw.diffs.normal).slice().reverse(), { backgroundColor: colorNormal }), - makeDataset('Reply', format(raw.diffs.reply).slice().reverse(), { backgroundColor: colorReply }), - makeDataset('Renote', format(raw.diffs.renote).slice().reverse(), { backgroundColor: colorRenote }), makeDataset('File', format(raw.diffs.withFile).slice().reverse(), { backgroundColor: colorFile }), + makeDataset('Renote', format(raw.diffs.renote).slice().reverse(), { backgroundColor: colorRenote }), + makeDataset('Reply', format(raw.diffs.reply).slice().reverse(), { backgroundColor: colorReply }), + makeDataset('Normal', format(raw.diffs.normal).slice().reverse(), { backgroundColor: colorNormal }), ], }, options: { From 4c8dbcc20d3d8ca5ab998e8f0bd948be5e0dac84 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 17:44:24 +0900 Subject: [PATCH 30/68] 13.0.0-beta.31 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 46e96d8716..277389de52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0-beta.30", + "version": "13.0.0-beta.31", "codename": "indigo", "repository": { "type": "git", From 809400ff23df8b42dedba3acf9170426e79bb4d5 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 18:08:43 +0900 Subject: [PATCH 31/68] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a911f63b7..a5db9bc963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ You should also include the user name that made the change. ## 13.0.0 (unreleased) ### TL;DR -- New features (Play, new widgets, new charts, etc) +- New features (Play, new widgets, new charts, 🍪👈, etc) - Rewriten backend - Better performance (backend and frontend) - Various usability improvements From 8977d87021f83b3f35698a267fd37cf84cb70f66 Mon Sep 17 00:00:00 2001 From: marihachi <marihachi0620@gmail.com> Date: Sun, 8 Jan 2023 19:59:05 +0900 Subject: [PATCH 32/68] fix typo (#9492) --- packages/frontend/src/pages/user/activity.pv.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue index 4be6978da0..d23f89a31e 100644 --- a/packages/frontend/src/pages/user/activity.pv.vue +++ b/packages/frontend/src/pages/user/activity.pv.vue @@ -89,7 +89,7 @@ async function renderChart() { makeDataset('UPV (user)', format(raw.upv.user).slice().reverse(), { backgroundColor: colorUser, stack: 'u' }), makeDataset('UPV (visitor)', format(raw.upv.visitor).slice().reverse(), { backgroundColor: colorVisitor, stack: 'u' }), makeDataset('NPV (user)', format(raw.pv.user).slice().reverse(), { backgroundColor: colorUser2, stack: 'n' }), - makeDataset('UPV (visitor)', format(raw.pv.visitor).slice().reverse(), { backgroundColor: colorVisitor2, stack: 'n' }), + makeDataset('NPV (visitor)', format(raw.pv.visitor).slice().reverse(), { backgroundColor: colorVisitor2, stack: 'n' }), ], }, options: { From 1d7e0293a8beb68aadaf4d0870822d12cbb45d64 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 20:02:07 +0900 Subject: [PATCH 33/68] fix following chart --- .../frontend/src/pages/user/activity.following.vue | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue index b7a51d5b69..500995e392 100644 --- a/packages/frontend/src/pages/user/activity.following.vue +++ b/packages/frontend/src/pages/user/activity.following.vue @@ -34,7 +34,7 @@ const chartEl = $shallowRef<HTMLCanvasElement>(null); let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>(); const now = new Date(); let chartInstance: Chart = null; -const chartLimit = 50; +const chartLimit = 30; let fetching = $ref(true); const { handler: externalTooltipHandler } = useChartTooltip(); @@ -77,7 +77,8 @@ async function renderChart() { borderWidth: 0, borderJoinStyle: 'round', borderRadius: 4, - barPercentage: 0.9, + barPercentage: 0.7, + categoryPercentage: 0.7, fill: true, } satisfies ChartDataset, extra); } @@ -86,10 +87,10 @@ async function renderChart() { type: 'bar', data: { datasets: [ - makeDataset('Follow (local)', format(raw.local.followings.inc).slice().reverse(), { backgroundColor: colorFollowLocal }), - makeDataset('Follow (remote)', format(raw.remote.followings.inc).slice().reverse(), { backgroundColor: colorFollowRemote }), - makeDataset('Followed (local)', format(raw.local.followers.inc).slice().reverse(), { backgroundColor: colorFollowedLocal }), - makeDataset('Followed (remote)', format(raw.remote.followers.inc).slice().reverse(), { backgroundColor: colorFollowedRemote }), + makeDataset('Follow (local)', format(raw.local.followings.inc).slice().reverse(), { backgroundColor: colorFollowLocal, stack: 'follow' }), + makeDataset('Follow (remote)', format(raw.remote.followings.inc).slice().reverse(), { backgroundColor: colorFollowRemote, stack: 'follow' }), + makeDataset('Followed (local)', format(raw.local.followers.inc).slice().reverse(), { backgroundColor: colorFollowedLocal, stack: 'followed' }), + makeDataset('Followed (remote)', format(raw.remote.followers.inc).slice().reverse(), { backgroundColor: colorFollowedRemote, stack: 'followed' }), ], }, options: { From 5d13e2744fc6fb8ca0f79223ed06d7d5c25b9555 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 20:21:32 +0900 Subject: [PATCH 34/68] :art: --- packages/frontend/src/directives/click-anime.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts index 3d070177bd..5cd54c7323 100644 --- a/packages/frontend/src/directives/click-anime.ts +++ b/packages/frontend/src/directives/click-anime.ts @@ -12,7 +12,6 @@ export default { target.classList.add('_anime_bounce_standBy'); el.addEventListener('mousedown', () => { - target.classList.remove('_anime_bounce_ready'); target.classList.remove('_anime_bounce'); target.classList.add('_anime_bounce_standBy'); @@ -25,10 +24,10 @@ export default { el.addEventListener('click', () => { target.classList.add('_anime_bounce'); + target.classList.remove('_anime_bounce_ready'); }); el.addEventListener('animationend', () => { - target.classList.remove('_anime_bounce_ready'); target.classList.remove('_anime_bounce'); target.classList.add('_anime_bounce_standBy'); }); From 7ed905f76b1a7c3e9a60a777bed9a17803455dd9 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 20:30:19 +0900 Subject: [PATCH 35/68] :cookie: cps --- .../frontend/src/components/MkClickerGame.vue | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 6ae202cb63..6d0b2236b4 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -1,6 +1,7 @@ <template> <div> <div v-if="game.ready" :class="$style.game"> + <div :class="$style.cps" class="">{{ number(cps) }}cps</div> <div :class="$style.count" class=""><i class="ti ti-cookie" style="font-size: 70%;"></i> {{ number(cookies) }}</div> <button v-click-anime class="_button" :class="$style.button" @click="onClick"> <img src="/client-assets/cookie.png" :class="$style.img"> @@ -25,6 +26,8 @@ defineProps<{ const saveData = game.saveData; const cookies = computed(() => saveData.value?.cookies); +let cps = $ref(0); +let prevCookies = $ref(0); function onClick(ev: MouseEvent) { saveData.value!.cookies++; @@ -35,6 +38,15 @@ function onClick(ev: MouseEvent) { os.popup(MkPlusOneEffect, { x, y }, {}, 'end'); } +useInterval(() => { + const diff = saveData.value!.cookies - prevCookies; + cps = diff; + prevCookies = saveData.value!.cookies; +}, 1000, { + immediate: false, + afterMounted: true, +}); + useInterval(game.save, 1000 * 5, { immediate: false, afterMounted: true, @@ -42,6 +54,7 @@ useInterval(game.save, 1000 * 5, { onMounted(async () => { await game.load(); + prevCookies = saveData.value!.cookies; }); onUnmounted(() => { @@ -55,6 +68,13 @@ onUnmounted(() => { text-align: center; } +.cps { + position: absolute; + top: 12px; + left: 12px; + opacity: 0.5; +} + .count { font-size: 1.3em; margin-bottom: 6px; From 10e526ba5682fef9488d1d38ba5dfcda38619673 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Sun, 8 Jan 2023 20:32:17 +0900 Subject: [PATCH 36/68] fix: Escape SQL LIKE (#9493) * SQL LIKE escape * CHANGELOG --- CHANGELOG.md | 1 + packages/backend/src/misc/sql-like-escape.ts | 3 +++ .../server/api/endpoints/admin/emoji/list-remote.ts | 3 ++- .../src/server/api/endpoints/admin/emoji/list.ts | 3 ++- .../src/server/api/endpoints/admin/show-users.ts | 3 ++- .../src/server/api/endpoints/federation/instances.ts | 3 ++- .../src/server/api/endpoints/hashtags/search.ts | 3 ++- .../backend/src/server/api/endpoints/notes/search.ts | 3 ++- .../endpoints/users/search-by-username-and-host.ts | 11 ++++++----- .../backend/src/server/api/endpoints/users/search.ts | 9 +++++---- 10 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 packages/backend/src/misc/sql-like-escape.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a5db9bc963..d19545c7d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ You should also include the user name that made the change. - Server: アンテナの作成数上限を追加 @syuilo - Server: pages/likeのエラーIDが重複しているのを修正 @syuilo - Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo +- Server: Escape SQL LIKE @mei23 - Client: case insensitive emoji search @saschanaz - Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina - Client: use proxied image for instance icon @syuilo diff --git a/packages/backend/src/misc/sql-like-escape.ts b/packages/backend/src/misc/sql-like-escape.ts new file mode 100644 index 0000000000..8470dca3de --- /dev/null +++ b/packages/backend/src/misc/sql-like-escape.ts @@ -0,0 +1,3 @@ +export function sqlLikeEscape(s: string) { + return s.replace(/([%_])/g, '\\$1'); +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index c03d27878c..ed60efd7b4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -5,6 +5,7 @@ import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape'; export const meta = { tags: ['admin'], @@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } if (ps.query) { - q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' }); + q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); } const emojis = await q 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 271b142126..f357e45a52 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -5,6 +5,7 @@ import type { Emoji } from '@/models/entities/Emoji.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +//import { sqlLikeEscape } from '@/misc/sql-like-escape'; export const meta = { tags: ['admin'], @@ -82,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { let emojis: Emoji[]; if (ps.query) { - //q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` }); + //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); //const emojis = await q.take(ps.limit).getMany(); emojis = await q.getMany(); diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 33e1be8041..722e284dde 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -3,6 +3,7 @@ import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape'; export const meta = { tags: ['admin'], @@ -68,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } if (ps.username) { - query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' }); + query.andWhere('user.usernameLower like :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }); } if (ps.hostname) { diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 5e2f204661..726979309f 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -4,6 +4,7 @@ import type { InstancesRepository } from '@/models/index.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape'; export const meta = { tags: ['federation'], @@ -120,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } if (ps.host) { - query.andWhere('instance.host like :host', { host: '%' + ps.host.toLowerCase() + '%' }); + query.andWhere('instance.host like :host', { host: '%' + sqlLikeEscape(ps.host.toLowerCase()) + '%' }); } const instances = await query.take(ps.limit).skip(ps.offset).getMany(); diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index 7f787ea38f..6c56ef5da2 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { HashtagsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape'; export const meta = { tags: ['hashtags'], @@ -37,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { ) { super(meta, paramDef, async (ps, me) => { const hashtags = await this.hashtagsRepository.createQueryBuilder('tag') - .where('tag.name like :q', { q: ps.query.toLowerCase() + '%' }) + .where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' }) .orderBy('tag.count', 'DESC') .groupBy('tag.id') .take(ps.limit) diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 27b477e141..02701ffe1e 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -6,6 +6,7 @@ import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape'; export const meta = { tags: ['notes'], @@ -70,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } query - .andWhere('note.text ILIKE :q', { q: `%${ps.query}%` }) + .andWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }) .innerJoinAndSelect('note.user', 'user') .leftJoinAndSelect('user.avatar', 'avatar') .leftJoinAndSelect('user.banner', 'banner') diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index f13df3ee9d..029b1e91c3 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -6,6 +6,7 @@ import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape'; export const meta = { tags: ['users'], @@ -59,10 +60,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { if (ps.host) { const q = this.usersRepository.createQueryBuilder('user') .where('user.isSuspended = FALSE') - .andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' }); + .andWhere('user.host LIKE :host', { host: sqlLikeEscape(ps.host.toLowerCase()) + '%' }); if (ps.username) { - q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }); + q.andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }); } q.andWhere('user.updatedAt IS NOT NULL'); @@ -83,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .where(`user.id IN (${ followingQuery.getQuery() })`) .andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); @@ -101,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { .where(`user.id NOT IN (${ followingQuery.getQuery() })`) .andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }) .andWhere('user.updatedAt IS NOT NULL'); otherQuery.setParameters(followingQuery.getParameters()); @@ -116,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } else { users = await this.usersRepository.createQueryBuilder('user') .where('user.isSuspended = FALSE') - .andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' }) + .andWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.username.toLowerCase()) + '%' }) .andWhere('user.updatedAt IS NOT NULL') .orderBy('user.updatedAt', 'DESC') .take(ps.limit - users.length) diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index ba07714972..25bd621269 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -5,6 +5,7 @@ import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; +import { sqlLikeEscape } from '@/misc/sql-like-escape'; export const meta = { tags: ['users'], @@ -57,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { if (isUsername) { const usernameQuery = this.usersRepository.createQueryBuilder('user') - .where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) + .where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' }) .andWhere(new Brackets(qb => { qb .where('user.updatedAt IS NULL') .orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold }); @@ -78,11 +79,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { } else { const nameQuery = this.usersRepository.createQueryBuilder('user') .where(new Brackets(qb => { - qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' }); + qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); // Also search username if it qualifies as username if (this.userEntityService.validateLocalUsername(ps.query)) { - qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' }); + qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' }); } })) .andWhere(new Brackets(qb => { qb @@ -106,7 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { if (users.length < ps.limit) { const profQuery = this.userProfilesRepository.createQueryBuilder('prof') .select('prof.userId') - .where('prof.description ILIKE :query', { query: '%' + ps.query + '%' }); + .where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' }); if (ps.origin === 'local') { profQuery.andWhere('prof.userHost IS NULL'); From dd78ac089c54847b916e54ff746ad915d9d061b7 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sun, 8 Jan 2023 20:42:45 +0900 Subject: [PATCH 37/68] :cookie: --- .../frontend/src/components/MkClickerGame.vue | 2 ++ packages/frontend/src/scripts/clicker-game.ts | 24 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue index 6d0b2236b4..03736ac5e4 100644 --- a/packages/frontend/src/components/MkClickerGame.vue +++ b/packages/frontend/src/components/MkClickerGame.vue @@ -31,6 +31,8 @@ let prevCookies = $ref(0); function onClick(ev: MouseEvent) { saveData.value!.cookies++; + saveData.value!.totalCookies++; + saveData.value!.totalHandmadeCookies++; saveData.value!.clicked++; const x = ev.clientX; diff --git a/packages/frontend/src/scripts/clicker-game.ts b/packages/frontend/src/scripts/clicker-game.ts index 77206cc8e2..d3b0f9d1e2 100644 --- a/packages/frontend/src/scripts/clicker-game.ts +++ b/packages/frontend/src/scripts/clicker-game.ts @@ -4,7 +4,11 @@ import * as os from '@/os'; type SaveData = { gameVersion: number; cookies: number; + totalCookies: number; + totalHandmadeCookies: number; clicked: number; + achievements: any[]; + facilities: any[]; }; export const saveData = ref<SaveData>(); @@ -21,15 +25,33 @@ export async function load() { } catch (err) { if (err.code === 'NO_SUCH_KEY') { saveData.value = { - gameVersion: 1, + gameVersion: 2, cookies: 0, + totalCookies: 0, + totalHandmadeCookies: 0, clicked: 0, + achievements: [], + facilities: [], }; save(); return; } throw err; } + + // migration + if (saveData.value.gameVersion === 1) { + saveData.value = { + gameVersion: 2, + cookies: saveData.value.cookies, + totalCookies: saveData.value.cookies, + totalHandmadeCookies: saveData.value.cookies, + clicked: saveData.value.clicked, + achievements: [], + facilities: [], + }; + save(); + } } export async function save() { From 91ced90fb2e724e77afc3ef59456165ea6dbd5c5 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 08:58:16 +0900 Subject: [PATCH 38/68] fix imports --- .../backend/src/server/api/endpoints/admin/emoji/list-remote.ts | 2 +- packages/backend/src/server/api/endpoints/admin/emoji/list.ts | 2 +- packages/backend/src/server/api/endpoints/admin/show-users.ts | 2 +- .../backend/src/server/api/endpoints/federation/instances.ts | 2 +- packages/backend/src/server/api/endpoints/hashtags/search.ts | 2 +- packages/backend/src/server/api/endpoints/notes/search.ts | 2 +- .../server/api/endpoints/users/search-by-username-and-host.ts | 2 +- packages/backend/src/server/api/endpoints/users/search.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index ed60efd7b4..08ffc73fea 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -5,7 +5,7 @@ import { QueryService } from '@/core/QueryService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { DI } from '@/di-symbols.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['admin'], 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 f357e45a52..6bcd4973d6 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -5,7 +5,7 @@ import type { Emoji } from '@/models/entities/Emoji.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -//import { sqlLikeEscape } from '@/misc/sql-like-escape'; +//import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['admin'], diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 722e284dde..5a67cf522a 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -3,7 +3,7 @@ import type { UsersRepository } from '@/models/index.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['admin'], diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 726979309f..4c19988eb8 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -4,7 +4,7 @@ import type { InstancesRepository } from '@/models/index.js'; import { InstanceEntityService } from '@/core/entities/InstanceEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['federation'], diff --git a/packages/backend/src/server/api/endpoints/hashtags/search.ts b/packages/backend/src/server/api/endpoints/hashtags/search.ts index 6c56ef5da2..4f5f979767 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/search.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/search.ts @@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { HashtagsRepository } from '@/models/index.js'; import { DI } from '@/di-symbols.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['hashtags'], diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 02701ffe1e..8eb031dfe3 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -6,7 +6,7 @@ import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts index 029b1e91c3..95491211bc 100644 --- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts +++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts @@ -6,7 +6,7 @@ import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['users'], diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts index 25bd621269..d7a60f0437 100644 --- a/packages/backend/src/server/api/endpoints/users/search.ts +++ b/packages/backend/src/server/api/endpoints/users/search.ts @@ -5,7 +5,7 @@ import type { User } from '@/models/entities/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; -import { sqlLikeEscape } from '@/misc/sql-like-escape'; +import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['users'], From 8524e9d73596e28e1e9f6b267bcfc6ea16e3a577 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 09:04:35 +0900 Subject: [PATCH 39/68] tweak client --- packages/frontend/src/components/MkNote.vue | 4 ++-- packages/frontend/src/components/MkNoteDetailed.vue | 8 ++++---- packages/frontend/src/components/MkNoteHeader.vue | 4 ++-- packages/frontend/src/pages/about.emojis.vue | 4 ++-- packages/frontend/src/pages/about.federation.vue | 4 ++-- packages/frontend/src/pages/admin/users.vue | 4 ++-- packages/frontend/src/widgets/clicker.vue | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 86f176a693..cb97f19d31 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -27,12 +27,12 @@ <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> <MkTime :time="note.createdAt"/> </button> - <span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]"> + <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> <i v-if="note.visibility === 'home'" class="ti ti-home"></i> <i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> - <span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> </div> </div> <article class="article" @contextmenu.stop="onContextmenu"> diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 7c57a64b09..4dc1358b24 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -25,12 +25,12 @@ <i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i> <MkTime :time="note.createdAt"/> </button> - <span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]"> + <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> <i v-if="note.visibility === 'home'" class="ti ti-home"></i> <i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> - <span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> </div> </div> <article class="article" @contextmenu.stop="onContextmenu"> @@ -43,12 +43,12 @@ </MkA> <span v-if="appearNote.user.isBot" class="is-bot">bot</span> <div class="info"> - <span v-if="appearNote.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[appearNote.visibility]"> + <span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]"> <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i> <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock-open"></i> <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> - <span v-if="appearNote.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> + <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> </div> </div> <div class="username"><MkAcct :user="appearNote.user"/></div> diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 61d07f85e5..ddd25e784f 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -9,12 +9,12 @@ <MkA class="created-at" :to="notePage(note)"> <MkTime :time="note.createdAt"/> </MkA> - <span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]"> + <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> <i v-if="note.visibility === 'home'" class="ti ti-home"></i> <i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> - <span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> + <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> </div> </header> </template> diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index 669b6f1264..5a3f58cebe 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -19,10 +19,10 @@ </div> </MkFolder> - <MkFolder v-for="category in customEmojiCategories" :key="category" class="emojis"> + <MkFolder v-for="category in customEmojiCategories" v-once :key="category" class="emojis"> <template #header>{{ category || $ts.other }}</template> <div class="zuvgdzyt"> - <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" v-once :key="emoji.name" class="emoji" :emoji="emoji"/> + <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/> </div> </MkFolder> </div> diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index e530bb49dd..a4e449911c 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -36,8 +36,8 @@ <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> <div class="dqokceoi"> - <MkA v-for="instance in items" v-once :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`"> - <MkInstanceCardMini :instance="instance"/> + <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`"> + <MkInstanceCardMini v-once :instance="instance"/> </MkA> </div> </MkPagination> diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index c198dfdb46..445a0a3a71 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -41,8 +41,8 @@ </div> <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users"> - <MkA v-for="user in items" v-once :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`"> - <MkUserCardMini :user="user"/> + <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`"> + <MkUserCardMini v-once :user="user"/> </MkA> </MkPagination> </div> diff --git a/packages/frontend/src/widgets/clicker.vue b/packages/frontend/src/widgets/clicker.vue index 77d1777e97..168f05bccd 100644 --- a/packages/frontend/src/widgets/clicker.vue +++ b/packages/frontend/src/widgets/clicker.vue @@ -18,7 +18,7 @@ const name = 'clicker'; const widgetPropsDef = { showHeader: { type: 'boolean' as const, - default: true, + default: false, }, }; From e633c3b84b96fb1eb0e6cc864c391a00adc20bd8 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 09:41:25 +0900 Subject: [PATCH 40/68] refactor --- packages/frontend/src/components/MkAsUi.vue | 6 +- .../src/components/MkFoldableSection.vue | 154 ++++++++++ packages/frontend/src/components/MkFolder.vue | 270 ++++++++++-------- .../src/components/MkInstanceStats.vue | 18 +- .../frontend/src/components/form/folder.vue | 175 ------------ packages/frontend/src/pages/about.emojis.vue | 12 +- .../frontend/src/pages/admin/integrations.vue | 14 +- .../frontend/src/pages/admin/overview.vue | 46 +-- .../frontend/src/pages/admin/security.vue | 22 +- packages/frontend/src/pages/explore.users.vue | 40 +-- packages/frontend/src/pages/explore.vue | 4 +- packages/frontend/src/pages/flash/flash.vue | 6 +- packages/frontend/src/pages/gallery/index.vue | 10 +- .../src/pages/settings/import-export.vue | 42 +-- .../frontend/src/pages/settings/privacy.vue | 6 +- .../frontend/src/pages/settings/profile.vue | 10 +- .../frontend/src/pages/settings/sounds.vue | 6 +- .../frontend/src/pages/settings/statusbar.vue | 6 +- packages/frontend/src/pages/theme-editor.vue | 22 +- packages/frontend/src/pages/user-info.vue | 14 +- packages/frontend/src/pages/user/activity.vue | 18 +- packages/frontend/src/pages/user/home.vue | 2 +- 22 files changed, 453 insertions(+), 450 deletions(-) create mode 100644 packages/frontend/src/components/MkFoldableSection.vue delete mode 100644 packages/frontend/src/components/form/folder.vue diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue index b6b49725d2..5844a18633 100644 --- a/packages/frontend/src/components/MkAsUi.vue +++ b/packages/frontend/src/components/MkAsUi.vue @@ -33,12 +33,12 @@ <option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option> </MkSelect> <MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton> - <FormFolder v-else-if="c.type === 'folder'" :default-open="c.opened"> + <MkFolder v-else-if="c.type === 'folder'" :default-open="c.opened"> <template #label>{{ c.title }}</template> <template v-for="child in c.children" :key="child"> <MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> </template> - </FormFolder> + </MkFolder> <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }"> <template v-for="child in c.children" :key="child"> <MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/> @@ -56,7 +56,7 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkSelect from '@/components/MkSelect.vue'; import { AsUiComponent } from '@/scripts/aiscript/ui'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; const props = withDefaults(defineProps<{ component: AsUiComponent; diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue new file mode 100644 index 0000000000..aa2d9aac25 --- /dev/null +++ b/packages/frontend/src/components/MkFoldableSection.vue @@ -0,0 +1,154 @@ +<template> +<div class="ssazuxis"> + <header class="_button" :style="{ background: bg }" @click="showBody = !showBody"> + <div class="title"><div><slot name="header"></slot></div></div> + <div class="divider"></div> + <button class="_button"> + <template v-if="showBody"><i class="ti ti-chevron-up"></i></template> + <template v-else><i class="ti ti-chevron-down"></i></template> + </button> + </header> + <Transition + :name="$store.state.animation ? 'folder-toggle' : ''" + @enter="enter" + @after-enter="afterEnter" + @leave="leave" + @after-leave="afterLeave" + > + <div v-show="showBody"> + <slot></slot> + </div> + </Transition> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import tinycolor from 'tinycolor2'; +import { miLocalStorage } from '@/local-storage'; + +const miLocalStoragePrefix = 'ui:folder:' as const; + +export default defineComponent({ + props: { + expanded: { + type: Boolean, + required: false, + default: true, + }, + persistKey: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + bg: null, + showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded, + }; + }, + watch: { + showBody() { + if (this.persistKey) { + miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f'); + } + }, + }, + mounted() { + function getParentBg(el: Element | null): string { + if (el == null || el.tagName === 'BODY') return 'var(--bg)'; + const bg = el.style.background || el.style.backgroundColor; + if (bg) { + return bg; + } else { + return getParentBg(el.parentElement); + } + } + const rawBg = getParentBg(this.$el); + const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); + bg.setAlpha(0.85); + this.bg = bg.toRgbString(); + }, + methods: { + toggleContent(show: boolean) { + this.showBody = show; + }, + + enter(el) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = 0; + el.offsetHeight; // reflow + el.style.height = elementHeight + 'px'; + }, + afterEnter(el) { + el.style.height = null; + }, + leave(el) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = elementHeight + 'px'; + el.offsetHeight; // reflow + el.style.height = 0; + }, + afterLeave(el) { + el.style.height = null; + }, + }, +}); +</script> + +<style lang="scss" scoped> +.folder-toggle-enter-active, .folder-toggle-leave-active { + overflow-y: clip; + transition: opacity 0.5s, height 0.5s !important; +} +.folder-toggle-enter-from { + opacity: 0; +} +.folder-toggle-leave-to { + opacity: 0; +} + +.ssazuxis { + position: relative; + + > header { + display: flex; + position: relative; + z-index: 10; + position: sticky; + top: var(--stickyTop, 0px); + padding: var(--x-padding); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(20px)); + + > .title { + display: grid; + place-content: center; + margin: 0; + padding: 12px 16px 12px 0; + } + + > .divider { + flex: 1; + margin: auto; + height: 1px; + background: var(--divider); + } + + > button { + padding: 12px 0 12px 16px; + } + } +} + +@container (max-width: 500px) { + .ssazuxis { + > header { + > .title { + padding: 8px 10px 8px 0; + } + } + } +} +</style> diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index aa2d9aac25..7fb0720d3d 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -1,153 +1,177 @@ <template> -<div class="ssazuxis"> - <header class="_button" :style="{ background: bg }" @click="showBody = !showBody"> - <div class="title"><div><slot name="header"></slot></div></div> - <div class="divider"></div> - <button class="_button"> - <template v-if="showBody"><i class="ti ti-chevron-up"></i></template> - <template v-else><i class="ti ti-chevron-down"></i></template> - </button> - </header> - <Transition - :name="$store.state.animation ? 'folder-toggle' : ''" - @enter="enter" - @after-enter="afterEnter" - @leave="leave" - @after-leave="afterLeave" - > - <div v-show="showBody"> - <slot></slot> - </div> - </Transition> +<div ref="rootEl" class="dwzlatin" :class="{ opened }"> + <div class="header _button" @click="toggle"> + <span class="icon"><slot name="icon"></slot></span> + <span class="text"><slot name="label"></slot></span> + <span class="right"> + <span class="text"><slot name="suffix"></slot></span> + <i v-if="opened" class="ti ti-chevron-up icon"></i> + <i v-else class="ti ti-chevron-down icon"></i> + </span> + </div> + <div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null }"> + <Transition + :name="$store.state.animation ? 'folder-toggle' : ''" + @enter="enter" + @after-enter="afterEnter" + @leave="leave" + @after-leave="afterLeave" + > + <KeepAlive> + <div v-show="opened"> + <MkSpacer :margin-min="14" :margin-max="22"> + <slot></slot> + </MkSpacer> + </div> + </KeepAlive> + </Transition> + </div> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; -import tinycolor from 'tinycolor2'; -import { miLocalStorage } from '@/local-storage'; +<script lang="ts" setup> +import { nextTick, onMounted } from 'vue'; -const miLocalStoragePrefix = 'ui:folder:' as const; +const props = withDefaults(defineProps<{ + defaultOpen: boolean; + maxHeight: number | null; +}>(), { + defaultOpen: false, + maxHeight: null, +}); -export default defineComponent({ - props: { - expanded: { - type: Boolean, - required: false, - default: true, - }, - persistKey: { - type: String, - required: false, - default: null, - }, - }, - data() { - return { - bg: null, - showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded, - }; - }, - watch: { - showBody() { - if (this.persistKey) { - miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f'); - } - }, - }, - mounted() { - function getParentBg(el: Element | null): string { - if (el == null || el.tagName === 'BODY') return 'var(--bg)'; - const bg = el.style.background || el.style.backgroundColor; - if (bg) { - return bg; - } else { - return getParentBg(el.parentElement); - } - } - const rawBg = getParentBg(this.$el); - const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); - bg.setAlpha(0.85); - this.bg = bg.toRgbString(); - }, - methods: { - toggleContent(show: boolean) { - this.showBody = show; - }, +const getBgColor = (el: HTMLElement) => { + const style = window.getComputedStyle(el); + if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { + return style.backgroundColor; + } else { + return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; + } +}; - enter(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; - el.offsetHeight; // reflow - el.style.height = elementHeight + 'px'; - }, - afterEnter(el) { - el.style.height = null; - }, - leave(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; - el.offsetHeight; // reflow - el.style.height = 0; - }, - afterLeave(el) { - el.style.height = null; - }, - }, +let rootEl = $ref<HTMLElement>(); +let bgSame = $ref(false); +let opened = $ref(props.defaultOpen); +let openedAtLeastOnce = $ref(props.defaultOpen); + +function enter(el) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = 0; + el.offsetHeight; // reflow + el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px'; +} + +function afterEnter(el) { + el.style.height = null; +} + +function leave(el) { + const elementHeight = el.getBoundingClientRect().height; + el.style.height = elementHeight + 'px'; + el.offsetHeight; // reflow + el.style.height = 0; +} + +function afterLeave(el) { + el.style.height = null; +} + +function toggle() { + if (!opened) { + openedAtLeastOnce = true; + } + + nextTick(() => { + opened = !opened; + }); +} + +onMounted(() => { + const computedStyle = getComputedStyle(document.documentElement); + const parentBg = getBgColor(rootEl.parentElement); + const myBg = computedStyle.getPropertyValue('--panel'); + bgSame = parentBg === myBg; }); </script> <style lang="scss" scoped> .folder-toggle-enter-active, .folder-toggle-leave-active { overflow-y: clip; - transition: opacity 0.5s, height 0.5s !important; + transition: opacity 0.3s, height 0.3s, transform 0.3s !important; } -.folder-toggle-enter-from { - opacity: 0; -} -.folder-toggle-leave-to { +.folder-toggle-enter-from, .folder-toggle-leave-to { opacity: 0; } -.ssazuxis { - position: relative; +.dwzlatin { + display: block; - > header { + > .header { display: flex; - position: relative; - z-index: 10; - position: sticky; - top: var(--stickyTop, 0px); - padding: var(--x-padding); - -webkit-backdrop-filter: var(--blur, blur(8px)); - backdrop-filter: var(--blur, blur(20px)); + align-items: center; + width: 100%; + box-sizing: border-box; + padding: 10px 14px 10px 14px; + background: var(--buttonBg); + border-radius: 6px; - > .title { - display: grid; - place-content: center; - margin: 0; - padding: 12px 16px 12px 0; + &:hover { + text-decoration: none; + background: var(--buttonHoverBg); } - > .divider { - flex: 1; - margin: auto; - height: 1px; - background: var(--divider); + &.active { + color: var(--accent); + background: var(--buttonHoverBg); } - > button { - padding: 12px 0 12px 16px; + > .icon { + margin-right: 0.75em; + flex-shrink: 0; + text-align: center; + opacity: 0.8; + + &:empty { + display: none; + + & + .text { + padding-left: 4px; + } + } + } + + > .text { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + padding-right: 12px; + } + + > .right { + margin-left: auto; + opacity: 0.7; + white-space: nowrap; + + > .text:not(:empty) { + margin-right: 0.75em; + } } } -} -@container (max-width: 500px) { - .ssazuxis { - > header { - > .title { - padding: 8px 10px 8px 0; - } + > .body { + background: var(--panel); + border-radius: 0 0 6px 6px; + container-type: inline-size; + overflow: auto; + + &.bgSame { + background: var(--bg); + } + } + + &.opened { + > .header { + border-radius: 6px 6px 0 0; } } } diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue index 05aea5c731..0f87fef6b1 100644 --- a/packages/frontend/src/components/MkInstanceStats.vue +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -1,6 +1,6 @@ <template> <div :class="$style.root"> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Chart</template> <div :class="$style.chart"> <div class="selects"> @@ -34,9 +34,9 @@ <MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart> </div> </div> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Active users heatmap</template> <MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;"> <option value="active-users">Active users</option> @@ -48,16 +48,16 @@ <div class="_panel" :class="$style.heatmap"> <MkHeatmap :src="heatmapSrc"/> </div> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Retention rate</template> <div class="_panel" :class="$style.retention"> <MkRetentionHeatmap/> </div> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Federation</template> <div :class="$style.federation"> <div class="pies"> @@ -71,7 +71,7 @@ </div> </div> </div> - </MkFolder> + </MkFoldableSection> </div> </template> @@ -84,7 +84,7 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip'; import * as os from '@/os'; import { i18n } from '@/i18n'; import MkHeatmap from '@/components/MkHeatmap.vue'; -import MkFolder from '@/components/MkFolder.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; import { initChart } from '@/scripts/init-chart'; diff --git a/packages/frontend/src/components/form/folder.vue b/packages/frontend/src/components/form/folder.vue deleted file mode 100644 index 961f1c1cac..0000000000 --- a/packages/frontend/src/components/form/folder.vue +++ /dev/null @@ -1,175 +0,0 @@ -<template> -<div ref="rootEl" class="dwzlatin" :class="{ opened }"> - <div class="header _button" @click="toggle"> - <span class="icon"><slot name="icon"></slot></span> - <span class="text"><slot name="label"></slot></span> - <span class="right"> - <span class="text"><slot name="suffix"></slot></span> - <i v-if="opened" class="ti ti-chevron-up icon"></i> - <i v-else class="ti ti-chevron-down icon"></i> - </span> - </div> - <div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }"> - <Transition - :name="$store.state.animation ? 'folder-toggle' : ''" - @enter="enter" - @after-enter="afterEnter" - @leave="leave" - @after-leave="afterLeave" - > - <KeepAlive> - <div v-show="opened"> - <MkSpacer :margin-min="14" :margin-max="22"> - <slot></slot> - </MkSpacer> - </div> - </KeepAlive> - </Transition> - </div> -</div> -</template> - -<script lang="ts" setup> -import { nextTick, onMounted } from 'vue'; - -const props = withDefaults(defineProps<{ - defaultOpen: boolean; -}>(), { - defaultOpen: false, -}); - -const getBgColor = (el: HTMLElement) => { - const style = window.getComputedStyle(el); - if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { - return style.backgroundColor; - } else { - return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; - } -}; - -let rootEl = $ref<HTMLElement>(); -let bgSame = $ref(false); -let opened = $ref(props.defaultOpen); -let openedAtLeastOnce = $ref(props.defaultOpen); - -function enter(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = 0; - el.offsetHeight; // reflow - el.style.height = elementHeight + 'px'; -} - -function afterEnter(el) { - el.style.height = null; -} - -function leave(el) { - const elementHeight = el.getBoundingClientRect().height; - el.style.height = elementHeight + 'px'; - el.offsetHeight; // reflow - el.style.height = 0; -} - -function afterLeave(el) { - el.style.height = null; -} - -function toggle() { - if (!opened) { - openedAtLeastOnce = true; - } - - nextTick(() => { - opened = !opened; - }); -} - -onMounted(() => { - const computedStyle = getComputedStyle(document.documentElement); - const parentBg = getBgColor(rootEl.parentElement); - const myBg = computedStyle.getPropertyValue('--panel'); - bgSame = parentBg === myBg; -}); -</script> - -<style lang="scss" scoped> -.folder-toggle-enter-active, .folder-toggle-leave-active { - overflow-y: clip; - transition: opacity 0.3s, height 0.3s, transform 0.3s !important; -} -.folder-toggle-enter-from, .folder-toggle-leave-to { - opacity: 0; -} - -.dwzlatin { - display: block; - - > .header { - display: flex; - align-items: center; - width: 100%; - box-sizing: border-box; - padding: 10px 14px 10px 14px; - background: var(--buttonBg); - border-radius: 6px; - - &:hover { - text-decoration: none; - background: var(--buttonHoverBg); - } - - &.active { - color: var(--accent); - background: var(--buttonHoverBg); - } - - > .icon { - margin-right: 0.75em; - flex-shrink: 0; - text-align: center; - opacity: 0.8; - - &:empty { - display: none; - - & + .text { - padding-left: 4px; - } - } - } - - > .text { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - padding-right: 12px; - } - - > .right { - margin-left: auto; - opacity: 0.7; - white-space: nowrap; - - > .text:not(:empty) { - margin-right: 0.75em; - } - } - } - - > .body { - background: var(--panel); - border-radius: 0 0 6px 6px; - container-type: inline-size; - - &.bgSame { - background: var(--bg); - } - } - - &.opened { - > .header { - border-radius: 6px 6px 0 0; - } - } -} -</style> diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index 5a3f58cebe..e15a2b1bdf 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -12,19 +12,19 @@ --> </div> - <MkFolder v-if="searchEmojis" class="emojis"> + <MkFoldableSection v-if="searchEmojis" class="emojis"> <template #header>{{ $ts.searchResult }}</template> <div class="zuvgdzyt"> <XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/> </div> - </MkFolder> + </MkFoldableSection> - <MkFolder v-for="category in customEmojiCategories" v-once :key="category" class="emojis"> + <MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category" class="emojis"> <template #header>{{ category || $ts.other }}</template> <div class="zuvgdzyt"> <XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/> </div> - </MkFolder> + </MkFoldableSection> </div> </template> @@ -34,7 +34,7 @@ import XEmoji from './emojis.emoji.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; -import MkFolder from '@/components/MkFolder.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkTab from '@/components/MkTab.vue'; import * as os from '@/os'; import { emojiCategories, emojiTags } from '@/instance'; @@ -44,7 +44,7 @@ export default defineComponent({ MkButton, MkInput, MkSelect, - MkFolder, + MkFoldableSection, MkTab, XEmoji, }, diff --git a/packages/frontend/src/pages/admin/integrations.vue b/packages/frontend/src/pages/admin/integrations.vue index e319d48617..6888a492f6 100644 --- a/packages/frontend/src/pages/admin/integrations.vue +++ b/packages/frontend/src/pages/admin/integrations.vue @@ -4,24 +4,24 @@ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> <div class="_gaps_m"> - <FormFolder> + <MkFolder> <template #icon><i class="ti ti-brand-twitter"></i></template> <template #label>Twitter</template> <template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template> <XTwitter/> - </FormFolder> - <FormFolder> + </MkFolder> + <MkFolder> <template #icon><i class="ti ti-brand-github"></i></template> <template #label>GitHub</template> <template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template> <XGithub/> - </FormFolder> - <FormFolder> + </MkFolder> + <MkFolder> <template #icon><i class="ti ti-brand-discord"></i></template> <template #label>Discord</template> <template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template> <XDiscord/> - </FormFolder> + </MkFolder> </div> </FormSuspense> </MkSpacer> @@ -34,7 +34,7 @@ import XTwitter from './integrations.twitter.vue'; import XGithub from './integrations.github.vue'; import XDiscord from './integrations.discord.vue'; import FormSuspense from '@/components/form/suspense.vue'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue index 2e0b49c5a3..0166724e01 100644 --- a/packages/frontend/src/pages/admin/overview.vue +++ b/packages/frontend/src/pages/admin/overview.vue @@ -1,60 +1,60 @@ <template> <MkSpacer :content-max="1000"> <div ref="rootEl" class="edbbcaef"> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Stats</template> <XStats/> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Active users</template> <XActiveUsers/> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Heatmap</template> <XHeatmap/> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Retention rate</template> <XRetention/> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Moderators</template> <XModerators/> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Federation</template> <XFederation/> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Instances</template> <XInstances/> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Ap requests</template> <XApRequests/> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>New users</template> <XUsers/> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Deliver queue</template> <XQueue domain="deliver"/> - </MkFolder> + </MkFoldableSection> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header>Inbox queue</template> <XQueue domain="inbox"/> - </MkFolder> + </MkFoldableSection> </div> </MkSpacer> </template> @@ -79,7 +79,7 @@ import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import { defaultStore } from '@/store'; import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue'; -import MkFolder from '@/components/MkFolder.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; const rootEl = $shallowRef<HTMLElement>(); let serverInfo: any = $ref(null); diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index b510f5522e..191da506e9 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -4,7 +4,7 @@ <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> <div class="_gaps_m"> - <FormFolder> + <MkFolder> <template #icon><i class="ti ti-shield"></i></template> <template #label>{{ i18n.ts.botProtection }}</template> <template v-if="enableHcaptcha" #suffix>hCaptcha</template> @@ -13,9 +13,9 @@ <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> <XBotProtection/> - </FormFolder> + </MkFolder> - <FormFolder> + <MkFolder> <template #icon><i class="ti ti-eye-off"></i></template> <template #label>{{ i18n.ts.sensitiveMediaDetection }}</template> <template v-if="sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template> @@ -56,9 +56,9 @@ <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> - </FormFolder> + </MkFolder> - <FormFolder> + <MkFolder> <template #label>Active Email Validation</template> <template v-if="enableActiveEmailValidation" #suffix>Enabled</template> <template v-else #suffix>Disabled</template> @@ -69,9 +69,9 @@ <template #label>Enable</template> </MkSwitch> </div> - </FormFolder> + </MkFolder> - <FormFolder> + <MkFolder> <template #label>Log IP address</template> <template v-if="enableIpLogging" #suffix>Enabled</template> <template v-else #suffix>Disabled</template> @@ -81,9 +81,9 @@ <template #label>Enable</template> </MkSwitch> </div> - </FormFolder> + </MkFolder> - <FormFolder> + <MkFolder> <template #label>Summaly Proxy</template> <div class="_gaps_m"> @@ -94,7 +94,7 @@ <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </div> - </FormFolder> + </MkFolder> </div> </FormSuspense> </MkSpacer> @@ -105,7 +105,7 @@ import { } from 'vue'; import XBotProtection from './bot-protection.vue'; import XHeader from './_header_.vue'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormInfo from '@/components/MkInfo.vue'; diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index f1bb0cc62e..3a74e8518d 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -6,52 +6,52 @@ </MkTab> <div v-if="origin === 'local'"> <template v-if="tag == null"> - <MkFolder class="_margin" persist-key="explore-pinned-users"> + <MkFoldableSection class="_margin" persist-key="explore-pinned-users"> <template #header><i class="fas fa-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template> <XUserList :pagination="pinnedUsers"/> - </MkFolder> - <MkFolder class="_margin" persist-key="explore-popular-users"> + </MkFoldableSection> + <MkFoldableSection class="_margin" persist-key="explore-popular-users"> <template #header><i class="fas fa-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> <XUserList :pagination="popularUsers"/> - </MkFolder> - <MkFolder class="_margin" persist-key="explore-recently-updated-users"> + </MkFoldableSection> + <MkFoldableSection class="_margin" persist-key="explore-recently-updated-users"> <template #header><i class="fas fa-comment-alt ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template> <XUserList :pagination="recentlyUpdatedUsers"/> - </MkFolder> - <MkFolder class="_margin" persist-key="explore-recently-registered-users"> + </MkFoldableSection> + <MkFoldableSection class="_margin" persist-key="explore-recently-registered-users"> <template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template> <XUserList :pagination="recentlyRegisteredUsers"/> - </MkFolder> + </MkFoldableSection> </template> </div> <div v-else> - <MkFolder ref="tagsEl" :foldable="true" :expanded="false" class="_margin"> + <MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin"> <template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template> <div class="vxjfqztj"> <MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/explore/tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA> <MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/explore/tags/${tag.tag}`">{{ tag.tag }}</MkA> </div> - </MkFolder> + </MkFoldableSection> - <MkFolder v-if="tag != null" :key="`${tag}`" class="_margin"> + <MkFoldableSection v-if="tag != null" :key="`${tag}`" class="_margin"> <template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template> <XUserList :pagination="tagUsers"/> - </MkFolder> + </MkFoldableSection> <template v-if="tag == null"> - <MkFolder class="_margin"> + <MkFoldableSection class="_margin"> <template #header><i class="fas fa-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> <XUserList :pagination="popularUsersF"/> - </MkFolder> - <MkFolder class="_margin"> + </MkFoldableSection> + <MkFoldableSection class="_margin"> <template #header><i class="fas fa-comment-alt ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template> <XUserList :pagination="recentlyUpdatedUsersF"/> - </MkFolder> - <MkFolder class="_margin"> + </MkFoldableSection> + <MkFoldableSection class="_margin"> <template #header><i class="fas fa-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template> <XUserList :pagination="recentlyRegisteredUsersF"/> - </MkFolder> + </MkFoldableSection> </template> </div> </MkSpacer> @@ -60,7 +60,7 @@ <script lang="ts" setup> import { computed, watch } from 'vue'; import XUserList from '@/components/MkUserList.vue'; -import MkFolder from '@/components/MkFolder.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkTab from '@/components/MkTab.vue'; import number from '@/filters/number'; import * as os from '@/os'; @@ -72,7 +72,7 @@ const props = defineProps<{ }>(); let origin = $ref('local'); -let tagsEl = $shallowRef<InstanceType<typeof MkFolder>>(); +let tagsEl = $shallowRef<InstanceType<typeof MkFoldableSection>>(); let tagsLocal = $ref([]); let tagsRemote = $ref([]); diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue index 12e93b826d..57df58bbb8 100644 --- a/packages/frontend/src/pages/explore.vue +++ b/packages/frontend/src/pages/explore.vue @@ -33,7 +33,7 @@ import { computed, watch } from 'vue'; import XFeatured from './explore.featured.vue'; import XUsers from './explore.users.vue'; -import MkFolder from '@/components/MkFolder.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkInput from '@/components/MkInput.vue'; import MkRadios from '@/components/MkRadios.vue'; import number from '@/filters/number'; @@ -51,7 +51,7 @@ const props = withDefaults(defineProps<{ }); let tab = $ref(props.initialTab); -let tagsEl = $shallowRef<InstanceType<typeof MkFolder>>(); +let tagsEl = $shallowRef<InstanceType<typeof MkFoldableSection>>(); let searchQuery = $ref(null); let searchOrigin = $ref('combined'); diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index c1afaa2d29..ebd470ca4f 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -27,12 +27,12 @@ </div> </div> </Transition> - <FormFolder class="_margin"> + <MkFolder class="_margin"> <template #icon><i class="ti ti-code"></i></template> <template #label>{{ i18n.ts._play.viewSource }}</template> <MkTextarea :model-value="flash.script" readonly tall class="_monospace" spellcheck="false"></MkTextarea> - </FormFolder> + </MkFolder> <div :class="$style.footer"> <Mfm :text="`By @${flash.user.username}`"/> <div class="date"> @@ -65,7 +65,7 @@ import { definePageMetadata } from '@/scripts/page-metadata'; import MkAsUi from '@/components/MkAsUi.vue'; import { AsUiComponent, AsUiRoot, patch, registerAsUiLib, render } from '@/scripts/aiscript/ui'; import { createAiScriptEnv } from '@/scripts/aiscript/api'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; import MkTextarea from '@/components/MkTextarea.vue'; const props = defineProps<{ diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue index 2fc2a8205a..1001c3b2e7 100644 --- a/packages/frontend/src/pages/gallery/index.vue +++ b/packages/frontend/src/pages/gallery/index.vue @@ -4,22 +4,22 @@ <MkSpacer :content-max="1400"> <div class="_root"> <div v-if="tab === 'explore'"> - <MkFolder class="_margin"> + <MkFoldableSection class="_margin"> <template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template> <MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true"> <div class="vfpdbgtk"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> </MkPagination> - </MkFolder> - <MkFolder class="_margin"> + </MkFoldableSection> + <MkFoldableSection class="_margin"> <template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template> <MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true"> <div class="vfpdbgtk"> <MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/> </div> </MkPagination> - </MkFolder> + </MkFoldableSection> </div> <div v-else-if="tab === 'liked'"> <MkPagination v-slot="{items}" :pagination="likedPostsPagination"> @@ -44,7 +44,7 @@ <script lang="ts" setup> import { computed, defineComponent, watch } from 'vue'; import XUserList from '@/components/MkUserList.vue'; -import MkFolder from '@/components/MkFolder.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import MkTab from '@/components/MkTab.vue'; diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index e04fe37ac8..a8274f5601 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -2,24 +2,24 @@ <div class="_gaps_m"> <FormSection first> <template #label><i class="ti ti-pencil"></i> {{ i18n.ts._exportOrImport.allNotes }}</template> - <FormFolder> + <MkFolder> <template #label>{{ i18n.ts.export }}</template> <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportNotes()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> - </FormFolder> + </MkFolder> </FormSection> <FormSection> <template #label><i class="ti ti-star"></i> {{ i18n.ts._exportOrImport.favoritedNotes }}</template> - <FormFolder> + <MkFolder> <template #label>{{ i18n.ts.export }}</template> <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> - </FormFolder> + </MkFolder> </FormSection> <FormSection> <template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.followingList }}</template> <div class="_gaps_s"> - <FormFolder> + <MkFolder> <template #label>{{ i18n.ts.export }}</template> <template #icon><i class="ti ti-download"></i></template> <div class="_gaps_s"> @@ -31,57 +31,57 @@ </MkSwitch> <MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </div> - </FormFolder> - <FormFolder> + </MkFolder> + <MkFolder> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> - </FormFolder> + </MkFolder> </div> </FormSection> <FormSection> <template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.userLists }}</template> <div class="_gaps_s"> - <FormFolder> + <MkFolder> <template #label>{{ i18n.ts.export }}</template> <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> - </FormFolder> - <FormFolder> + </MkFolder> + <MkFolder> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> - </FormFolder> + </MkFolder> </div> </FormSection> <FormSection> <template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.muteList }}</template> <div class="_gaps_s"> - <FormFolder> + <MkFolder> <template #label>{{ i18n.ts.export }}</template> <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> - </FormFolder> - <FormFolder> + </MkFolder> + <MkFolder> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> - </FormFolder> + </MkFolder> </div> </FormSection> <FormSection> <template #label><i class="ti ti-user-off"></i> {{ i18n.ts._exportOrImport.blockingList }}</template> <div class="_gaps_s"> - <FormFolder> + <MkFolder> <template #label>{{ i18n.ts.export }}</template> <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> - </FormFolder> - <FormFolder> + </MkFolder> + <MkFolder> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> - </FormFolder> + </MkFolder> </div> </FormSection> </div> @@ -91,7 +91,7 @@ import { ref } from 'vue'; import MkButton from '@/components/MkButton.vue'; import FormSection from '@/components/form/section.vue'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import * as os from '@/os'; import { selectFile } from '@/scripts/select-file'; diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 9ed53931b8..5692ce80cb 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -32,7 +32,7 @@ <FormSection> <div class="_gaps_m"> <MkSwitch v-model="rememberNoteVisibility" @update:model-value="save()">{{ i18n.ts.rememberNoteVisibility }}</MkSwitch> - <FormFolder v-if="!rememberNoteVisibility"> + <MkFolder v-if="!rememberNoteVisibility"> <template #label>{{ i18n.ts.defaultNoteVisibility }}</template> <template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template> <template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template> @@ -48,7 +48,7 @@ </MkSelect> <MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.localOnly }}</MkSwitch> </div> - </FormFolder> + </MkFolder> </div> </FormSection> @@ -61,7 +61,7 @@ import { } from 'vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import FormSection from '@/components/form/section.vue'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index eb4b929c6f..a9d735c07f 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -33,7 +33,7 @@ </MkSelect> <FormSlot> - <FormFolder> + <MkFolder> <template #icon><i class="ti ti-list"></i></template> <template #label>{{ i18n.ts._profile.metadataEdit }}</template> @@ -51,18 +51,18 @@ <MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> </div> </div> - </FormFolder> + </MkFolder> <template #caption>{{ i18n.ts._profile.metadataDescription }}</template> </FormSlot> - <FormFolder> + <MkFolder> <template #label>{{ i18n.ts.advancedSettings }}</template> <div class="_gaps_m"> <MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch> <MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch> </div> - </FormFolder> + </MkFolder> <MkSwitch v-model="profile.showTimelineReplies">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></MkSwitch> </div> @@ -76,7 +76,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue'; import FormSplit from '@/components/form/split.vue'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; import FormSlot from '@/components/form/slot.vue'; import { host } from '@/config'; import { selectFile } from '@/scripts/select-file'; diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index d93bdc2b1f..f9e301aef9 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -7,12 +7,12 @@ <FormSection> <template #label>{{ i18n.ts.sounds }}</template> <div class="_gaps_s"> - <FormFolder v-for="type in Object.keys(sounds)" :key="type"> + <MkFolder v-for="type in Object.keys(sounds)" :key="type"> <template #label>{{ $t('_sfx.' + type) }}</template> <template #suffix>{{ sounds[type].type ?? i18n.ts.none }}</template> <XSound :type="sounds[type].type" :volume="sounds[type].volume" @update="(res) => updated(type, res)"/> - </FormFolder> + </MkFolder> </div> </FormSection> @@ -27,7 +27,7 @@ import MkRange from '@/components/MkRange.vue'; import MkButton from '@/components/MkButton.vue'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; import * as os from '@/os'; import { ColdDeviceStorage } from '@/store'; import { playFile } from '@/scripts/sound'; diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue index 37dd701fc8..ab081964c2 100644 --- a/packages/frontend/src/pages/settings/statusbar.vue +++ b/packages/frontend/src/pages/settings/statusbar.vue @@ -1,10 +1,10 @@ <template> <div class="_gaps_m"> - <FormFolder v-for="x in statusbars" :key="x.id"> + <MkFolder v-for="x in statusbars" :key="x.id"> <template #label>{{ x.type ?? i18n.ts.notSet }}</template> <template #suffix>{{ x.name }}</template> <XStatusbar :_id="x.id" :user-lists="userLists"/> - </FormFolder> + </MkFolder> <MkButton primary @click="add">{{ i18n.ts.add }}</MkButton> </div> </template> @@ -14,7 +14,7 @@ import { computed, onMounted, ref, watch } from 'vue'; import { v4 as uuid } from 'uuid'; import XStatusbar from './statusbar.statusbar.vue'; import MkRadios from '@/components/MkRadios.vue'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os'; import { defaultStore } from '@/store'; diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue index 9875fb6163..56fdfdf782 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -3,7 +3,7 @@ <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> <div class="cwepdizn _gaps_m"> - <FormFolder :default-open="true"> + <MkFolder :default-open="true"> <template #label>{{ i18n.ts.backgroundColor }}</template> <div class="cwepdizn-colors"> <div class="row"> @@ -17,9 +17,9 @@ </button> </div> </div> - </FormFolder> + </MkFolder> - <FormFolder :default-open="true"> + <MkFolder :default-open="true"> <template #label>{{ i18n.ts.accentColor }}</template> <div class="cwepdizn-colors"> <div class="row"> @@ -28,9 +28,9 @@ </button> </div> </div> - </FormFolder> + </MkFolder> - <FormFolder :default-open="true"> + <MkFolder :default-open="true"> <template #label>{{ i18n.ts.textColor }}</template> <div class="cwepdizn-colors"> <div class="row"> @@ -39,9 +39,9 @@ </button> </div> </div> - </FormFolder> + </MkFolder> - <FormFolder :default-open="false"> + <MkFolder :default-open="false"> <template #icon><i class="ti ti-code"></i></template> <template #label>{{ i18n.ts.editCode }}</template> @@ -51,9 +51,9 @@ </MkTextarea> <MkButton primary @click="applyThemeCode">{{ i18n.ts.apply }}</MkButton> </div> - </FormFolder> + </MkFolder> - <FormFolder :default-open="false"> + <MkFolder :default-open="false"> <template #label>{{ i18n.ts.addDescription }}</template> <div class="_gaps_m"> @@ -61,7 +61,7 @@ <template #label>{{ i18n.ts._theme.description }}</template> </MkTextarea> </div> - </FormFolder> + </MkFolder> </div> </MkSpacer> </MkStickyContainer> @@ -76,7 +76,7 @@ import JSON5 from 'json5'; import MkButton from '@/components/MkButton.vue'; import MkTextarea from '@/components/MkTextarea.vue'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; import { $i } from '@/account'; import { Theme, applyTheme } from '@/scripts/theme'; diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue index 216e1385b0..3d1742cb22 100644 --- a/packages/frontend/src/pages/user-info.vue +++ b/packages/frontend/src/pages/user-info.vue @@ -77,12 +77,12 @@ <MkButton v-if="user.host != null" @click="updateRemoteUser"><i class="ti ti-refresh"></i> {{ i18n.ts.updateRemoteUser }}</MkButton> - <FormFolder> + <MkFolder> <template #label>Raw</template> <MkObjectView v-if="ap" tall :value="ap"> </MkObjectView> - </FormFolder> + </MkFolder> </div> </FormSection> </div> @@ -98,7 +98,7 @@ <MkTextarea v-model="moderationNote" manual-save> <template #label>Moderation note</template> </MkTextarea> - <FormFolder> + <MkFolder> <template #label>IP</template> <MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo> <MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo> @@ -108,12 +108,12 @@ <span class="ip">{{ record.ip }}</span> </div> </template> - </FormFolder> - <FormFolder> + </MkFolder> + <MkFolder> <template #label>{{ i18n.ts.files }}</template> <MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/> - </FormFolder> + </MkFolder> <FormSection> <template #label>Drive Capacity Override</template> @@ -165,7 +165,7 @@ import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import FormSplit from '@/components/form/split.vue'; -import FormFolder from '@/components/form/folder.vue'; +import MkFolder from '@/components/MkFolder.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkSelect from '@/components/MkSelect.vue'; import FormSuspense from '@/components/form/suspense.vue'; diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue index e74b82fb27..6d7c7e7722 100644 --- a/packages/frontend/src/pages/user/activity.vue +++ b/packages/frontend/src/pages/user/activity.vue @@ -1,22 +1,22 @@ <template> <MkSpacer :content-max="700"> <div class="_gaps"> - <MkFolder class="item"> + <MkFoldableSection class="item"> <template #header><i class="ti ti-activity"></i> Heatmap</template> <XHeatmap :user="user" :src="'notes'"/> - </MkFolder> - <MkFolder class="item"> + </MkFoldableSection> + <MkFoldableSection class="item"> <template #header><i class="ti ti-pencil"></i> Notes</template> <XNotes :user="user"/> - </MkFolder> - <MkFolder class="item"> + </MkFoldableSection> + <MkFoldableSection class="item"> <template #header><i class="ti ti-users"></i> Following</template> <XFollowing :user="user"/> - </MkFolder> - <MkFolder class="item"> + </MkFoldableSection> + <MkFoldableSection class="item"> <template #header><i class="ti ti-eye"></i> PV</template> <XPv :user="user"/> - </MkFolder> + </MkFoldableSection> </div> </MkSpacer> </template> @@ -28,7 +28,7 @@ import XHeatmap from './activity.heatmap.vue'; import XPv from './activity.pv.vue'; import XNotes from './activity.notes.vue'; import XFollowing from './activity.following.vue'; -import MkFolder from '@/components/MkFolder.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; const props = defineProps<{ user: misskey.entities.User; diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 53ae6a2c53..07d34a794d 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -115,7 +115,7 @@ import XUserTimeline from './index.timeline.vue'; import XNote from '@/components/MkNote.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import MkContainer from '@/components/MkContainer.vue'; -import MkFolder from '@/components/MkFolder.vue'; +import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; import MkTab from '@/components/MkTab.vue'; import MkInfo from '@/components/MkInfo.vue'; From 2817ca03f59a53f70d81dd7718cc2dcbd2c85ed8 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 09:42:11 +0900 Subject: [PATCH 41/68] Update queue.chart.vue --- .../frontend/src/pages/admin/queue.chart.vue | 73 +++++++++++-------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue index 7f1dc2cd77..100d1ea545 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -1,12 +1,10 @@ <template> -<div class="pumxzjhg"> - <div class="_table status"> - <div class="_row"> - <div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div> - <div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div> - <div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div> - <div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div> - </div> +<div class="pumxzjhg _gaps"> + <div :class="$style.status"> + <div class="item _panel"><div class="label">Process</div>{{ number(activeSincePrevTick) }}</div> + <div class="item _panel"><div class="label">Active</div>{{ number(active) }}</div> + <div class="item _panel"><div class="label">Waiting</div>{{ number(waiting) }}</div> + <div class="item _panel"><div class="label">Delayed</div>{{ number(delayed) }}</div> </div> <div class="charts"> <div class="chart"> @@ -26,15 +24,21 @@ <XChart ref="chartWaiting" type="waiting"/> </div> </div> - <div class="jobs"> - <div v-if="jobs.length > 0"> - <div v-for="job in jobs" :key="job[0]"> - <span>{{ job[0] }}</span> - <span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span> + <MkFolder :default-open="true" :max-height="250"> + <template #icon><i class="ti ti-alert-triangle"></i></template> + <template #label>Errored instances</template> + <template #suffix>({{ number(jobs.reduce((a, b) => a + b[1], 0)) }} jobs)</template> + + <div :class="$style.jobs"> + <div v-if="jobs.length > 0"> + <div v-for="job in jobs" :key="job[0]"> + <MkA :to="`/instance-info/${job[0]}`" behavior="window">{{ job[0] }}</MkA> + <span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span> + </div> </div> + <span v-else style="opacity: 0.5;">{{ i18n.ts.noJobs }}</span> </div> - <span v-else style="opacity: 0.5;">{{ i18n.ts.noJobs }}</span> - </div> + </MkFolder> </div> </template> @@ -45,6 +49,7 @@ import number from '@/filters/number'; import * as os from '@/os'; import { stream } from '@/stream'; import { i18n } from '@/i18n'; +import MkFolder from '@/components/MkFolder.vue'; const connection = markRaw(stream.useChannel('queueStats')); @@ -115,14 +120,10 @@ onUnmounted(() => { <style lang="scss" scoped> .pumxzjhg { - > .status { - padding: 16px; - } - > .charts { display: grid; grid-template-columns: 1fr 1fr; - gap: 16px; + gap: 10px; > .chart { min-width: 0; @@ -135,15 +136,27 @@ onUnmounted(() => { } } } - - > .jobs { - margin-top: 16px; - padding: 16px; - max-height: 180px; - overflow: auto; - background: var(--panel); - border-radius: var(--radius); - } - +} +</style> + +<style lang="scss" module> +.status { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + grid-gap: 10px; + + &:global { + > .item { + padding: 12px 16px; + + > .label { + font-size: 80%; + opacity: 0.6; + } + } + } +} + +.jobs { } </style> From 5ce56886a16ee773e60a44ec4f6a8d5f17c77635 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 09:43:28 +0900 Subject: [PATCH 42/68] fix --- packages/frontend/src/pages/about.federation.vue | 2 +- packages/frontend/src/pages/admin/users.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index a4e449911c..101ea2297b 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -37,7 +37,7 @@ <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> <div class="dqokceoi"> <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`"> - <MkInstanceCardMini v-once :instance="instance"/> + <MkInstanceCardMini :instance="instance"/> </MkA> </div> </MkPagination> diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue index 445a0a3a71..babe76e4ec 100644 --- a/packages/frontend/src/pages/admin/users.vue +++ b/packages/frontend/src/pages/admin/users.vue @@ -42,7 +42,7 @@ <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users"> <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`"> - <MkUserCardMini v-once :user="user"/> + <MkUserCardMini :user="user"/> </MkA> </MkPagination> </div> From 13aa4b64b44d7fe2a44c3e2722e2edfcecb57449 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 10:07:37 +0900 Subject: [PATCH 43/68] tweak client --- .../frontend/src/pages/admin/federation.vue | 123 ++++++++++++++++++ packages/frontend/src/pages/admin/index.vue | 2 +- packages/frontend/src/router.ts | 4 + 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/src/pages/admin/federation.vue diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue new file mode 100644 index 0000000000..96bd087e66 --- /dev/null +++ b/packages/frontend/src/pages/admin/federation.vue @@ -0,0 +1,123 @@ +<template> +<div> + <MkStickyContainer> + <template #header><XHeader :actions="headerActions"/></template> + <MkSpacer :content-max="900"> + <div class="taeiyrib"> + <div class="query"> + <MkInput v-model="host" :debounce="true" class=""> + <template #prefix><i class="ti ti-search"></i></template> + <template #label>{{ i18n.ts.host }}</template> + </MkInput> + <FormSplit style="margin-top: var(--margin);"> + <MkSelect v-model="state"> + <template #label>{{ i18n.ts.state }}</template> + <option value="all">{{ i18n.ts.all }}</option> + <option value="federating">{{ i18n.ts.federating }}</option> + <option value="subscribing">{{ i18n.ts.subscribing }}</option> + <option value="publishing">{{ i18n.ts.publishing }}</option> + <option value="suspended">{{ i18n.ts.suspended }}</option> + <option value="blocked">{{ i18n.ts.blocked }}</option> + <option value="notResponding">{{ i18n.ts.notResponding }}</option> + </MkSelect> + <MkSelect v-model="sort"> + <template #label>{{ i18n.ts.sort }}</template> + <option value="+pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.descendingOrder }})</option> + <option value="-pubSub">{{ i18n.ts.pubSub }} ({{ i18n.ts.ascendingOrder }})</option> + <option value="+notes">{{ i18n.ts.notes }} ({{ i18n.ts.descendingOrder }})</option> + <option value="-notes">{{ i18n.ts.notes }} ({{ i18n.ts.ascendingOrder }})</option> + <option value="+users">{{ i18n.ts.users }} ({{ i18n.ts.descendingOrder }})</option> + <option value="-users">{{ i18n.ts.users }} ({{ i18n.ts.ascendingOrder }})</option> + <option value="+following">{{ i18n.ts.following }} ({{ i18n.ts.descendingOrder }})</option> + <option value="-following">{{ i18n.ts.following }} ({{ i18n.ts.ascendingOrder }})</option> + <option value="+followers">{{ i18n.ts.followers }} ({{ i18n.ts.descendingOrder }})</option> + <option value="-followers">{{ i18n.ts.followers }} ({{ i18n.ts.ascendingOrder }})</option> + <option value="+caughtAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.descendingOrder }})</option> + <option value="-caughtAt">{{ i18n.ts.registeredAt }} ({{ i18n.ts.ascendingOrder }})</option> + </MkSelect> + </FormSplit> + </div> + + <MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination"> + <div class="dqokceoj"> + <MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`"> + <MkInstanceCardMini :instance="instance"/> + </MkA> + </div> + </MkPagination> + </div> + </MkSpacer> + </MkStickyContainer> +</div> +</template> + +<script lang="ts" setup> +import { computed } from 'vue'; +import XHeader from './_header_.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import MkPagination from '@/components/MkPagination.vue'; +import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue'; +import FormSplit from '@/components/form/split.vue'; +import * as os from '@/os'; +import { i18n } from '@/i18n'; +import { dateString } from '@/filters/date'; +import { definePageMetadata } from '@/scripts/page-metadata'; + +let host = $ref(''); +let state = $ref('federating'); +let sort = $ref('+pubSub'); +const pagination = { + endpoint: 'federation/instances' as const, + limit: 10, + offsetMode: true, + params: computed(() => ({ + sort: sort, + host: host !== '' ? host : null, + ...( + state === 'federating' ? { federating: true } : + state === 'subscribing' ? { subscribing: true } : + state === 'publishing' ? { publishing: true } : + state === 'suspended' ? { suspended: true } : + state === 'blocked' ? { blocked: true } : + state === 'notResponding' ? { notResponding: true } : + {}), + })), +}; + +function getStatus(instance) { + if (instance.isSuspended) return 'Suspended'; + if (instance.isBlocked) return 'Blocked'; + if (instance.isNotResponding) return 'Error'; + return 'Alive'; +} + +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata(computed(() => ({ + title: i18n.ts.federation, + icon: 'ti ti-whirl', +}))); +</script> + +<style lang="scss" scoped> +.taeiyrib { + > .query { + background: var(--bg); + margin-bottom: 16px; + } +} + +.dqokceoj { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); + grid-gap: 12px; + + > .instance:hover { + text-decoration: none; + } +} +</style> diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index c71ebd4e2e..c90a1c1b00 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -104,7 +104,7 @@ const menuDef = $computed(() => [{ }, { icon: 'ti ti-whirl', text: i18n.ts.federation, - to: '/about#federation', + to: '/admin/federation', active: currentPage?.route.name === 'federation', }, { icon: 'ti ti-clock-play', diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index bfa4a3ceab..4b9f49f8fd 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -339,6 +339,10 @@ export const routes = [{ path: '/files', name: 'files', component: page(() => import('./pages/admin/files.vue')), + }, { + path: '/federation', + name: 'federation', + component: page(() => import('./pages/admin/federation.vue')), }, { path: '/announcements', name: 'announcements', From 962373cf06f233f7a3c3a5dbe25c558752d8b2d7 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 13:26:42 +0900 Subject: [PATCH 44/68] =?UTF-8?q?fix(server):=20=E9=9D=9E=E5=85=AC?= =?UTF-8?q?=E9=96=8B=E3=81=AE=E3=82=AF=E3=83=AA=E3=83=83=E3=83=97=E3=81=AE?= =?UTF-8?q?URL=E3=81=A7OGP=E3=83=AC=E3=83=B3=E3=83=80=E3=83=AA=E3=83=B3?= =?UTF-8?q?=E3=82=B0=E3=81=95=E3=82=8C=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #9129 --- CHANGELOG.md | 1 + packages/backend/src/server/web/ClientServerService.ts | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d19545c7d9..f468bc3cbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ You should also include the user name that made the change. - Server: pages/likeのエラーIDが重複しているのを修正 @syuilo - Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo - Server: Escape SQL LIKE @mei23 +- Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo - Client: case insensitive emoji search @saschanaz - Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina - Client: use proxied image for instance icon @syuilo diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index c69975fd79..5c29224019 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -312,7 +312,7 @@ export class ClientServerService { fastify.get('/opensearch.xml', async (request, reply) => { const meta = await this.metaService.fetch(); - const name = meta.name || 'Misskey'; + const name = meta.name ?? 'Misskey'; let content = ''; content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">'; content += `<ShortName>${name}</ShortName>`; @@ -533,13 +533,12 @@ export class ClientServerService { }); // Clip - // TODO: 非publicなclipのハンドリング fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => { const clip = await this.clipsRepository.findOneBy({ id: request.params.clip, }); - if (clip) { + if (clip && clip.isPublic) { const _clip = await this.clipEntityService.pack(clip); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); const meta = await this.metaService.fetch(); From dfee79f8416da0966276d83654165a1adfc424b9 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 13:32:48 +0900 Subject: [PATCH 45/68] :art: Resolve #9498 --- packages/frontend/src/components/MkNote.vue | 2 +- packages/frontend/src/components/MkNoteDetailed.vue | 4 ++-- packages/frontend/src/components/MkNoteHeader.vue | 2 +- packages/frontend/src/components/MkPostForm.vue | 2 +- packages/frontend/src/components/MkVisibilityPicker.vue | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index cb97f19d31..45aba1d24b 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -29,7 +29,7 @@ </button> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> <i v-if="note.visibility === 'home'" class="ti ti-home"></i> - <i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 4dc1358b24..f0493ba357 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -27,7 +27,7 @@ </button> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> <i v-if="note.visibility === 'home'" class="ti ti-home"></i> - <i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> @@ -45,7 +45,7 @@ <div class="info"> <span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]"> <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i> - <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock-open"></i> + <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i> <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index ddd25e784f..38bd33ff74 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -11,7 +11,7 @@ </MkA> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> <i v-if="note.visibility === 'home'" class="ti ti-home"></i> - <i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i> + <i v-else-if="note.visibility === 'followers'" class="ti ti-lock"></i> <i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i> </span> <span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span> diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index ff3b7ec1f5..4904440533 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -18,7 +18,7 @@ <button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility"> <span v-if="visibility === 'public'"><i class="ti ti-world"></i></span> <span v-if="visibility === 'home'"><i class="ti ti-home"></i></span> - <span v-if="visibility === 'followers'"><i class="ti ti-lock-open"></i></span> + <span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span> <span v-if="visibility === 'specified'"><i class="ti ti-mail"></i></span> </button> <button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button> diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue index 4c78659f5c..b05590e5ee 100644 --- a/packages/frontend/src/components/MkVisibilityPicker.vue +++ b/packages/frontend/src/components/MkVisibilityPicker.vue @@ -16,7 +16,7 @@ </div> </button> <button key="followers" class="_button item" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')"> - <div class="icon"><i class="ti ti-lock-open"></i></div> + <div class="icon"><i class="ti ti-lock"></i></div> <div class="body"> <span>{{ i18n.ts._visibility.followers }}</span> <span>{{ i18n.ts._visibility.followersDescription }}</span> From 4c9b93a12f1d9b4b8659d7663eb5e12d218560fe Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 13:39:16 +0900 Subject: [PATCH 46/68] :art: --- packages/frontend/src/ui/_common_/common.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index c3b22cd9e1..c63e962a8d 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -48,21 +48,11 @@ export function openInstanceMenu(ev: MouseEvent) { icon: 'ti ti-cookie', }], }, null, { - type: 'parent', text: i18n.ts.help, icon: 'ti ti-question-circle', - children: [{ - type: 'link', - to: '/mfm-cheat-sheet', - text: i18n.ts._mfm.cheatSheet, - icon: 'ti ti-code', - }, null, { - text: i18n.ts.document, - icon: 'ti ti-question-circle', - action: () => { - window.open('https://misskey-hub.net/help.html', '_blank'); - }, - }], + action: () => { + window.open('https://misskey-hub.net/help.html', '_blank'); + }, }, { type: 'link', text: i18n.ts.aboutMisskey, From eba6b326fac926259d064e93182c2bede206d12c Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 13:45:04 +0900 Subject: [PATCH 47/68] 13.0.0-beta.32 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 277389de52..4e72612e0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0-beta.31", + "version": "13.0.0-beta.32", "codename": "indigo", "repository": { "type": "git", From 402b234d158b3a9436765782d514d8bafa7e0245 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 13:56:30 +0900 Subject: [PATCH 48/68] :art: --- packages/frontend/src/pages/emojis.emoji.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index f27d2af858..3393675a3c 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -49,6 +49,7 @@ function menu(ev) { > .img { width: 42px; height: 42px; + object-fit: contain; } > .body { From 403849805aa222e584f83116df9a25fbecc19080 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 14:00:04 +0900 Subject: [PATCH 49/68] enhance(client): force lazy load some images --- packages/frontend/src/components/MkInstanceCardMini.vue | 2 +- packages/frontend/src/pages/emojis.emoji.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue index 4625de40af..3de3f8de65 100644 --- a/packages/frontend/src/components/MkInstanceCardMini.vue +++ b/packages/frontend/src/components/MkInstanceCardMini.vue @@ -1,6 +1,6 @@ <template> <div :class="[$style.root, { yellow: instance.isNotResponding, red: instance.isBlocked, gray: instance.isSuspended }]"> - <img class="icon" :src="getInstanceIcon(instance)" alt=""/> + <img class="icon" :src="getInstanceIcon(instance)" alt="" loading="lazy"/> <div class="body"> <span class="host">{{ instance.name ?? instance.host }}</span> <span class="sub _monospace"><b>{{ instance.host }}</b> / {{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</span> diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index 3393675a3c..fb3bce570c 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -1,6 +1,6 @@ <template> <button class="zuvgdzyu _button" @click="menu"> - <img :src="`/emoji/${emoji.name}.webp`" class="img" :alt="emoji.name"/> + <img :src="`/emoji/${emoji.name}.webp`" class="img" loading="lazy"/> <div class="body"> <div class="name _monospace">{{ emoji.name }}</div> <div class="info">{{ emoji.aliases.join(' ') }}</div> From 3cfd017538d87230ab36c42fe1392a1880ec2297 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 14:03:22 +0900 Subject: [PATCH 50/68] =?UTF-8?q?fix(server):=20=E7=89=B9=E5=AE=9A?= =?UTF-8?q?=E3=81=AEPNG=E7=94=BB=E5=83=8F=E3=81=AE=E3=82=A2=E3=83=83?= =?UTF-8?q?=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89=E3=81=AB=E5=A4=B1=E6=95=97?= =?UTF-8?q?=E3=81=99=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: haru <64310155+usbharu@users.noreply.github.com> --- CHANGELOG.md | 1 + packages/backend/src/core/FileInfoService.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f468bc3cbb..44b5c30dcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ You should also include the user name that made the change. - Server: pages/likeのエラーIDが重複しているのを修正 @syuilo - Server: pages/updateのパラメータによってはsummaryの値が更新されないのを修正 @syuilo - Server: Escape SQL LIKE @mei23 +- Server: 特定のPNG画像のアップロードに失敗する問題を修正 @usbharu - Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo - Client: case insensitive emoji search @saschanaz - Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index dad94da421..67337b5056 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -398,13 +398,13 @@ export class FileInfoService { .raw() .ensureAlpha() .resize(64, 64, { fit: 'inside' }) - .toBuffer((err, buffer, { width, height }) => { + .toBuffer((err, buffer, info) => { if (err) return reject(err); let hash; try { - hash = encode(new Uint8ClampedArray(buffer), width, height, 5, 5); + hash = encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5); } catch (e) { return reject(e); } From e4144a17a44b98229d0c0b18969d3d86a768a43d Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 14:12:42 +0900 Subject: [PATCH 51/68] =?UTF-8?q?fix(server):=20=E3=82=A2=E3=83=B3?= =?UTF-8?q?=E3=83=86=E3=83=8A=E3=82=BF=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=83=B3=EF=BC=88=E3=82=B9=E3=83=88=E3=83=AA=E3=83=BC=E3=83=9F?= =?UTF-8?q?=E3=83=B3=E3=82=B0=EF=BC=89=E3=81=8C=E3=80=81=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=81=97=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=AE=E9=8D=B5=E6=8A=95?= =?UTF-8?q?=E7=A8=BF=E3=82=82=E6=8B=BE=E3=81=A3=E3=81=A6=E3=81=97=E3=81=BE?= =?UTF-8?q?=E3=81=86=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #9025 --- CHANGELOG.md | 1 + packages/backend/src/core/AntennaService.ts | 20 ++++++-------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b5c30dcd..269f8b1000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,7 @@ You should also include the user name that made the change. - Server: Escape SQL LIKE @mei23 - Server: 特定のPNG画像のアップロードに失敗する問題を修正 @usbharu - Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo +- Server: アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう @syuilo - Client: case insensitive emoji search @saschanaz - Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina - Client: use proxied image for instance icon @syuilo diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index d6f326f616..1b5abce29a 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -15,8 +15,8 @@ import type { Packed } from '@/misc/schema.js'; import { DI } from '@/di-symbols.js'; import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class AntennaService implements OnApplicationShutdown { @@ -135,7 +135,7 @@ export class AntennaService implements OnApplicationShutdown { this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna); this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', { antenna: { id: antenna.id, name: antenna.name }, - note: await this.noteEntityService.pack(note) + note: await this.noteEntityService.pack(note), }); } }, 2000); @@ -144,27 +144,19 @@ export class AntennaService implements OnApplicationShutdown { // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている - /** - * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい - */ @bindThis - public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> { + public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> { if (note.visibility === 'specified') return false; - + if (note.visibility === 'followers') return false; + // アンテナ作成者がノート作成者にブロックされていたらスキップ const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId))); if (blockings.some(blocking => blocking === antenna.userId)) return false; - if (note.visibility === 'followers') { - if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; - if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; - } - if (!antenna.withReplies && note.replyId != null) return false; if (antenna.src === 'home') { - if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; - if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; + // TODO } else if (antenna.src === 'list') { const listUsers = (await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId!, From 462acc9eee3f89a926fd4b46ffed0b066519759b Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 15:50:25 +0900 Subject: [PATCH 52/68] =?UTF-8?q?=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0?= =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E4=B8=80=E8=A6=A7=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E3=82=92meta=E3=81=8B=E3=82=89=E5=88=86=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 + .../src/core/entities/EmojiEntityService.ts | 8 +- packages/backend/src/models/schema/emoji.ts | 8 +- .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../src/server/api/endpoints/emojis.ts | 91 +++++++++++++++++++ .../backend/src/server/api/endpoints/meta.ts | 57 ------------ .../src/components/MkAutocomplete.vue | 5 +- .../frontend/src/components/MkEmojiPicker.vue | 9 +- packages/frontend/src/custom-emojis.ts | 48 ++++++++++ packages/frontend/src/instance.ts | 24 +---- packages/frontend/src/local-storage.ts | 2 + packages/frontend/src/pages/about.emojis.vue | 87 +++++++----------- .../src/pages/admin/emoji-edit-dialog.vue | 4 +- .../src/pages/admin/overview.stats.vue | 5 +- .../frontend/src/pages/mfm-cheat-sheet.vue | 5 +- .../frontend/src/pages/settings/index.vue | 2 + 17 files changed, 212 insertions(+), 151 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/emojis.ts create mode 100644 packages/frontend/src/custom-emojis.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 269f8b1000..43cf0ef6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ You should also include the user name that made the change. - Firefox109以下はサポートされなくなりました #### For app developers +- API: metaのレスポンスに`emojis`プロパティが含まれなくなりました + - カスタム絵文字一覧情報を取得するには、`emojis`エンドポイントにリクエストします - API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました - 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。 - e.g. `https://p1.a9z.dev/emoji/misskey.webp` diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 8a2dc70eda..2a4e09519f 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -22,23 +22,25 @@ export class EmojiEntityService { @bindThis public async pack( src: Emoji['id'] | Emoji, + opts: { omitHost?: boolean; omitId?: boolean; } = {}, ): Promise<Packed<'Emoji'>> { const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); return { - id: emoji.id, + id: opts.omitId ? undefined : emoji.id, aliases: emoji.aliases, name: emoji.name, category: emoji.category, - host: emoji.host, + host: opts.omitHost ? undefined : emoji.host, }; } @bindThis public packMany( emojis: any[], + opts: { omitHost?: boolean; omitId?: boolean; } = {}, ) { - return Promise.all(emojis.map(x => this.pack(x))); + return Promise.all(emojis.map(x => this.pack(x, opts))); } } diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/backend/src/models/schema/emoji.ts index 9a52609b68..d897a0fc05 100644 --- a/packages/backend/src/models/schema/emoji.ts +++ b/packages/backend/src/models/schema/emoji.ts @@ -3,7 +3,7 @@ export const packedEmojiSchema = { properties: { id: { type: 'string', - optional: false, nullable: false, + optional: true, nullable: false, format: 'id', example: 'xxxxxxxxxx', }, @@ -26,12 +26,8 @@ export const packedEmojiSchema = { }, host: { type: 'string', - optional: false, nullable: true, + optional: true, nullable: true, description: 'The local host is represented with `null`.', }, - url: { - type: 'string', - optional: true, nullable: false, - }, }, } as const; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 60beca4f47..ab9349966d 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -220,6 +220,7 @@ import * as ep___messaging_messages_create from './endpoints/messaging/messages/ import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; import * as ep___meta from './endpoints/meta.js'; +import * as ep___emojis from './endpoints/emojis.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___mute_create from './endpoints/mute/create.js'; import * as ep___mute_delete from './endpoints/mute/delete.js'; @@ -550,6 +551,7 @@ const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/c const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default }; const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default }; const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default }; +const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default }; const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default }; const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default }; const $mute_delete: Provider = { provide: 'ep:mute/delete', useClass: ep___mute_delete.default }; @@ -884,6 +886,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $messaging_messages_delete, $messaging_messages_read, $meta, + $emojis, $miauth_genToken, $mute_create, $mute_delete, @@ -1212,6 +1215,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $messaging_messages_delete, $messaging_messages_read, $meta, + $emojis, $miauth_genToken, $mute_create, $mute_delete, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index d4f8be5b85..f9749ad660 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -219,6 +219,7 @@ import * as ep___messaging_messages_create from './endpoints/messaging/messages/ import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js'; import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js'; import * as ep___meta from './endpoints/meta.js'; +import * as ep___emojis from './endpoints/emojis.js'; import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js'; import * as ep___mute_create from './endpoints/mute/create.js'; import * as ep___mute_delete from './endpoints/mute/delete.js'; @@ -547,6 +548,7 @@ const eps = [ ['messaging/messages/delete', ep___messaging_messages_delete], ['messaging/messages/read', ep___messaging_messages_read], ['meta', ep___meta], + ['emojis', ep___emojis], ['miauth/gen-token', ep___miauth_genToken], ['mute/create', ep___mute_create], ['mute/delete', ep___mute_delete], diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts new file mode 100644 index 0000000000..0a16268229 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -0,0 +1,91 @@ +import { IsNull, MoreThan } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import type { EmojisRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import type { Config } from '@/config.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['meta'], + + requireCredential: false, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + emojis: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + aliases: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + category: { + type: 'string', + optional: false, nullable: true, + }, + }, + }, + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.config) + private config: Config, + + @Inject(DI.emojisRepository) + private emojisRepository: EmojisRepository, + + private emojiEntityService: EmojiEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const emojis = await this.emojisRepository.find({ + where: { + host: IsNull(), + }, + order: { + category: 'ASC', + name: 'ASC', + }, + cache: { + id: 'meta_emojis', + milliseconds: 3600000, // 1 hour + }, + }); + + return { + emojis: await this.emojiEntityService.packMany(emojis, { + omitId: true, + omitHost: true, + }), + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 05da011979..c44d63d64b 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -4,7 +4,6 @@ import type { AdsRepository, EmojisRepository, UsersRepository } from '@/models/ import { MAX_NOTE_TEXT_LENGTH, DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { MetaService } from '@/core/MetaService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; @@ -152,43 +151,6 @@ export const meta = { type: 'number', optional: false, nullable: false, }, - emojis: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - aliases: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'string', - optional: false, nullable: false, - }, - }, - category: { - type: 'string', - optional: false, nullable: true, - }, - host: { - type: 'string', - optional: false, nullable: true, - description: 'The local host is represented with `null`.', - }, - url: { - type: 'string', - optional: false, nullable: false, - format: 'url', - }, - }, - }, - }, ads: { type: 'array', optional: false, nullable: false, @@ -326,30 +288,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { @Inject(DI.adsRepository) private adsRepository: AdsRepository, - @Inject(DI.emojisRepository) - private emojisRepository: EmojisRepository, - private userEntityService: UserEntityService, - private emojiEntityService: EmojiEntityService, private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const instance = await this.metaService.fetch(true); - const emojis = await this.emojisRepository.find({ - where: { - host: IsNull(), - }, - order: { - category: 'ASC', - name: 'ASC', - }, - cache: { - id: 'meta_emojis', - milliseconds: 3600000, // 1 hour - }, - }); - const ads = await this.adsRepository.find({ where: { expiresAt: MoreThan(new Date()), @@ -390,7 +334,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため - emojis: await this.emojiEntityService.packMany(emojis), defaultLightTheme: instance.defaultLightTheme, defaultDarkTheme: instance.defaultDarkTheme, ads: ads.map(ad => ({ diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 8ed60bc5dc..0d0c5edbae 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -47,6 +47,9 @@ import { emojilist } from '@/scripts/emojilist'; import { instance } from '@/instance'; import { i18n } from '@/i18n'; import { miLocalStorage } from '@/local-storage'; +import { getCustomEmojis } from '@/custom-emojis'; + +const customEmojis = await getCustomEmojis(); type EmojiDef = { emoji: string; @@ -86,7 +89,6 @@ for (const x of lib) { emjdb.sort((a, b) => a.name.length - b.name.length); //#region Construct Emoji DB -const customEmojis = instance.emojis; const emojiDefinitions: EmojiDef[] = []; for (const x of customEmojis) { @@ -117,7 +119,6 @@ export default { emojiDb, emojiDefinitions, emojilist, - customEmojis, }; </script> diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index f3b3ac0b50..8df01f6c25 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -6,7 +6,7 @@ <div v-if="searchResultCustom.length > 0" class="body"> <button v-for="emoji in searchResultCustom" - :key="emoji.id" + :key="emoji.name" class="_button item" :title="emoji.name" tabindex="0" @@ -85,9 +85,10 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; import { deviceKind } from '@/scripts/device-kind'; -import { emojiCategories, instance } from '@/instance'; +import { instance } from '@/instance'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; +import { getCustomEmojiCategories, getCustomEmojis } from '@/custom-emojis'; const props = withDefaults(defineProps<{ showPinned?: boolean; @@ -103,6 +104,7 @@ const emit = defineEmits<{ (ev: 'chosen', v: string): void; }>(); +const customEmojis = await getCustomEmojis(); const search = shallowRef<HTMLInputElement>(); const emojis = shallowRef<HTMLDivElement>(); @@ -118,8 +120,7 @@ const { const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1); const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3); const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2); -const customEmojiCategories = emojiCategories; -const customEmojis = instance.emojis; +const customEmojiCategories = await getCustomEmojiCategories(); const q = ref<string>(''); const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]); const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts new file mode 100644 index 0000000000..2a52753f84 --- /dev/null +++ b/packages/frontend/src/custom-emojis.ts @@ -0,0 +1,48 @@ +import { api } from './os'; +import { miLocalStorage } from './local-storage'; + +const storageCache = miLocalStorage.getItem('emojis'); +let cached = storageCache ? JSON.parse(storageCache) : null; +export async function getCustomEmojis() { + const now = Date.now(); + const lastFetchedAt = miLocalStorage.getItem('lastEmojisFetchedAt'); + if (cached && lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60) return cached; + + const res = await api('emojis', {}); + + cached = res.emojis; + miLocalStorage.setItem('emojis', JSON.stringify(cached)); + miLocalStorage.setItem('lastEmojisFetchedAt', now.toString()); +} + +let cachedCategories; +export async function getCustomEmojiCategories() { + if (cachedCategories) return cachedCategories; + + const customEmojis = await getCustomEmojis(); + + const categories = new Set(); + for (const emoji of customEmojis) { + categories.add(emoji.category); + } + const res = Array.from(categories); + cachedCategories = res; + return res; +} + +let cachedTags; +export async function getCustomEmojiTags() { + if (cachedTags) return cachedTags; + + const customEmojis = await getCustomEmojis(); + + const tags = new Set(); + for (const emoji of customEmojis) { + for (const tag of emoji.aliases) { + tags.add(tag); + } + } + const res = Array.from(tags); + cachedTags = res; + return res; +} diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts index 82d3e7aea2..08dbd9737c 100644 --- a/packages/frontend/src/instance.ts +++ b/packages/frontend/src/instance.ts @@ -5,11 +5,11 @@ import { miLocalStorage } from './local-storage'; // TODO: 他のタブと永続化されたstateを同期 -const instanceData = miLocalStorage.getItem('instance'); +const cached = miLocalStorage.getItem('instance'); // TODO: instanceをリアクティブにするかは再考の余地あり -export const instance: Misskey.entities.InstanceMetadata = reactive(instanceData ? JSON.parse(instanceData) : { +export const instance: Misskey.entities.InstanceMetadata = reactive(cached ? JSON.parse(cached) : { // TODO: set default values }); @@ -24,23 +24,3 @@ export async function fetchInstance() { miLocalStorage.setItem('instance', JSON.stringify(instance)); } - -export const emojiCategories = computed(() => { - if (instance.emojis == null) return []; - const categories = new Set(); - for (const emoji of instance.emojis) { - categories.add(emoji.category); - } - return Array.from(categories); -}); - -export const emojiTags = computed(() => { - if (instance.emojis == null) return []; - const tags = new Set(); - for (const emoji of instance.emojis) { - for (const tag of emoji.aliases) { - tags.add(tag); - } - } - return Array.from(tags); -}); diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 50e28d621f..bb8192e980 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -2,6 +2,8 @@ type Keys = 'v' | 'lastVersion' | 'instance' | + 'emojis' | // TODO: indexed db + 'lastEmojisFetchedAt' | 'account' | 'accounts' | 'latestDonationInfoShownAt' | diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index e15a2b1bdf..acf6237c87 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -7,7 +7,7 @@ <!-- たくさんあると邪魔 <div class="tags"> - <span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span> + <span class="tag _button" v-for="tag in customEmojiTags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span> </div> --> </div> @@ -28,8 +28,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent, computed } from 'vue'; +<script lang="ts" setup> +import { defineComponent, computed, watch } from 'vue'; import XEmoji from './emojis.emoji.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; @@ -37,62 +37,43 @@ import MkSelect from '@/components/MkSelect.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkTab from '@/components/MkTab.vue'; import * as os from '@/os'; -import { emojiCategories, emojiTags } from '@/instance'; +import { getCustomEmojis, getCustomEmojiCategories, getCustomEmojiTags } from '@/custom-emojis'; -export default defineComponent({ - components: { - MkButton, - MkInput, - MkSelect, - MkFoldableSection, - MkTab, - XEmoji, - }, +const customEmojis = await getCustomEmojis(); +const customEmojiCategories = await getCustomEmojiCategories(); +const customEmojiTags = await getCustomEmojiTags(); +let q = $ref(''); +let searchEmojis = $ref(null); +let selectedTags = $ref(new Set()); - data() { - return { - q: '', - customEmojiCategories: emojiCategories, - customEmojis: this.$instance.emojis, - tags: emojiTags, - selectedTags: new Set(), - searchEmojis: null, - }; - }, +function search() { + if ((q === '' || q == null) && selectedTags.size === 0) { + searchEmojis = null; + return; + } - watch: { - q() { this.search(); }, - selectedTags: { - handler() { - this.search(); - }, - deep: true, - }, - }, + if (selectedTags.size === 0) { + searchEmojis = customEmojis.filter(emoji => emoji.name.includes(q) || emoji.aliases.includes(q)); + } else { + searchEmojis = customEmojis.filter(emoji => (emoji.name.includes(q) || emoji.aliases.includes(q)) && [...selectedTags].every(t => emoji.aliases.includes(t))); + } +} - methods: { - search() { - if ((this.q === '' || this.q == null) && this.selectedTags.size === 0) { - this.searchEmojis = null; - return; - } +function toggleTag(tag) { + if (selectedTags.has(tag)) { + selectedTags.delete(tag); + } else { + selectedTags.add(tag); + } +} - if (this.selectedTags.size === 0) { - this.searchEmojis = this.customEmojis.filter(emoji => emoji.name.includes(this.q) || emoji.aliases.includes(this.q)); - } else { - this.searchEmojis = this.customEmojis.filter(emoji => (emoji.name.includes(this.q) || emoji.aliases.includes(this.q)) && [...this.selectedTags].every(t => emoji.aliases.includes(t))); - } - }, - - toggleTag(tag) { - if (this.selectedTags.has(tag)) { - this.selectedTags.delete(tag); - } else { - this.selectedTags.add(tag); - } - }, - }, +watch($$(q), () => { + search(); }); + +watch($$(selectedTags), () => { + search(); +}, { deep: true }); </script> <style lang="scss" scoped> diff --git a/packages/frontend/src/pages/admin/emoji-edit-dialog.vue b/packages/frontend/src/pages/admin/emoji-edit-dialog.vue index c0a997d7b4..0b6a5e1557 100644 --- a/packages/frontend/src/pages/admin/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/admin/emoji-edit-dialog.vue @@ -36,7 +36,7 @@ import MkInput from '@/components/MkInput.vue'; import * as os from '@/os'; import { unique } from '@/scripts/array'; import { i18n } from '@/i18n'; -import { emojiCategories } from '@/instance'; +import { getCustomEmojiCategories } from '@/custom-emojis'; const props = defineProps<{ emoji: any, @@ -46,7 +46,7 @@ let dialog = $ref(null); let name: string = $ref(props.emoji.name); let category: string = $ref(props.emoji.category); let aliases: string = $ref(props.emoji.aliases.join(' ')); -let categories: string[] = $ref(emojiCategories); +const categories = await getCustomEmojiCategories(); const emit = defineEmits<{ (ev: 'done', v: { deleted?: boolean, updated?: any }): void, diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue index 43a735cbf9..08f31676f3 100644 --- a/packages/frontend/src/pages/admin/overview.stats.vue +++ b/packages/frontend/src/pages/admin/overview.stats.vue @@ -36,7 +36,7 @@ <div class="icon"><i class="ti ti-icons"></i></div> <div class="body"> <div class="value"> - <MkNumber :value="$instance.emojis.length" style="margin-right: 0.5em;"/> + <MkNumber :value="customEmojis.length" style="margin-right: 0.5em;"/> </div> <div class="label">Custom emojis</div> </div> @@ -63,6 +63,9 @@ import number from '@/filters/number'; import MkNumberDiff from '@/components/MkNumberDiff.vue'; import MkNumber from '@/components/MkNumber.vue'; import { i18n } from '@/i18n'; +import { getCustomEmojis } from '@/custom-emojis'; + +const customEmojis = await getCustomEmojis(); let stats: any = $ref(null); let usersComparedToThePrevDay = $ref<number>(); diff --git a/packages/frontend/src/pages/mfm-cheat-sheet.vue b/packages/frontend/src/pages/mfm-cheat-sheet.vue index 697a692743..f49b6959c8 100644 --- a/packages/frontend/src/pages/mfm-cheat-sheet.vue +++ b/packages/frontend/src/pages/mfm-cheat-sheet.vue @@ -317,12 +317,15 @@ import MkTextarea from '@/components/MkTextarea.vue'; import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; import { instance } from '@/instance'; +import { getCustomEmojis } from '@/custom-emojis'; + +const customEmojis = await getCustomEmojis(); let preview_mention = $ref('@example'); let preview_hashtag = $ref('#test'); let preview_url = $ref('https://example.com'); let preview_link = $ref(`[${i18n.ts._mfm.dummy}](https://example.com)`); -let preview_emoji = $ref(instance.emojis.length ? `:${instance.emojis[0].name}:` : ':emojiname:'); +let preview_emoji = $ref(customEmojis.length ? `:${customEmojis[0].name}:` : ':emojiname:'); let preview_bold = $ref(`**${i18n.ts._mfm.dummy}**`); let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`); let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`); diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 3468d44e00..e1e050ee70 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -183,6 +183,8 @@ const menuDef = computed(() => [{ action: () => { miLocalStorage.removeItem('locale'); miLocalStorage.removeItem('theme'); + miLocalStorage.removeItem('emojis'); + miLocalStorage.removeItem('lastEmojisFetchedAt'); unisonReload(); }, }, { From c438bd2e277a9e0400d1a0773389f788e6634dbf Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 15:51:36 +0900 Subject: [PATCH 53/68] Update vite.config.ts --- packages/frontend/vite.config.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 80f50b6cfb..40daf8b7cb 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -41,11 +41,6 @@ export default defineConfig(({ command, mode }) => { }, build: { - target: [ - 'chrome100', - 'firefox100', - 'safari15', - ], manifest: 'manifest.json', rollupOptions: { input: { From 6a18360269ce5d4a53db02c898efb8f4b1d1ff2a Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 16:07:14 +0900 Subject: [PATCH 54/68] update mfm-js --- packages/backend/package.json | 2 +- packages/frontend/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 31dac2156d..293b630ad6 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -72,7 +72,7 @@ "json5-loader": "4.0.1", "jsonld": "8.1.0", "jsrsasign": "10.6.1", - "mfm-js": "0.23.0", + "mfm-js": "0.23.1", "mime-types": "2.1.35", "misskey-js": "0.0.14", "ms": "3.0.0-canary.1", diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 234490e506..72a9532c61 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -38,7 +38,7 @@ "json5": "2.2.3", "katex": "0.16.4", "matter-js": "0.18.0", - "mfm-js": "0.23.0", + "mfm-js": "0.23.1", "misskey-js": "0.0.14", "photoswipe": "5.3.4", "prismjs": "1.29.0", diff --git a/yarn.lock b/yarn.lock index 35c9103280..c46e744c3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4232,7 +4232,7 @@ __metadata: json5-loader: 4.0.1 jsonld: 8.1.0 jsrsasign: 10.6.1 - mfm-js: 0.23.0 + mfm-js: 0.23.1 mime-types: 2.1.35 misskey-js: 0.0.14 ms: 3.0.0-canary.1 @@ -8092,7 +8092,7 @@ __metadata: json5: 2.2.3 katex: 0.16.4 matter-js: 0.18.0 - mfm-js: 0.23.0 + mfm-js: 0.23.1 misskey-js: 0.0.14 photoswipe: 5.3.4 prismjs: 1.29.0 @@ -11592,12 +11592,12 @@ __metadata: languageName: node linkType: hard -"mfm-js@npm:0.23.0": - version: 0.23.0 - resolution: "mfm-js@npm:0.23.0" +"mfm-js@npm:0.23.1": + version: 0.23.1 + resolution: "mfm-js@npm:0.23.1" dependencies: twemoji-parser: 14.0.0 - checksum: 0351c22be1074c83fa02fae5f34e481c7f509fe012022f767ea505fb9b633e50e9717cb47683b62dc2130993fd470110f48ebddd9b33f39416fd4151220dfe3e + checksum: 1c489ee30db7b4a2abd979f5e6453edb0c2e5ebb45e303cf4275215bb606620cf7df4b22442dfc560f6d2724986ed03153a301684a19a5decd72e8f8b5b3b81b languageName: node linkType: hard From c65957853b372c0812982aa83cc0653bd954c5ac Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 16:08:25 +0900 Subject: [PATCH 55/68] 13.0.0-beta.33 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e72612e0d..eb1ab711ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0-beta.32", + "version": "13.0.0-beta.33", "codename": "indigo", "repository": { "type": "git", From ed43369797c023e7094163326edded0577c78a72 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 16:09:40 +0900 Subject: [PATCH 56/68] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43cf0ef6e9..5247f5b05c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,7 @@ You should also include the user name that made the change. - Server: 特定のPNG画像のアップロードに失敗する問題を修正 @usbharu - Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo - Server: アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう @syuilo +- Client: 日付形式の文字列などがカスタム絵文字として表示されるのを修正 @syuilo - Client: case insensitive emoji search @saschanaz - Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina - Client: use proxied image for instance icon @syuilo From 6071e962f4cac2a2f0c930e1ddbdf70b8117dbef Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 16:16:00 +0900 Subject: [PATCH 57/68] Revert "Update vite.config.ts" This reverts commit c438bd2e277a9e0400d1a0773389f788e6634dbf. --- packages/frontend/vite.config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 40daf8b7cb..80f50b6cfb 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -41,6 +41,11 @@ export default defineConfig(({ command, mode }) => { }, build: { + target: [ + 'chrome100', + 'firefox100', + 'safari15', + ], manifest: 'manifest.json', rollupOptions: { input: { From 3ece2dc9909be2e25c60c3a33a32356715a8acec Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 16:16:25 +0900 Subject: [PATCH 58/68] 13.0.0-beta.34 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb1ab711ea..ca4d44c397 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0-beta.33", + "version": "13.0.0-beta.34", "codename": "indigo", "repository": { "type": "git", From 4d66077f85d7d7a3e274c4027429475e6bda7dd2 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 16:27:09 +0900 Subject: [PATCH 59/68] Update MkEmojiPicker.vue --- .../frontend/src/components/MkEmojiPicker.vue | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 8df01f6c25..18950b18d3 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -1,7 +1,7 @@ <template> <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> <input ref="search" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()"> - <div ref="emojis" class="emojis"> + <div v-if="customEmojis != null && customEmojiCategories != null" ref="emojisEl" class="emojis"> <section class="result"> <div v-if="searchResultCustom.length > 0" class="body"> <button @@ -104,9 +104,17 @@ const emit = defineEmits<{ (ev: 'chosen', v: string): void; }>(); -const customEmojis = await getCustomEmojis(); +let customEmojis = $ref(null); +getCustomEmojis().then((x) => { + customEmojis = x; +}); +let customEmojiCategories = $ref(null); +getCustomEmojiCategories().then((x) => { + customEmojiCategories = x; +}); + const search = shallowRef<HTMLInputElement>(); -const emojis = shallowRef<HTMLDivElement>(); +const emojisEl = shallowRef<HTMLDivElement>(); const { reactions: pinned, @@ -120,14 +128,13 @@ const { const size = computed(() => props.asReactionPicker ? reactionPickerSize.value : 1); const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3); const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2); -const customEmojiCategories = await getCustomEmojiCategories(); const q = ref<string>(''); const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]); const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index'); watch(q, () => { - if (emojis.value) emojis.value.scrollTop = 0; + if (emojisEl.value) emojisEl.value.scrollTop = 0; if (q.value === '') { searchResultCustom.value = []; @@ -276,7 +283,7 @@ function focus() { } function reset() { - if (emojis.value) emojis.value.scrollTop = 0; + if (emojisEl.value) emojisEl.value.scrollTop = 0; q.value = ''; } From 8cc80faf2067470489006f8cce8493e253424033 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 16:27:27 +0900 Subject: [PATCH 60/68] 13.0.0-beta.35 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca4d44c397..ddba184f3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0-beta.34", + "version": "13.0.0-beta.35", "codename": "indigo", "repository": { "type": "git", From 39c3995c74380a5b9b473583594966c26c008777 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 16:45:05 +0900 Subject: [PATCH 61/68] refactor --- .../src/components/MkAutocomplete.vue | 4 +--- .../frontend/src/components/MkEmojiPicker.vue | 14 +++---------- packages/frontend/src/custom-emojis.ts | 21 +++++++++---------- packages/frontend/src/pages/about.emojis.vue | 7 +++---- .../src/pages/admin/emoji-edit-dialog.vue | 2 +- .../src/pages/admin/overview.stats.vue | 4 +--- .../frontend/src/pages/mfm-cheat-sheet.vue | 4 +--- 7 files changed, 20 insertions(+), 36 deletions(-) diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 0d0c5edbae..b184e4489e 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -47,9 +47,7 @@ import { emojilist } from '@/scripts/emojilist'; import { instance } from '@/instance'; import { i18n } from '@/i18n'; import { miLocalStorage } from '@/local-storage'; -import { getCustomEmojis } from '@/custom-emojis'; - -const customEmojis = await getCustomEmojis(); +import { customEmojis } from '@/custom-emojis'; type EmojiDef = { emoji: string; diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 18950b18d3..a34326822f 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -1,7 +1,7 @@ <template> <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> <input ref="search" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()"> - <div v-if="customEmojis != null && customEmojiCategories != null" ref="emojisEl" class="emojis"> + <div ref="emojisEl" class="emojis"> <section class="result"> <div v-if="searchResultCustom.length > 0" class="body"> <button @@ -88,7 +88,7 @@ import { deviceKind } from '@/scripts/device-kind'; import { instance } from '@/instance'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; -import { getCustomEmojiCategories, getCustomEmojis } from '@/custom-emojis'; +import { getCustomEmojiCategories, customEmojis } from '@/custom-emojis'; const props = withDefaults(defineProps<{ showPinned?: boolean; @@ -104,15 +104,7 @@ const emit = defineEmits<{ (ev: 'chosen', v: string): void; }>(); -let customEmojis = $ref(null); -getCustomEmojis().then((x) => { - customEmojis = x; -}); -let customEmojiCategories = $ref(null); -getCustomEmojiCategories().then((x) => { - customEmojiCategories = x; -}); - +const customEmojiCategories = getCustomEmojiCategories(); const search = shallowRef<HTMLInputElement>(); const emojisEl = shallowRef<HTMLDivElement>(); diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index 2a52753f84..19469999b6 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -2,25 +2,26 @@ import { api } from './os'; import { miLocalStorage } from './local-storage'; const storageCache = miLocalStorage.getItem('emojis'); -let cached = storageCache ? JSON.parse(storageCache) : null; -export async function getCustomEmojis() { +export let customEmojis = storageCache ? JSON.parse(storageCache) : []; + +fetchCustomEmojis(); + +export async function fetchCustomEmojis() { const now = Date.now(); const lastFetchedAt = miLocalStorage.getItem('lastEmojisFetchedAt'); - if (cached && lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60) return cached; + if (lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60) return; const res = await api('emojis', {}); - cached = res.emojis; - miLocalStorage.setItem('emojis', JSON.stringify(cached)); + customEmojis = res.emojis; + miLocalStorage.setItem('emojis', JSON.stringify(customEmojis)); miLocalStorage.setItem('lastEmojisFetchedAt', now.toString()); } let cachedCategories; -export async function getCustomEmojiCategories() { +export function getCustomEmojiCategories() { if (cachedCategories) return cachedCategories; - const customEmojis = await getCustomEmojis(); - const categories = new Set(); for (const emoji of customEmojis) { categories.add(emoji.category); @@ -31,11 +32,9 @@ export async function getCustomEmojiCategories() { } let cachedTags; -export async function getCustomEmojiTags() { +export function getCustomEmojiTags() { if (cachedTags) return cachedTags; - const customEmojis = await getCustomEmojis(); - const tags = new Set(); for (const emoji of customEmojis) { for (const tag of emoji.aliases) { diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue index acf6237c87..2c56b111c6 100644 --- a/packages/frontend/src/pages/about.emojis.vue +++ b/packages/frontend/src/pages/about.emojis.vue @@ -37,11 +37,10 @@ import MkSelect from '@/components/MkSelect.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkTab from '@/components/MkTab.vue'; import * as os from '@/os'; -import { getCustomEmojis, getCustomEmojiCategories, getCustomEmojiTags } from '@/custom-emojis'; +import { customEmojis, getCustomEmojiCategories, getCustomEmojiTags } from '@/custom-emojis'; -const customEmojis = await getCustomEmojis(); -const customEmojiCategories = await getCustomEmojiCategories(); -const customEmojiTags = await getCustomEmojiTags(); +const customEmojiCategories = getCustomEmojiCategories(); +const customEmojiTags = getCustomEmojiTags(); let q = $ref(''); let searchEmojis = $ref(null); let selectedTags = $ref(new Set()); diff --git a/packages/frontend/src/pages/admin/emoji-edit-dialog.vue b/packages/frontend/src/pages/admin/emoji-edit-dialog.vue index 0b6a5e1557..b2880b60b1 100644 --- a/packages/frontend/src/pages/admin/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/admin/emoji-edit-dialog.vue @@ -46,7 +46,7 @@ let dialog = $ref(null); let name: string = $ref(props.emoji.name); let category: string = $ref(props.emoji.category); let aliases: string = $ref(props.emoji.aliases.join(' ')); -const categories = await getCustomEmojiCategories(); +const categories = getCustomEmojiCategories(); const emit = defineEmits<{ (ev: 'done', v: { deleted?: boolean, updated?: any }): void, diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue index 08f31676f3..ce68222fbb 100644 --- a/packages/frontend/src/pages/admin/overview.stats.vue +++ b/packages/frontend/src/pages/admin/overview.stats.vue @@ -63,9 +63,7 @@ import number from '@/filters/number'; import MkNumberDiff from '@/components/MkNumberDiff.vue'; import MkNumber from '@/components/MkNumber.vue'; import { i18n } from '@/i18n'; -import { getCustomEmojis } from '@/custom-emojis'; - -const customEmojis = await getCustomEmojis(); +import { customEmojis } from '@/custom-emojis'; let stats: any = $ref(null); let usersComparedToThePrevDay = $ref<number>(); diff --git a/packages/frontend/src/pages/mfm-cheat-sheet.vue b/packages/frontend/src/pages/mfm-cheat-sheet.vue index f49b6959c8..4dde3f2666 100644 --- a/packages/frontend/src/pages/mfm-cheat-sheet.vue +++ b/packages/frontend/src/pages/mfm-cheat-sheet.vue @@ -317,9 +317,7 @@ import MkTextarea from '@/components/MkTextarea.vue'; import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; import { instance } from '@/instance'; -import { getCustomEmojis } from '@/custom-emojis'; - -const customEmojis = await getCustomEmojis(); +import { customEmojis } from '@/custom-emojis'; let preview_mention = $ref('@example'); let preview_hashtag = $ref('#test'); From 1bb2c2249356b083ef67212c6b6e4ec132ae9e8e Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 16:45:18 +0900 Subject: [PATCH 62/68] Update vite.config.ts --- packages/frontend/vite.config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index 80f50b6cfb..850a10838d 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -42,9 +42,9 @@ export default defineConfig(({ command, mode }) => { build: { target: [ - 'chrome100', - 'firefox100', - 'safari15', + 'chrome108', + 'firefox109', + 'safari16', ], manifest: 'manifest.json', rollupOptions: { From b7dec6e87d6865478d2aaf0991d8559a67798302 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 17:04:52 +0900 Subject: [PATCH 63/68] refactor --- packages/frontend/src/components/MkEmojiPicker.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index a34326822f..9c6d62ce8b 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -1,6 +1,6 @@ <template> <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"> - <input ref="search" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()"> + <input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()"> <div ref="emojisEl" class="emojis"> <section class="result"> <div v-if="searchResultCustom.length > 0" class="body"> @@ -105,7 +105,7 @@ const emit = defineEmits<{ }>(); const customEmojiCategories = getCustomEmojiCategories(); -const search = shallowRef<HTMLInputElement>(); +const searchEl = shallowRef<HTMLInputElement>(); const emojisEl = shallowRef<HTMLDivElement>(); const { @@ -268,7 +268,7 @@ watch(q, () => { function focus() { if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) { - search.value?.focus({ + searchEl.value?.focus({ preventScroll: true, }); } @@ -308,7 +308,7 @@ function input(): void { // Using custom input event instead of v-model to respond immediately on // Android, where composition happens on all languages // (v-model does not update during composition) - q.value = search.value?.value.trim() ?? ''; + q.value = searchEl.value?.value.trim() ?? ''; } function paste(event: ClipboardEvent): void { From 8f28ff63f15531927d57d9aa4e3fde570bb66488 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 17:08:58 +0900 Subject: [PATCH 64/68] =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=83=94?= =?UTF-8?q?=E3=83=83=E3=82=AB=E3=83=BC=E3=81=AB=E3=83=95=E3=82=A9=E3=83=BC?= =?UTF-8?q?=E3=82=AB=E3=82=B9=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E3=81=AE?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/components/MkEmojiPickerDialog.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue index da68ecd809..c568d4ed5c 100644 --- a/packages/frontend/src/components/MkEmojiPickerDialog.vue +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -59,6 +59,11 @@ function chosen(emoji: any) { function opening() { picker.value?.reset(); picker.value?.focus(); + + // 何故かちょっと待たないとフォーカスされない + setTimeout(() => { + picker.value?.focus(); + }, 10); } </script> From 96ccf550b1c7322eb80d97dd42c2c7c630fc97cd Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 17:13:25 +0900 Subject: [PATCH 65/68] New Crowdin updates (#9490) * New translations ja-JP.yml (French) * New translations ja-JP.yml (Spanish) * New translations ja-JP.yml (Arabic) * New translations ja-JP.yml (German) * New translations ja-JP.yml (Italian) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Polish) * New translations ja-JP.yml (Portuguese) * New translations ja-JP.yml (Russian) * New translations ja-JP.yml (Slovak) * New translations ja-JP.yml (Ukrainian) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Vietnamese) * New translations ja-JP.yml (Indonesian) * New translations ja-JP.yml (Bengali) * New translations ja-JP.yml (Thai) * New translations ja-JP.yml (Japanese, Kansai) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (Chinese Traditional) * New translations ja-JP.yml (Thai) * New translations ja-JP.yml (German) * New translations ja-JP.yml (English) * New translations ja-JP.yml (Korean) * New translations ja-JP.yml (Swedish) * New translations ja-JP.yml (Thai) * New translations ja-JP.yml (Swedish) * New translations ja-JP.yml (Chinese Simplified) * New translations ja-JP.yml (Thai) * New translations ja-JP.yml (Chinese Traditional) --- locales/ar-SA.yml | 2 - locales/bn-BD.yml | 2 - locales/de-DE.yml | 3 +- locales/en-US.yml | 3 +- locales/es-ES.yml | 2 - locales/fr-FR.yml | 2 - locales/id-ID.yml | 2 - locales/it-IT.yml | 2 - locales/ja-KS.yml | 2 - locales/ko-KR.yml | 3 +- locales/pl-PL.yml | 2 - locales/pt-PT.yml | 2 - locales/ru-RU.yml | 2 - locales/sk-SK.yml | 2 - locales/sv-SE.yml | 119 ++++++++++++++++++++++++++++++++++++++++++++-- locales/th-TH.yml | 9 ++-- locales/uk-UA.yml | 2 - locales/vi-VN.yml | 2 - locales/zh-CN.yml | 5 +- locales/zh-TW.yml | 8 ++-- 20 files changed, 130 insertions(+), 46 deletions(-) diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 161a393bc2..773e239812 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1294,7 +1294,6 @@ _notification: youGotReply: "ردّ عليك {name}" youGotQuote: "اقتبس منك {name}" youRenoted: "إعادت نشر من {name}" - youGotPoll: "شارك {name} في استطلاع الرأي" youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}" youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}" youWereFollowed: "يتابعك" @@ -1311,7 +1310,6 @@ _notification: renote: "أعد النشر" quote: "الاقتباسات" reaction: "التفاعلات" - pollVote: "مصوِت شارك في الاستطلاع" receiveFollowRequest: "طلبات المتابعة المتلقاة" followRequestAccepted: "طلبات المتابعة المقبولة" groupInvited: "دعوات الفريق" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 593cbb1b32..ac0a0e1a51 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -1386,7 +1386,6 @@ _notification: youGotReply: "{name} আপনাকে জবাব দিয়েছে" youGotQuote: "{name} আপনাকে উদ্ধৃত করেছে" youRenoted: "{name} এর Renote" - youGotPoll: "{name} আপনার পোলে ভোট দিয়েছে" youGotMessagingMessageFromUser: "{name} আপনাকে মেসেজ করেছে" youGotMessagingMessageFromGroup: "{name} গ্রুপে একটি নতুন মেসেজ আছে" youWereFollowed: "আপনাকে অনুসরণ করছে" @@ -1403,7 +1402,6 @@ _notification: renote: "রিনোট" quote: "উদ্ধৃতি" reaction: "প্রতিক্রিয়া" - pollVote: "পোলে ভোট আছে" pollEnded: "পোল শেষ" receiveFollowRequest: "প্রাপ্ত অনুসরণের অনুরোধসমূহ" followRequestAccepted: "গৃহীত অনুসরণের অনুরোধসমূহ" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 3ab2e6abc3..70d0acc264 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1328,6 +1328,7 @@ _widgets: userList: "Benutzerliste" _userList: chooseList: "Liste auswählen" + clicker: "Klickzähler" _cw: hide: "Inhalt verbergen" show: "Inhalt anzeigen" @@ -1503,7 +1504,6 @@ _notification: youGotReply: "{name} hat dir geantwortet" youGotQuote: "{name} hat dich zitiert" youRenoted: "Renote deiner Notiz von {name}" - youGotPoll: "{name} hat in deiner Umfrage abgestimmt" youGotMessagingMessageFromUser: "{name} hat dir eine Chatnachricht gesendet" youGotMessagingMessageFromGroup: "In die Gruppe {name} wurde eine Chatnachricht gesendet" youWereFollowed: "ist dir gefolgt" @@ -1521,7 +1521,6 @@ _notification: renote: "Renotes" quote: "Zitationen" reaction: "Reaktionen" - pollVote: "Antworten auf Umfragen" pollEnded: "Ende von Umfragen" receiveFollowRequest: "Erhaltene Follow-Anfragen" followRequestAccepted: "Akzeptierte Follow-Anfragen" diff --git a/locales/en-US.yml b/locales/en-US.yml index 5da7032eda..ef42a8b97d 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1328,6 +1328,7 @@ _widgets: userList: "User list" _userList: chooseList: "Select a list" + clicker: "Clicker" _cw: hide: "Hide" show: "Show content" @@ -1503,7 +1504,6 @@ _notification: youGotReply: "{name} replied to you" youGotQuote: "{name} quoted you" youRenoted: "Renote from {name}" - youGotPoll: "{name} voted on your poll" youGotMessagingMessageFromUser: "{name} sent you a chat message" youGotMessagingMessageFromGroup: "A chat message was sent to the {name} group" youWereFollowed: "followed you" @@ -1521,7 +1521,6 @@ _notification: renote: "Renotes" quote: "Quotes" reaction: "Reactions" - pollVote: "Votes on polls" pollEnded: "Polls ending" receiveFollowRequest: "Received follow requests" followRequestAccepted: "Accepted follow requests" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index c328737c4b..5e48fb6d31 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -1487,7 +1487,6 @@ _notification: youGotReply: "Respuesta de {name}" youGotQuote: "Citado por {name}" youRenoted: "Renotado por {name}" - youGotPoll: "Encuestado por {name}" youGotMessagingMessageFromUser: "{name} comenzó un chat contigo" youGotMessagingMessageFromGroup: "Tienes un chat de {name}" youWereFollowed: "te ha seguido" @@ -1505,7 +1504,6 @@ _notification: renote: "Renotar" quote: "Citar" reaction: "Reacción" - pollVote: "Votado en la encuesta" pollEnded: "La encuesta terminó" receiveFollowRequest: "Recibió una solicitud de seguimiento" followRequestAccepted: "El seguimiento fue aceptado" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 0d7399533d..8c777ab513 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1478,7 +1478,6 @@ _notification: youGotReply: "Réponse de {name}" youGotQuote: "Cité·e par {name}" youRenoted: "{name} vous a Renoté" - youGotPoll: "{name} a participé à votre sondage" youGotMessagingMessageFromUser: "{name} vous envoyé un message" youGotMessagingMessageFromGroup: "Un message a été envoyé au groupe {name}" youWereFollowed: "Vous suit" @@ -1496,7 +1495,6 @@ _notification: renote: "Renotes" quote: "Citations" reaction: "Réactions" - pollVote: "Votes dans des sondages" pollEnded: "Sondages se cloturant" receiveFollowRequest: "Demande d'abonnement reçue" followRequestAccepted: "Demande d'abonnement acceptée" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index 3a2bf69a72..eee647d0f6 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -1402,7 +1402,6 @@ _notification: youGotReply: "{name} membalas kamu" youGotQuote: "{name} mengutip kamu" youRenoted: "{name} me-renote kamu" - youGotPoll: "{name} memilih di angket kamu" youGotMessagingMessageFromUser: "{name} mengirimi kamu pesan" youGotMessagingMessageFromGroup: "Sebuah pesan telah dikirim ke grup {name}" youWereFollowed: "Mengikuti kamu" @@ -1419,7 +1418,6 @@ _notification: renote: "Renote" quote: "Kutip" reaction: "Reaksi" - pollVote: "Memilih di angket" pollEnded: "Jajak pendapat berakhir" receiveFollowRequest: "Permintaan mengikuti diterima" followRequestAccepted: "Permintaan mengikuti disetujui" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 10e8c98177..013a1d2de4 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1487,7 +1487,6 @@ _notification: youGotReply: "{name} ti ha risposto" youGotQuote: "{name} ha citato il tuo Nota e ha detto" youRenoted: "{name} ha rinotato" - youGotPoll: "{name} ha votato" youGotMessagingMessageFromUser: "{name} ti ha mandato un messaggio" youGotMessagingMessageFromGroup: "{name} ti ha mandato un messaggio nella chat" youWereFollowed: "Ha iniziato a seguirti" @@ -1505,7 +1504,6 @@ _notification: renote: "Rinota" quote: "Cita" reaction: "Reazioni" - pollVote: "Voti ricevuti" pollEnded: "Sondaggio chiuso." receiveFollowRequest: "Richiesta di follow ricevuta" followRequestAccepted: "Richiesta di follow accettata" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index f8c045db00..4eff3268d4 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1485,7 +1485,6 @@ _notification: youGotReply: "{name}からのリプライ" youGotQuote: "{name}による引用" youRenoted: "{name}がRenoteしたみたいやで" - youGotPoll: "{name}が投票したみたいやで" youGotMessagingMessageFromUser: "{name}からのチャットがあるで" youGotMessagingMessageFromGroup: "{name}のチャットがあるで" youWereFollowed: "フォローされたで" @@ -1503,7 +1502,6 @@ _notification: renote: "Renote" quote: "引用" reaction: "リアクション" - pollVote: "アンケートに投票されたで" pollEnded: "アンケートが終了したで" receiveFollowRequest: "フォロー許可してほしいみたいやで" followRequestAccepted: "フォローが受理されたで" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 522f1f4aa7..f2ba0e6c7b 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1328,6 +1328,7 @@ _widgets: userList: "사용자 목록" _userList: chooseList: "리스트 선택" + clicker: "클리커" _cw: hide: "숨기기" show: "더 보기" @@ -1503,7 +1504,6 @@ _notification: youGotReply: "{name}님이 답글함" youGotQuote: "{name}님이 인용함" youRenoted: "{name}님이 Renote" - youGotPoll: "{name}님이 투표함" youGotMessagingMessageFromUser: "{name} 님이 보낸 채팅이 있어요" youGotMessagingMessageFromGroup: "{name}에서 보낸 채팅이 있어요" youWereFollowed: "새로운 팔로워가 있습니다" @@ -1521,7 +1521,6 @@ _notification: renote: "리노트" quote: "인용" reaction: "리액션" - pollVote: "투표 참여" pollEnded: "투표가 종료됨" receiveFollowRequest: "팔로우 요청을 받았을 때" followRequestAccepted: "팔로우 요청이 승인되었을 때" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 712c05bb78..9043d2ab94 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1380,7 +1380,6 @@ _notification: youGotReply: "{name} odpowiedział(a) Tobie" youGotQuote: "{name} zacytował(a) Ciebie" youRenoted: "{name} udostępnił(a) Twój wpis" - youGotPoll: "{name} zagłosował(a) w Twojej ankiecie" youGotMessagingMessageFromUser: "{name} wysłał(a) Ci wiadomość" youGotMessagingMessageFromGroup: "Została wysłana wiadomość do grupy {name}" youWereFollowed: "Zaobserwował(a) Cię" @@ -1398,7 +1397,6 @@ _notification: renote: "Udostępnij" quote: "Cytuj" reaction: "Reakcja" - pollVote: "Głosy w ankietach" receiveFollowRequest: "Otrzymano prośbę o możliwość obserwacji" followRequestAccepted: "Przyjęto prośbę o możliwość obserwacji" groupInvited: "Zaproszono do grup" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index dd1c2954b7..ba4814c81b 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -524,7 +524,6 @@ _notification: youGotMention: "{name} te mencionou" youGotReply: "{name} te respondeu" youGotQuote: "{name} te citou" - youGotPoll: "{name} votou em sua enquete" youGotMessagingMessageFromUser: "{name} te mandou uma mensagem de bate-papo" youGotMessagingMessageFromGroup: "Uma mensagem foi mandada para o grupo {name}" youWereFollowed: "Você tem um novo seguidor" @@ -541,7 +540,6 @@ _notification: renote: "Repostar" quote: "Citar" reaction: "Reações" - pollVote: "Votações em enquetes" pollEnded: "Enquetes terminando" receiveFollowRequest: "Recebeu pedidos de seguimento" followRequestAccepted: "Aceitou pedidos de seguimento" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 553fa9e811..49cccc71ae 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1399,7 +1399,6 @@ _notification: youGotReply: "{name} отвечает вам." youGotQuote: "{name} цитирует вас." youRenoted: "{name} передаёт вашу заметку." - youGotPoll: "{name} участвует в вашем опросе." youGotMessagingMessageFromUser: "{name} пишет вам." youGotMessagingMessageFromGroup: "Новое сообщение в группе «{name}»." youWereFollowed: "У вас новый подписчик." @@ -1414,7 +1413,6 @@ _notification: renote: "Репосты" quote: "Цитаты" reaction: "Реакции" - pollVote: "Голосования" receiveFollowRequest: "Получен запрос на подписку" followRequestAccepted: "Запрос на подписку одобрен" groupInvited: "Приглашение в группы" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 35afc91918..2627c70b67 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1484,7 +1484,6 @@ _notification: youGotReply: "{name} vám odpovedal/a" youGotQuote: "{name} vás citoval/a" youRenoted: "{name} preposlal/a vašu poznámku" - youGotPoll: "{name} hlasoval/a" youGotMessagingMessageFromUser: "{name} vám poslal/a správu" youGotMessagingMessageFromGroup: "Prišla správa do skupiny {name}" youWereFollowed: "Máte nového sledujúceho" @@ -1502,7 +1501,6 @@ _notification: renote: "Preposlať" quote: "Citovať" reaction: "Reakcie" - pollVote: "Hlasy v hlasovaniach" pollEnded: "Hlasovanie skončilo" receiveFollowRequest: "Doručené žiadosti o sledovanie" followRequestAccepted: "Schválené žiadosti o sledovanie" diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index 8b87e36acd..4e2f942686 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -1,7 +1,7 @@ --- _lang_: "Svenska" headlineMisskey: "Ett nätverk kopplat av noter" -introMisskey: "Välkommen! Misskey är en öppen och decentraliserad mikrobloggningstjänst.\nSkapa en \"not\" och dela dina tankar med alla runtomkring dig. 📡\nMed \"reaktioner\" kan du snabbt uttrycka dina känslor kring andras noter.👍\nLåt oss utforska en nya värld!🚀" +introMisskey: "Välkommen! Misskey är en öppen och decentraliserad mikrobloggningstjänst.\nSkapa en \"not\" och dela dina tankar med alla runtomkring dig. 📡\nMed \"reaktioner\" kan du snabbt uttrycka dina känslor kring andras noter. 👍\nLåt oss utforska en ny värld! 🚀" poweredByMisskeyDescription: "{name} är en tjänst driven av den öppna källkodsplatformen <b>Misskey</b> (benämns \"Misskey instans\")." monthAndDay: "{day}/{month}" search: "Sök" @@ -17,7 +17,7 @@ noThankYou: "Nej tack" enterUsername: "Ange användarnamn" renotedBy: "Omnoterad av {user}" noNotes: "Inga noteringar" -noNotifications: "Inga aviseringar" +noNotifications: "Inga notifikationer" instance: "Instanser" settings: "Inställningar" basicSettings: "Basinställningar" @@ -30,13 +30,13 @@ login: "Logga in" loggingIn: "Loggar in" logout: "Logga ut" signup: "Registrera" -uploading: "Uppladdning sker..." +uploading: "Laddar upp..." save: "Spara" users: "Användare" addUser: "Lägg till användare" favorite: "Lägg till i favoriter" favorites: "Favoriter" -unfavorite: "Avfavorisera" +unfavorite: "Ta bort från favoriter" favorited: "Tillagd i favoriter." alreadyFavorited: "Redan tillagd i favoriter." cantFavorite: "Gick inte att lägga till i favoriter." @@ -146,7 +146,7 @@ flagAsBotDescription: "Aktivera det här alternativet om kontot är kontrollerat flagAsCat: "Markera konto som katt" flagAsCatDescription: "Aktivera denna inställning för att markera kontot som en katt." flagShowTimelineReplies: "Visa svar i tidslinje" -flagShowTimelineRepliesDescription: "Visar användarsvar till andra användares noter i tidslinjen om påslagen." +flagShowTimelineRepliesDescription: "Visar användarsvar till andra användares noter i tidslinjen om aktiverad." autoAcceptFollowed: "Godkänn följarförfrågningar från användare du följer automatiskt" addAccount: "Lägg till konto" loginFailed: "Inloggningen misslyckades" @@ -253,16 +253,111 @@ explore: "Utforska" messageRead: "Läs" noMoreHistory: "Det finns ingen mer historik" startMessaging: "Starta en chatt" +nUsersRead: "läst av {n}" +agreeTo: "Jag accepterar {0}" +tos: "Användarvillkor" +home: "Hem" +remoteUserCaution: "Då denna användaren kommer från en fjärrinstans, kan informationen visad vara ofullständig." +activity: "Aktivitet" +images: "Bilder" +birthday: "Födelsedag" +yearsOld: "{age} år gammal" +registeredDate: "Gick med" +location: "Plats" +theme: "Teman" +themeForLightMode: "Tema att använda i Ljust Läge" +themeForDarkMode: "Tema att använda i Mörkt Läge" +light: "Ljust" +dark: "Mörk" +lightThemes: "Ljusa teman" +darkThemes: "Mörka teman" +syncDeviceDarkMode: "Synka Mörkt Läge med din enhets inställningar" +drive: "Drive" +fileName: "Filnamn" +selectFile: "Välj en fil" +selectFiles: "Välj filer" +selectFolder: "Välj en mapp" +selectFolders: "Välj mappar" +renameFile: "Byt namn på filen" +folderName: "Mappnamn" +createFolder: "Skapa en mapp" +renameFolder: "Byt namn på mappen" +deleteFolder: "Ta bort mappen" +addFile: "Lägg till fil" +emptyDrive: "Din Drive är tom" +emptyFolder: "Denna mappen är tom" +unableToDelete: "Kunde inte ta bort" +inputNewFileName: "Ange nytt filnamn" +inputNewDescription: "Ange ny bildtext" +inputNewFolderName: "Ange nytt mappnamn" +circularReferenceFolder: "Destinationsmappen är en undermapp av mappen du vill flytta." +hasChildFilesOrFolders: "Då denna mappen inte är tom, kan den inte tas bort." +copyUrl: "Kopiera URL" +rename: "Byt namn" +avatar: "Profilbild" +banner: "Banner" nsfw: "Känsligt innehåll" +reload: "Ladda om" +doNothing: "Ignorera" +reloadConfirm: "Vill du ladda om tidslinjen?" +accept: "Tillåt" +reject: "Neka" +normal: "Normal" +instanceName: "Instansnamn" +instanceDescription: "Instansbeskrivning" +maintainerEmail: "Administratörens epost" +tosUrl: "URL till användarvillkår" +thisYear: "Detta året" +thisMonth: "Denna månaden" +today: "Idag" +dayX: "{day}" +monthX: "{month}" +yearX: "{year}" +pages: "Sidor" +integration: "Integrationer" +connectService: "Anslut" +disconnectService: "Koppla från" +enableLocalTimeline: "Aktivera lokal tidslinje" +enableGlobalTimeline: "Aktivera global tidslinje" +enableRegistration: "Aktivera registrering av nya användare" +inMb: "I megabyte" +iconUrl: "URL till profilbilden" +bannerUrl: "URL till banner-bilden" pinnedNotes: "Fästad not" +enableHcaptcha: "Aktivera hCaptcha" +enableRecaptcha: "Aktivera reCAPTCHA" +enableTurnstile: "Aktivera Turnstile" +enableServiceworker: "Aktivera pushnotiser i denna webbläsaren" +recentlyUpdatedUsers: "Nyligen aktiva användare" +recentlyRegisteredUsers: "Nyligen registrerade användare" userList: "Listor" +aboutMisskey: "Om Misskey" +administrator: "Administratör" +newPasswordIs: "Det nya lösenordet är \"{password}\"" +share: "Dela" +enable: "Aktivera" +serviceworkerInfo: "Måste vara aktiverad för pushnotiser." +enableInfiniteScroll: "Ladda mer automatiskt" +enablePlayer: "Öppna videospelare" +enableAll: "Aktivera alla" +enableEmail: "Aktivera epost-utskick" smtpHost: "Värd" smtpUser: "Användarnamn" smtpPass: "Lösenord" clearCache: "Rensa cache" +enabled: "Aktiverad" user: "Användare" +global: "Global" +squareAvatars: "Visa fyrkantiga profilbilder" searchByGoogle: "Sök" file: "Filer" +enableAutoSensitive: "Automatisk NSFW markering" +enableAutoSensitiveDescription: "Tillåter automatiskt detektering och marketing av NSFW media genom Maskininlärning när möjligt. Även om denna inställningen är avaktiverad, kan det vara aktiverat på hela instansen." +pushNotification: "Pushnotiser" +subscribePushNotification: "Aktivera pushnotiser" +unsubscribePushNotification: "Avaktivera pushnotiser" +pushNotificationAlreadySubscribed: "Pushnotiser är redan aktiverade" +pushNotificationNotSupported: "Din webbläsare eller instans har inte stöd för pushnotiser" _email: _follow: title: "följde dig" @@ -271,6 +366,9 @@ _mfm: quote: "Citat" emoji: "Anpassa emoji" search: "Sök" +_channel: + setBanner: "Välj banner" + removeBanner: "Ta bort banner" _theme: keys: mention: "Nämn" @@ -282,6 +380,7 @@ _sfx: _widgets: notifications: "Notifikationer" timeline: "Tidslinje" + activity: "Aktivitet" federation: "Federation" jobQueue: "Jobbkö" _userList: @@ -289,9 +388,12 @@ _widgets: _cw: show: "Ladda mer" _visibility: + home: "Hem" followers: "Följare" _profile: username: "Användarnamn" + changeAvatar: "Ändra profilbild" + changeBanner: "Ändra banner" _exportOrImport: followingList: "Följer" muteList: "Tysta" @@ -299,8 +401,15 @@ _exportOrImport: userLists: "Listor" _charts: federation: "Federation" +_timelines: + home: "Hem" + global: "Global" +_pages: + blocks: + image: "Bilder" _notification: youWereFollowed: "följde dig" + unreadAntennaNote: "Antenn {name}" _types: follow: "Följer" mention: "Nämn" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 5a3400feb1..d86c3de0e9 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -8,7 +8,7 @@ search: "ค้นหา" notifications: "การเเจ้งเตือน" username: "ชื่อผู้ใช้" password: "รหัสผ่าน" -forgotPassword: "ลืมรหัสผ่าน?" +forgotPassword: "ลืมรหัสผ่านใช่ไหม" fetchingAsApObject: "กำลังดึงข้อมูล จาก เฟดิเวิร์ส..." ok: "โอเค" gotIt: "เข้าใจแล้ว !" @@ -920,6 +920,10 @@ like: "ชื่นชอบ" unlike: "ไม่ชอบ" numberOfLikes: "จำนวนไลค์" show: "แสดงผล" +neverShow: "ไม่ต้องแสดงข้อความนี้อีก" +remindMeLater: "ไว้ครั้งหน้าแล้วกัน" +didYouLikeMisskey: "คุณเคยชอบ Misskey ไหม?" +pleaseDonate: "{host} ใช้ซอฟต์แวร์ฟรี Misskey เราขอขอบคุณการบริจาคของคุณอย่างสูงเพื่อให้การพัฒนา Misskey สามารถดำเนินต่อไปได้นะ!" _sensitiveMediaDetection: description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย" sensitivity: "การตรวจจับความไว" @@ -1324,6 +1328,7 @@ _widgets: userList: "รายชื่อผู้ใช้" _userList: chooseList: "เลือกรายการ" + clicker: "คลิกเกอร์" _cw: hide: "ซ่อน" show: "โหลดเพิ่มเติม" @@ -1499,7 +1504,6 @@ _notification: youGotReply: "{name} ตอบกลับถึงคุณ" youGotQuote: "{name} อ้างถึงคุณ" youRenoted: "รีโน้ตจาก {name}" - youGotPoll: "{name} โหวตบนแบบสำรวจความคิดเห็นของคุณ" youGotMessagingMessageFromUser: "{name} ได้ส่งข้อความแชทถึงคุณ" youGotMessagingMessageFromGroup: "ข้อความแชทถูกส่งไปยัง {name} กลุ่ม" youWereFollowed: "ได้ติดตามคุณ" @@ -1517,7 +1521,6 @@ _notification: renote: "รีโน้ต" quote: "อ้างคำพูด" reaction: "รีแอคชั่น" - pollVote: "จำนวนโหวตที่ได้รับ" pollEnded: "โพลนี้สิ้นสุดลงแล้ว" receiveFollowRequest: "ได้รับคำขอติดตาม\n" followRequestAccepted: "ยอมรับคำขอติดตาม" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 352fb354ee..c6afcde112 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1415,7 +1415,6 @@ _notification: youGotReply: "{name} відповідає" youGotQuote: "{name} цитує вас" youRenoted: "{name} поширює" - youGotPoll: "{name} бере участь в опитуванні" youGotMessagingMessageFromUser: "Повідомлення від {name}" youGotMessagingMessageFromGroup: "Нове повідомлення в групі {name}" youWereFollowed: "Новий підписник" @@ -1430,7 +1429,6 @@ _notification: renote: "Поширення" quote: "Цитування" reaction: "Реакції" - pollVote: "Опитування" receiveFollowRequest: "Запити на підписку" followRequestAccepted: "Прийняті підписки" groupInvited: "Запрошення до груп" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 0070af56f0..6824faaee1 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1460,7 +1460,6 @@ _notification: youGotReply: "{name} trả lời bạn" youGotQuote: "{name} trích dẫn tút của bạn" youRenoted: "{name} đăng lại tút của bạn" - youGotPoll: "{name} bình chọn tút của bạn" youGotMessagingMessageFromUser: "{name} nhắn tin cho bạn" youGotMessagingMessageFromGroup: "Một tin nhắn trong nhóm {name}" youWereFollowed: "đã theo dõi bạn" @@ -1477,7 +1476,6 @@ _notification: renote: "Đăng lại" quote: "Trích dẫn" reaction: "Biểu cảm" - pollVote: "Lượt bình chọn" pollEnded: "Bình chọn kết thúc" receiveFollowRequest: "Yêu cầu theo dõi" followRequestAccepted: "Yêu cầu theo dõi được chấp nhận" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 7c3efec86c..8ebc3a1fb4 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -920,6 +920,9 @@ like: "点赞!" unlike: "取消赞" numberOfLikes: "点赞数" show: "显示" +neverShow: "不再显示" +remindMeLater: "稍后提醒我" +didYouLikeMisskey: "你在Misskey玩得还开心吗?" _sensitiveMediaDetection: description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。" sensitivity: "检测敏感度" @@ -1499,7 +1502,6 @@ _notification: youGotReply: "来自{name}的回复" youGotQuote: "来自{name}的引用" youRenoted: "来自{name}的转发" - youGotPoll: "来自{name}的投票" youGotMessagingMessageFromUser: "来自{name}的聊天" youGotMessagingMessageFromGroup: "来自{name}的群聊" youWereFollowed: "关注了你。" @@ -1517,7 +1519,6 @@ _notification: renote: "转发" quote: "引用" reaction: "回应" - pollVote: "问卷调查被投票" pollEnded: "问卷调查结束" receiveFollowRequest: "收到关注请求" followRequestAccepted: "关注请求已通过" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index de12ef1fbf..8330be0388 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -252,7 +252,7 @@ uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。" explore: "探索" messageRead: "已讀" noMoreHistory: "沒有更多歷史紀錄" -startMessaging: "開始傳送訊息" +startMessaging: "開始聊天" nUsersRead: "{n}人已讀" agreeTo: "我同意{0}" tos: "使用條款" @@ -923,6 +923,7 @@ show: "檢視" neverShow: "不再顯示" remindMeLater: "以後再說" didYouLikeMisskey: "您是否喜愛Misskey呢?" +pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發能夠持續!" _sensitiveMediaDetection: description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。" sensitivity: "檢測敏感度" @@ -1191,7 +1192,7 @@ _sfx: note: "貼文" noteMy: "我的貼文" notification: "通知" - chat: "傳送訊息" + chat: "聊天" chatBg: "聊天背景" antenna: "天線接收" channel: "頻道通知" @@ -1327,6 +1328,7 @@ _widgets: userList: "使用者列表" _userList: chooseList: "選擇清單" + clicker: "點擊器" _cw: hide: "隱藏" show: "瀏覽更多" @@ -1502,7 +1504,6 @@ _notification: youGotReply: "{name}回覆了您" youGotQuote: "{name}引用了您" youRenoted: "{name} 轉發了你的貼文" - youGotPoll: "{name}已投票" youGotMessagingMessageFromUser: "{name}發送給您的訊息" youGotMessagingMessageFromGroup: "{name}發送給您的訊息" youWereFollowed: "您有新的追隨者" @@ -1520,7 +1521,6 @@ _notification: renote: "轉發貼文" quote: "引用" reaction: "反應" - pollVote: "統計已投票數" pollEnded: "問卷調查結束" receiveFollowRequest: "已收到追隨請求" followRequestAccepted: "追隨請求已接受" From 014c97fa85767ed287c8fcdc5ac329218cfee131 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 17:13:38 +0900 Subject: [PATCH 66/68] 13.0.0-beta.36 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ddba184f3d..5d99966a18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.0.0-beta.35", + "version": "13.0.0-beta.36", "codename": "indigo", "repository": { "type": "git", From b04155e7ba5147beb2630294074f9e2ba3403f88 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 17:18:45 +0900 Subject: [PATCH 67/68] :art: --- packages/frontend/src/pages/admin/relays.vue | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue index eb2788fdeb..55d33e0158 100644 --- a/packages/frontend/src/pages/admin/relays.vue +++ b/packages/frontend/src/pages/admin/relays.vue @@ -2,15 +2,17 @@ <MkStickyContainer> <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :content-max="800"> - <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;"> - <div>{{ relay.inbox }}</div> - <div class="status"> - <i v-if="relay.status === 'accepted'" class="ti ti-check icon accepted"></i> - <i v-else-if="relay.status === 'rejected'" class="ti ti-ban icon rejected"></i> - <i v-else class="ti ti-clock icon requesting"></i> - <span>{{ $t(`_relayStatus.${relay.status}`) }}</span> + <div class="_gaps"> + <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;"> + <div>{{ relay.inbox }}</div> + <div class="status"> + <i v-if="relay.status === 'accepted'" class="ti ti-check icon accepted"></i> + <i v-else-if="relay.status === 'rejected'" class="ti ti-ban icon rejected"></i> + <i v-else class="ti ti-clock icon requesting"></i> + <span>{{ $t(`_relayStatus.${relay.status}`) }}</span> + </div> + <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> </div> - <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton> </div> </MkSpacer> </MkStickyContainer> From 3bc0cdbfb77ad07b878022b91f06f252fcf906cc Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 9 Jan 2023 17:22:21 +0900 Subject: [PATCH 68/68] typo --- packages/backend/src/server/api/endpoints/emojis.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts index 0a16268229..97dcfde596 100644 --- a/packages/backend/src/server/api/endpoints/emojis.ts +++ b/packages/backend/src/server/api/endpoints/emojis.ts @@ -22,10 +22,9 @@ export const meta = { type: 'object', optional: false, nullable: false, properties: { - id: { + name: { type: 'string', optional: false, nullable: false, - format: 'id', }, aliases: { type: 'array',