diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-07-13 21:59:47 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-07-13 21:59:47 +0900 |
| commit | bce48dfee933d9feb3be381ad22ea765dd6a3aab (patch) | |
| tree | b21a3a765040469885d286487245aeaed373bba6 /packages/client/src | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.113.0 (diff) | |
| download | misskey-bce48dfee933d9feb3be381ad22ea765dd6a3aab.tar.gz misskey-bce48dfee933d9feb3be381ad22ea765dd6a3aab.tar.bz2 misskey-bce48dfee933d9feb3be381ad22ea765dd6a3aab.zip | |
Merge branch 'develop'
Diffstat (limited to 'packages/client/src')
50 files changed, 567 insertions, 605 deletions
diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index fc7c4ff950..31e95404fa 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -316,7 +316,7 @@ const render = () => { plugins: [{ id: 'vLine', beforeDraw(chart, args, options) { - if (chart.tooltip._active && chart.tooltip._active.length) { + if (chart.tooltip?._active?.length) { const activePoint = chart.tooltip._active[0]; const ctx = chart.ctx; const x = activePoint.element.x; diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue index 16c77f7267..b346585cec 100644 --- a/packages/client/src/components/drive-file-thumbnail.vue +++ b/packages/client/src/components/drive-file-thumbnail.vue @@ -59,7 +59,7 @@ const isThumbnailAvailable = computed(() => { display: flex; background: var(--panel); border-radius: 8px; - overflow: hidden; overflow: clip; + overflow: clip; > .icon-sub { position: absolute; diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue index 5bbfca83c9..51ea2b5015 100644 --- a/packages/client/src/components/drive-window.vue +++ b/packages/client/src/components/drive-window.vue @@ -13,7 +13,7 @@ </template> <script lang="ts" setup> -import { } from 'vue'; +import { } from 'vue'; import * as Misskey from 'misskey-js'; import XDrive from './drive.vue'; import XWindow from '@/components/ui/window.vue'; diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue index ec1ad20de3..1cc3912178 100644 --- a/packages/client/src/components/form/input.vue +++ b/packages/client/src/components/form/input.vue @@ -41,7 +41,7 @@ import { useInterval } from '@/scripts/use-interval'; const props = defineProps<{ modelValue: string | number; - type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time'; + type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search'; required?: boolean; readonly?: boolean; disabled?: boolean; diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue index ebec482d84..8301d27830 100644 --- a/packages/client/src/components/form/range.vue +++ b/packages/client/src/components/form/range.vue @@ -189,7 +189,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => { height: 3px; background: rgba(0, 0, 0, 0.1); border-radius: 999px; - overflow: hidden; overflow: clip; + overflow: clip; > .highlight { position: absolute; diff --git a/packages/client/src/components/global/url.vue b/packages/client/src/components/global/url.vue index 34ba9024cc..dc79a20649 100644 --- a/packages/client/src/components/global/url.vue +++ b/packages/client/src/components/global/url.vue @@ -1,5 +1,6 @@ <template> -<component :is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target" +<component + :is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target" @contextmenu.stop="() => {}" > <template v-if="!self"> @@ -23,14 +24,7 @@ import { toUnicode as decodePunycode } from 'punycode/'; import { url as local } from '@/config'; import * as os from '@/os'; import { useTooltip } from '@/scripts/use-tooltip'; - -function safeURIDecode(str: string) { - try { - return decodeURIComponent(str); - } catch { - return str; - } -} +import { safeURIDecode } from '@/scripts/safe-uri-decode'; export default defineComponent({ props: { @@ -42,7 +36,7 @@ export default defineComponent({ type: String, required: false, default: null, - } + }, }, setup(props) { const self = props.url.startsWith(local); diff --git a/packages/client/src/components/marquee.vue b/packages/client/src/components/marquee.vue index ad9de9b845..5ca04b0b48 100644 --- a/packages/client/src/components/marquee.vue +++ b/packages/client/src/components/marquee.vue @@ -76,7 +76,7 @@ export default { <style lang="scss" module> .wrap { - overflow: hidden; overflow: clip; + overflow: clip; animation-play-state: running; &:hover { diff --git a/packages/client/src/components/mention.vue b/packages/client/src/components/mention.vue index 2c8bc0c04e..cf69437771 100644 --- a/packages/client/src/components/mention.vue +++ b/packages/client/src/components/mention.vue @@ -1,15 +1,15 @@ <template> -<MkA v-if="url.startsWith('/')" v-user-preview="canonical" :class="[$style.root, { isMe }]" :to="url" :style="{ background: bgCss }"> - <img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt=""> +<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="akbvjaqn" :class="{ isMe }" :to="url" :style="{ background: bgCss }"> + <img class="icon" :src="`/avatar/@${username}@${host}`" alt=""> <span class="main"> <span class="username">@{{ username }}</span> - <span v-if="(host != localHost) || $store.state.showFullAcct" :class="$style.mainHost">@{{ toUnicode(host) }}</span> + <span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span> </span> </MkA> -<a v-else :class="$style.root" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }"> +<a v-else class="akbvjaqn" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }"> <span class="main"> <span class="username">@{{ username }}</span> - <span :class="$style.mainHost">@{{ toUnicode(host) }}</span> + <span class="host">@{{ toUnicode(host) }}</span> </span> </a> </template> @@ -41,8 +41,8 @@ const bgCss = bg.toRgbString(); useCssModule(); </script> -<style lang="scss" module> -.root { +<style lang="scss" scoped> +.akbvjaqn { display: inline-block; padding: 4px 8px 4px 4px; border-radius: 999px; @@ -51,18 +51,18 @@ useCssModule(); &.isMe { color: var(--mentionMe); } -} -.icon { - width: 1.5em; - height: 1.5em; - object-fit: cover; - margin: 0 0.2em 0 0; - vertical-align: bottom; - border-radius: 100%; -} + > .icon { + width: 1.5em; + height: 1.5em; + object-fit: cover; + margin: 0 0.2em 0 0; + vertical-align: bottom; + border-radius: 100%; + } -.mainHost { - opacity: 0.5; + > .main > .host { + opacity: 0.5; + } } </style> diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts index f7dbca0d30..16ae45bfe1 100644 --- a/packages/client/src/components/mfm.ts +++ b/packages/client/src/components/mfm.ts @@ -47,7 +47,7 @@ export default defineComponent({ render() { if (this.text == null || this.text === '') return; - const ast = (this.plain ? mfm.parsePlain : mfm.parse)(this.text, { fnNameList: MFM_TAGS }); + const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text, { fnNameList: MFM_TAGS }); const validTime = (t: string | null | undefined) => { if (t == null) return null; @@ -312,6 +312,10 @@ export default defineComponent({ })]; } + case 'plain': { + return [h('span', genEl(token.children))]; + } + default: { console.error('unrecognized ast type:', token.type); diff --git a/packages/client/src/components/note-preview.vue b/packages/client/src/components/note-preview.vue index be7214db19..a78b499654 100644 --- a/packages/client/src/components/note-preview.vue +++ b/packages/client/src/components/note-preview.vue @@ -27,7 +27,7 @@ const props = defineProps<{ display: flex; margin: 0; padding: 0; - overflow: hidden; overflow: clip; + overflow: clip; font-size: 0.95em; &.min-width_350px { diff --git a/packages/client/src/components/note-simple.vue b/packages/client/src/components/note-simple.vue index 93c34b6bf4..b813b9a2b9 100644 --- a/packages/client/src/components/note-simple.vue +++ b/packages/client/src/components/note-simple.vue @@ -36,7 +36,7 @@ const showContent = $ref(false); display: flex; margin: 0; padding: 0; - overflow: hidden; overflow: clip; + overflow: clip; font-size: 0.95em; &.min-width_350px { diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index b494c70392..c96cdddef2 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -293,7 +293,7 @@ function readPromo() { position: relative; transition: box-shadow 0.1s ease; font-size: 1.05em; - overflow: hidden; overflow: clip; + overflow: clip; contain: content; // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、 diff --git a/packages/client/src/components/tag-cloud.vue b/packages/client/src/components/tag-cloud.vue index 9f3bc1c603..bbebff497f 100644 --- a/packages/client/src/components/tag-cloud.vue +++ b/packages/client/src/components/tag-cloud.vue @@ -71,7 +71,7 @@ defineExpose({ <style lang="scss" scoped> .meijqfqm { position: relative; - overflow: hidden; overflow: clip; + overflow: clip; display: grid; place-items: center; diff --git a/packages/client/src/components/toast.vue b/packages/client/src/components/toast.vue index e0230dccd4..c9fad64eb6 100644 --- a/packages/client/src/components/toast.vue +++ b/packages/client/src/components/toast.vue @@ -54,7 +54,7 @@ onMounted(() => { width: min-content; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); border-radius: 8px; - overflow: hidden; overflow: clip; + overflow: clip; text-align: center; pointer-events: none; diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue index d8052b511c..e6b20d9881 100644 --- a/packages/client/src/components/ui/button.vue +++ b/packages/client/src/components/ui/button.vue @@ -148,7 +148,7 @@ export default defineComponent({ text-decoration: none; background: var(--buttonBg); border-radius: 5px; - overflow: hidden; overflow: clip; + overflow: clip; box-sizing: border-box; transition: background 0.1s ease; diff --git a/packages/client/src/components/ui/container.vue b/packages/client/src/components/ui/container.vue index 784414e791..4be59adc2a 100644 --- a/packages/client/src/components/ui/container.vue +++ b/packages/client/src/components/ui/container.vue @@ -143,7 +143,7 @@ export default defineComponent({ .ukygtjoj { position: relative; - overflow: hidden; overflow: clip; + overflow: clip; contain: content; &.naked { diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue index 385f6cdb2b..2305a02794 100644 --- a/packages/client/src/components/ui/modal.vue +++ b/packages/client/src/components/ui/modal.vue @@ -10,7 +10,7 @@ </template> <script lang="ts" setup> -import { nextTick, onMounted, computed, ref, watch, provide } from 'vue'; +import { nextTick, onMounted, watch, provide } from 'vue'; import * as os from '@/os'; import { isTouchUsing } from '@/scripts/touch'; import { defaultStore } from '@/store'; @@ -57,13 +57,13 @@ const emit = defineEmits<{ provide('modal', true); -const maxHeight = ref<number>(); -const fixed = ref(false); -const transformOrigin = ref('center'); -const showing = ref(true); -const content = ref<HTMLElement>(); +let maxHeight = $ref<number>(); +let fixed = $ref(false); +let transformOrigin = $ref('center'); +let showing = $ref(true); +let content = $ref<HTMLElement>(); const zIndex = os.claimZIndex(props.zPriority); -const type = computed(() => { +const type = $computed(() => { if (props.preferType === 'auto') { if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') { return 'drawer'; @@ -80,7 +80,7 @@ let contentClicking = false; const close = () => { // eslint-disable-next-line vue/no-mutating-props if (props.src) props.src.style.pointerEvents = 'auto'; - showing.value = false; + showing = false; emit('close'); }; @@ -89,8 +89,8 @@ const onBgClick = () => { emit('click'); }; -if (type.value === 'drawer') { - maxHeight.value = window.innerHeight / 1.5; +if (type === 'drawer') { + maxHeight = window.innerHeight / 1.5; } const keymap = { @@ -101,22 +101,21 @@ const MARGIN = 16; const align = () => { if (props.src == null) return; - if (type.value === 'drawer') return; - if (type.value === 'dialog') return; + if (type === 'drawer') return; + if (type === 'dialog') return; - const popover = content.value!; - if (popover == null) return; + if (content == null) return; const srcRect = props.src.getBoundingClientRect(); - - const width = popover.offsetWidth; - const height = popover.offsetHeight; + + const width = content!.offsetWidth; + const height = content!.offsetHeight; let left; let top; - const x = srcRect.left + (fixed.value ? 0 : window.pageXOffset); - const y = srcRect.top + (fixed.value ? 0 : window.pageYOffset); + const x = srcRect.left + (fixed ? 0 : window.pageXOffset); + const y = srcRect.top + (fixed ? 0 : window.pageYOffset); if (props.anchor.x === 'center') { left = x + (props.src.offsetWidth / 2) - (width / 2); @@ -134,7 +133,7 @@ const align = () => { top = y + props.src.offsetHeight; } - if (fixed.value) { + if (fixed) { // 画面から横にはみ出る場合 if (left + width > window.innerWidth) { left = window.innerWidth - width; @@ -147,16 +146,16 @@ const align = () => { if (top + height > (window.innerHeight - MARGIN)) { if (props.noOverlap && props.anchor.x === 'center') { if (underSpace >= (upperSpace / 3)) { - maxHeight.value = underSpace; + maxHeight = underSpace; } else { - maxHeight.value = upperSpace; + maxHeight = upperSpace; top = (upperSpace + MARGIN) - height; } } else { top = (window.innerHeight - MARGIN) - height; } } else { - maxHeight.value = underSpace; + maxHeight = underSpace; } } else { // 画面から横にはみ出る場合 @@ -171,16 +170,16 @@ const align = () => { if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) { if (props.noOverlap && props.anchor.x === 'center') { if (underSpace >= (upperSpace / 3)) { - maxHeight.value = underSpace; + maxHeight = underSpace; } else { - maxHeight.value = upperSpace; + maxHeight = upperSpace; top = window.pageYOffset + ((upperSpace + MARGIN) - height); } } else { top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1; } } else { - maxHeight.value = underSpace; + maxHeight = underSpace; } } @@ -195,29 +194,29 @@ const align = () => { let transformOriginX = 'center'; let transformOriginY = 'center'; - if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.pageYOffset)) { + if (top >= srcRect.top + props.src.offsetHeight + (fixed ? 0 : window.pageYOffset)) { transformOriginY = 'top'; - } else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.pageYOffset)) { + } else if ((top + height) <= srcRect.top + (fixed ? 0 : window.pageYOffset)) { transformOriginY = 'bottom'; } - if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.pageXOffset)) { + if (left >= srcRect.left + props.src.offsetWidth + (fixed ? 0 : window.pageXOffset)) { transformOriginX = 'left'; - } else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.pageXOffset)) { + } else if ((left + width) <= srcRect.left + (fixed ? 0 : window.pageXOffset)) { transformOriginX = 'right'; } - transformOrigin.value = `${transformOriginX} ${transformOriginY}`; + transformOrigin = `${transformOriginX} ${transformOriginY}`; - popover.style.left = left + 'px'; - popover.style.top = top + 'px'; + content.style.left = left + 'px'; + content.style.top = top + 'px'; }; const onOpened = () => { emit('opened'); // モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する - const el = content.value!.children[0]; + const el = content!.children[0]; el.addEventListener('mousedown', ev => { contentClicking = true; window.addEventListener('mouseup', ev => { @@ -235,7 +234,7 @@ onMounted(() => { // eslint-disable-next-line vue/no-mutating-props props.src.style.pointerEvents = 'none'; } - fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null); + fixed = (type === 'drawer') || (getFixedContainer(props.src) != null); await nextTick(); @@ -243,10 +242,9 @@ onMounted(() => { }, { immediate: true }); nextTick(() => { - const popover = content.value; new ResizeObserver((entries, observer) => { align(); - }).observe(popover!); + }).observe(content!); }); }); @@ -389,7 +387,7 @@ defineExpose({ left: 0; width: 100%; height: 100%; - overflow: hidden; overflow: clip; + overflow: clip; > .content { position: fixed; diff --git a/packages/client/src/components/ui/super-menu.vue b/packages/client/src/components/ui/super-menu.vue index 78f524036d..8ce2dc5dc2 100644 --- a/packages/client/src/components/ui/super-menu.vue +++ b/packages/client/src/components/ui/super-menu.vue @@ -30,7 +30,7 @@ export default defineComponent({ props: { def: { type: Array, - required: true + required: true, }, grid: { type: Boolean, @@ -64,7 +64,7 @@ export default defineComponent({ box-sizing: border-box; padding: 10px 16px 10px 8px; border-radius: 9px; - font-size: 0.95em; + font-size: 0.9em; &:hover { text-decoration: none; diff --git a/packages/client/src/components/visibility-picker.vue b/packages/client/src/components/visibility-picker.vue index c717c3a461..7fe55858cc 100644 --- a/packages/client/src/components/visibility-picker.vue +++ b/packages/client/src/components/visibility-picker.vue @@ -105,7 +105,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void { } &.active { - color: #fff; + color: var(--fgOnAccent); background: var(--accent); } diff --git a/packages/client/src/components/widgets.vue b/packages/client/src/components/widgets.vue index 85b8ae0ed3..0a9769e197 100644 --- a/packages/client/src/components/widgets.vue +++ b/packages/client/src/components/widgets.vue @@ -4,7 +4,7 @@ <header> <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" class="mk-widget-select"> <template #label>{{ $ts.selectWidget }}</template> - <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ $t(`_widgets.${widget}`) }}</option> + <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option> </MkSelect> <MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> <MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton> @@ -26,79 +26,91 @@ </template> </XDraggable> </template> - <component :is="`mkw-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" class="widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)"/> + <component :is="`mkw-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" class="widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/> </div> </template> -<script lang="ts"> -import { defineComponent, defineAsyncComponent, reactive, ref, computed } from 'vue'; +<script lang="ts" setup> +import { defineAsyncComponent, reactive, ref, computed } from 'vue'; import { v4 as uuid } from 'uuid'; import MkSelect from '@/components/form/select.vue'; import MkButton from '@/components/ui/button.vue'; import { widgets as widgetDefs } from '@/widgets'; +import * as os from '@/os'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - XDraggable: defineAsyncComponent(() => import('vuedraggable')), - MkSelect, - MkButton, - }, +const XDraggable = defineAsyncComponent(() => import('vuedraggable')); - props: { - widgets: { - type: Array, - required: true, - }, - edit: { - type: Boolean, - required: true, - }, - }, +type Widget = { + name: string; + id: string; + data: Record<string, any>; +}; - emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'], +const props = defineProps<{ + widgets: Widget[]; + edit: boolean; +}>(); - setup(props, context) { - const widgetRefs = reactive({}); - const configWidget = (id: string) => { - widgetRefs[id].configure(); - }; - const widgetAdderSelected = ref(null); - const addWidget = () => { - if (widgetAdderSelected.value == null) return; +const emit = defineEmits<{ + (ev: 'updateWidgets', widgets: Widget[]): void; + (ev: 'addWidget', widget: Widget): void; + (ev: 'removeWidget', widget: Widget): void; + (ev: 'updateWidget', widget: Partial<Widget>): void; + (ev: 'exit'): void; +}>(); - context.emit('addWidget', { - name: widgetAdderSelected.value, - id: uuid(), - data: {}, - }); +const widgetRefs = {}; +const configWidget = (id: string) => { + widgetRefs[id].configure(); +}; +const widgetAdderSelected = ref(null); +const addWidget = () => { + if (widgetAdderSelected.value == null) return; - widgetAdderSelected.value = null; - }; - const removeWidget = (widget) => { - context.emit('removeWidget', widget); - }; - const updateWidget = (id, data) => { - context.emit('updateWidget', { id, data }); - }; - const widgets_ = computed({ - get: () => props.widgets, - set: (value) => { - context.emit('updateWidgets', value); - }, - }); + emit('addWidget', { + name: widgetAdderSelected.value, + id: uuid(), + data: {}, + }); - return { - widgetRefs, - configWidget, - widgetAdderSelected, - widgetDefs, - addWidget, - removeWidget, - updateWidget, - widgets_, - }; + widgetAdderSelected.value = null; +}; +const removeWidget = (widget) => { + emit('removeWidget', widget); +}; +const updateWidget = (id, data) => { + emit('updateWidget', { id, data }); +}; +const widgets_ = computed({ + get: () => props.widgets, + set: (value) => { + emit('updateWidgets', value); }, }); + +function onContextmenu(widget: Widget, ev: MouseEvent) { + const isLink = (el: HTMLElement) => { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + }; + if (isLink(ev.target)) return; + if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return; + if (window.getSelection()?.toString() !== '') return; + + os.contextMenu([{ + type: 'label', + text: i18n.t(`_widgets.${widget.name}`), + }, { + icon: 'fas fa-cog', + text: i18n.ts.settings, + action: () => { + configWidget(widget.id); + }, + }], ev); +} </script> <style lang="scss" scoped> diff --git a/packages/client/src/directives/size.ts b/packages/client/src/directives/size.ts index 51855e0de5..c472a528ac 100644 --- a/packages/client/src/directives/size.ts +++ b/packages/client/src/directives/size.ts @@ -27,8 +27,8 @@ function getClassOrder(width: number, queue: Value): ClassOrder { ...(queue.min ? queue.min.filter(v => width >= v).map(getMinClass) : []), ], remove: [ - ...(queue.max ? queue.max.filter(v => width > v).map(getMaxClass) : []), - ...(queue.min ? queue.min.filter(v => width < v).map(getMinClass) : []), + ...(queue.max ? queue.max.filter(v => width > v).map(getMaxClass) : []), + ...(queue.min ? queue.min.filter(v => width < v).map(getMinClass) : []), ] }; } diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index 98f69c701f..94e7f9f6b3 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -39,401 +39,403 @@ import { reactionPicker } from '@/scripts/reaction-picker'; import { getUrlWithoutLoginId } from '@/scripts/login-id'; import { getAccountFromId } from '@/scripts/get-account-from-id'; -console.info(`Misskey v${version}`); +(async () => { + console.info(`Misskey v${version}`); -if (_DEV_) { - console.warn('Development mode!!!'); + if (_DEV_) { + console.warn('Development mode!!!'); - console.info(`vue ${vueVersion}`); + console.info(`vue ${vueVersion}`); - (window as any).$i = $i; - (window as any).$store = defaultStore; + (window as any).$i = $i; + (window as any).$store = defaultStore; - window.addEventListener('error', event => { - console.error(event); - /* - alert({ - type: 'error', - title: 'DEV: Unhandled error', - text: event.message + window.addEventListener('error', event => { + console.error(event); + /* + alert({ + type: 'error', + title: 'DEV: Unhandled error', + text: event.message + }); + */ }); - */ - }); - window.addEventListener('unhandledrejection', event => { - console.error(event); - /* - alert({ - type: 'error', - title: 'DEV: Unhandled promise rejection', - text: event.reason + window.addEventListener('unhandledrejection', event => { + console.error(event); + /* + alert({ + type: 'error', + title: 'DEV: Unhandled promise rejection', + text: event.reason + }); + */ }); - */ - }); -} + } -// タッチデバイスでCSSの:hoverを機能させる -document.addEventListener('touchend', () => {}, { passive: true }); + // タッチデバイスでCSSの:hoverを機能させる + document.addEventListener('touchend', () => {}, { passive: true }); -// 一斉リロード -reloadChannel.addEventListener('message', path => { - if (path !== null) location.href = path; - else location.reload(); -}); + // 一斉リロード + reloadChannel.addEventListener('message', path => { + if (path !== null) location.href = path; + else location.reload(); + }); -//#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ -// TODO: いつの日にか消したい -const vh = window.innerHeight * 0.01; -document.documentElement.style.setProperty('--vh', `${vh}px`); -window.addEventListener('resize', () => { + //#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ + // TODO: いつの日にか消したい const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); -}); -//#endregion + window.addEventListener('resize', () => { + const vh = window.innerHeight * 0.01; + document.documentElement.style.setProperty('--vh', `${vh}px`); + }); + //#endregion -// If mobile, insert the viewport meta tag -if (['smartphone', 'tablet'].includes(deviceKind)) { - const viewport = document.getElementsByName('viewport').item(0); - viewport.setAttribute('content', - `${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`); -} + // If mobile, insert the viewport meta tag + if (['smartphone', 'tablet'].includes(deviceKind)) { + const viewport = document.getElementsByName('viewport').item(0); + viewport.setAttribute('content', + `${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`); + } -//#region Set lang attr -const html = document.documentElement; -html.setAttribute('lang', lang); -//#endregion + //#region Set lang attr + const html = document.documentElement; + html.setAttribute('lang', lang); + //#endregion -//#region loginId -const params = new URLSearchParams(location.search); -const loginId = params.get('loginId'); + //#region loginId + const params = new URLSearchParams(location.search); + const loginId = params.get('loginId'); -if (loginId) { - const target = getUrlWithoutLoginId(location.href); + if (loginId) { + const target = getUrlWithoutLoginId(location.href); - if (!$i || $i.id !== loginId) { - const account = await getAccountFromId(loginId); - if (account) { - await login(account.token, target); + if (!$i || $i.id !== loginId) { + const account = await getAccountFromId(loginId); + if (account) { + await login(account.token, target); + } } - } - - history.replaceState({ misskey: 'loginId' }, '', target); -} - -//#endregion -//#region Fetch user -if ($i && $i.token) { - if (_DEV_) { - console.log('account cache found. refreshing...'); - } - - refreshAccount(); -} else { - if (_DEV_) { - console.log('no account cache found.'); + history.replaceState({ misskey: 'loginId' }, '', target); } - // 連携ログインの場合用にCookieを参照する - const i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1]; + //#endregion - if (i != null && i !== 'null') { + //#region Fetch user + if ($i && $i.token) { if (_DEV_) { - console.log('signing...'); + console.log('account cache found. refreshing...'); } - try { - document.body.innerHTML = '<div>Please wait...</div>'; - await login(i); - } catch (err) { - // Render the error screen - // TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな) - document.body.innerHTML = '<div id="err">Oops!</div>'; - } + refreshAccount(); } else { if (_DEV_) { - console.log('not signed in'); + console.log('no account cache found.'); + } + + // 連携ログインの場合用にCookieを参照する + const i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1]; + + if (i != null && i !== 'null') { + if (_DEV_) { + console.log('signing...'); + } + + try { + document.body.innerHTML = '<div>Please wait...</div>'; + await login(i); + } catch (err) { + // Render the error screen + // TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな) + document.body.innerHTML = '<div id="err">Oops!</div>'; + } + } else { + if (_DEV_) { + console.log('not signed in'); + } } } -} -//#endregion + //#endregion -const fetchInstanceMetaPromise = fetchInstance(); + const fetchInstanceMetaPromise = fetchInstance(); -fetchInstanceMetaPromise.then(() => { - localStorage.setItem('v', instance.version); + fetchInstanceMetaPromise.then(() => { + localStorage.setItem('v', instance.version); - // Init service worker - initializeSw(); -}); + // Init service worker + initializeSw(); + }); -const app = createApp( - window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) : - !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : - ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : - ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : - defineAsyncComponent(() => import('@/ui/universal.vue')), -); + const app = createApp( + window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) : + !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : + ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : + ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : + defineAsyncComponent(() => import('@/ui/universal.vue')), + ); -if (_DEV_) { - app.config.performance = true; -} + if (_DEV_) { + app.config.performance = true; + } -app.config.globalProperties = { - $i, - $store: defaultStore, - $instance: instance, - $t: i18n.t, - $ts: i18n.ts, -}; + app.config.globalProperties = { + $i, + $store: defaultStore, + $instance: instance, + $t: i18n.t, + $ts: i18n.ts, + }; -widgets(app); -directives(app); -components(app); + widgets(app); + directives(app); + components(app); -const splash = document.getElementById('splash'); -// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) -if (splash) splash.addEventListener('transitionend', () => { - splash.remove(); -}); + const splash = document.getElementById('splash'); + // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) + if (splash) splash.addEventListener('transitionend', () => { + splash.remove(); + }); -// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 -// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する -const rootEl = (() => { - const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; + // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 + // なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する + const rootEl = (() => { + const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; - const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID); + const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID); - if (currentEl) { - console.warn('multiple import detected'); - return currentEl; - } + if (currentEl) { + console.warn('multiple import detected'); + return currentEl; + } - const rootEl = document.createElement('div'); - rootEl.id = MISSKEY_MOUNT_DIV_ID; - document.body.appendChild(rootEl); - return rootEl; -})(); + const rootEl = document.createElement('div'); + rootEl.id = MISSKEY_MOUNT_DIV_ID; + document.body.appendChild(rootEl); + return rootEl; + })(); -app.mount(rootEl); + app.mount(rootEl); -// boot.jsのやつを解除 -window.onerror = null; -window.onunhandledrejection = null; + // boot.jsのやつを解除 + window.onerror = null; + window.onunhandledrejection = null; -reactionPicker.init(); + reactionPicker.init(); -if (splash) { - splash.style.opacity = '0'; - splash.style.pointerEvents = 'none'; -} + if (splash) { + splash.style.opacity = '0'; + splash.style.pointerEvents = 'none'; + } -// クライアントが更新されたか? -const lastVersion = localStorage.getItem('lastVersion'); -if (lastVersion !== version) { - localStorage.setItem('lastVersion', version); + // クライアントが更新されたか? + const lastVersion = localStorage.getItem('lastVersion'); + if (lastVersion !== version) { + localStorage.setItem('lastVersion', version); - // テーマリビルドするため - localStorage.removeItem('theme'); + // テーマリビルドするため + localStorage.removeItem('theme'); - try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため - if (lastVersion != null && compareVersions(version, lastVersion) === 1) { - // ログインしてる場合だけ - if ($i) { - popup(defineAsyncComponent(() => import('@/components/updated.vue')), {}, {}, 'closed'); + try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため + if (lastVersion != null && compareVersions(version, lastVersion) === 1) { + // ログインしてる場合だけ + if ($i) { + popup(defineAsyncComponent(() => import('@/components/updated.vue')), {}, {}, 'closed'); + } } + } catch (err) { } - } catch (err) { } -} -// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため) -watch(defaultStore.reactiveState.darkMode, (darkMode) => { - applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); -}, { immediate: localStorage.theme == null }); + // NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため) + watch(defaultStore.reactiveState.darkMode, (darkMode) => { + applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); + }, { immediate: localStorage.theme == null }); -const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); -const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); - -watch(darkTheme, (theme) => { - if (defaultStore.state.darkMode) { - applyTheme(theme); - } -}); + const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); + const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); -watch(lightTheme, (theme) => { - if (!defaultStore.state.darkMode) { - applyTheme(theme); - } -}); + watch(darkTheme, (theme) => { + if (defaultStore.state.darkMode) { + applyTheme(theme); + } + }); -//#region Sync dark mode -if (ColdDeviceStorage.get('syncDeviceDarkMode')) { - defaultStore.set('darkMode', isDeviceDarkmode()); -} + watch(lightTheme, (theme) => { + if (!defaultStore.state.darkMode) { + applyTheme(theme); + } + }); -window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { + //#region Sync dark mode if (ColdDeviceStorage.get('syncDeviceDarkMode')) { - defaultStore.set('darkMode', mql.matches); + defaultStore.set('darkMode', isDeviceDarkmode()); } -}); -//#endregion -fetchInstanceMetaPromise.then(() => { - if (defaultStore.state.themeInitial) { - if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme)); - if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme)); - defaultStore.set('themeInitial', false); - } -}); + window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { + if (ColdDeviceStorage.get('syncDeviceDarkMode')) { + defaultStore.set('darkMode', mql.matches); + } + }); + //#endregion -watch(defaultStore.reactiveState.useBlurEffectForModal, v => { - document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); -}, { immediate: true }); + fetchInstanceMetaPromise.then(() => { + if (defaultStore.state.themeInitial) { + if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme)); + if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme)); + defaultStore.set('themeInitial', false); + } + }); -watch(defaultStore.reactiveState.useBlurEffect, v => { - if (v) { - document.documentElement.style.removeProperty('--blur'); - } else { - document.documentElement.style.setProperty('--blur', 'none'); - } -}, { immediate: true }); + watch(defaultStore.reactiveState.useBlurEffectForModal, v => { + document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); + }, { immediate: true }); -let reloadDialogShowing = false; -stream.on('_disconnected_', async () => { - if (defaultStore.state.serverDisconnectedBehavior === 'reload') { - location.reload(); - } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { - if (reloadDialogShowing) return; - reloadDialogShowing = true; - const { canceled } = await confirm({ - type: 'warning', - title: i18n.ts.disconnectedFromServer, - text: i18n.ts.reloadConfirm, - }); - reloadDialogShowing = false; - if (!canceled) { - location.reload(); + watch(defaultStore.reactiveState.useBlurEffect, v => { + if (v) { + document.documentElement.style.removeProperty('--blur'); + } else { + document.documentElement.style.setProperty('--blur', 'none'); } - } -}); + }, { immediate: true }); -stream.on('emojiAdded', emojiData => { - // TODO - //store.commit('instance/set', ); -}); - -for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { - import('./plugin').then(({ install }) => { - install(plugin); + let reloadDialogShowing = false; + stream.on('_disconnected_', async () => { + if (defaultStore.state.serverDisconnectedBehavior === 'reload') { + location.reload(); + } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { + if (reloadDialogShowing) return; + reloadDialogShowing = true; + const { canceled } = await confirm({ + type: 'warning', + title: i18n.ts.disconnectedFromServer, + text: i18n.ts.reloadConfirm, + }); + reloadDialogShowing = false; + if (!canceled) { + location.reload(); + } + } }); -} - -const hotkeys = { - 'd': (): void => { - defaultStore.set('darkMode', !defaultStore.state.darkMode); - }, - 's': search, -}; -if ($i) { - // only add post shortcuts if logged in - hotkeys['p|n'] = post; + stream.on('emojiAdded', emojiData => { + // TODO + //store.commit('instance/set', ); + }); - if ($i.isDeleted) { - alert({ - type: 'warning', - text: i18n.ts.accountDeletionInProgress, + for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { + import('./plugin').then(({ install }) => { + install(plugin); }); } - const lastUsed = localStorage.getItem('lastUsed'); - if (lastUsed) { - const lastUsedDate = parseInt(lastUsed, 10); - // 二時間以上前なら - if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { - toast(i18n.t('welcomeBackWithName', { - name: $i.name || $i.username, - })); + const hotkeys = { + 'd': (): void => { + defaultStore.set('darkMode', !defaultStore.state.darkMode); + }, + 's': search, + }; + + if ($i) { + // only add post shortcuts if logged in + hotkeys['p|n'] = post; + + if ($i.isDeleted) { + alert({ + type: 'warning', + text: i18n.ts.accountDeletionInProgress, + }); } - } - localStorage.setItem('lastUsed', Date.now().toString()); - if ('Notification' in window) { - // 許可を得ていなかったらリクエスト - if (Notification.permission === 'default') { - Notification.requestPermission(); + const lastUsed = localStorage.getItem('lastUsed'); + if (lastUsed) { + const lastUsedDate = parseInt(lastUsed, 10); + // 二時間以上前なら + if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { + toast(i18n.t('welcomeBackWithName', { + name: $i.name || $i.username, + })); + } } - } + localStorage.setItem('lastUsed', Date.now().toString()); - const main = markRaw(stream.useChannel('main', null, 'System')); + if ('Notification' in window) { + // 許可を得ていなかったらリクエスト + if (Notification.permission === 'default') { + Notification.requestPermission(); + } + } - // 自分の情報が更新されたとき - main.on('meUpdated', i => { - updateAccount(i); - }); + const main = markRaw(stream.useChannel('main', null, 'System')); - main.on('readAllNotifications', () => { - updateAccount({ hasUnreadNotification: false }); - }); + // 自分の情報が更新されたとき + main.on('meUpdated', i => { + updateAccount(i); + }); - main.on('unreadNotification', () => { - updateAccount({ hasUnreadNotification: true }); - }); + main.on('readAllNotifications', () => { + updateAccount({ hasUnreadNotification: false }); + }); - main.on('unreadMention', () => { - updateAccount({ hasUnreadMentions: true }); - }); + main.on('unreadNotification', () => { + updateAccount({ hasUnreadNotification: true }); + }); - main.on('readAllUnreadMentions', () => { - updateAccount({ hasUnreadMentions: false }); - }); + main.on('unreadMention', () => { + updateAccount({ hasUnreadMentions: true }); + }); - main.on('unreadSpecifiedNote', () => { - updateAccount({ hasUnreadSpecifiedNotes: true }); - }); + main.on('readAllUnreadMentions', () => { + updateAccount({ hasUnreadMentions: false }); + }); - main.on('readAllUnreadSpecifiedNotes', () => { - updateAccount({ hasUnreadSpecifiedNotes: false }); - }); + main.on('unreadSpecifiedNote', () => { + updateAccount({ hasUnreadSpecifiedNotes: true }); + }); - main.on('readAllMessagingMessages', () => { - updateAccount({ hasUnreadMessagingMessage: false }); - }); + main.on('readAllUnreadSpecifiedNotes', () => { + updateAccount({ hasUnreadSpecifiedNotes: false }); + }); - main.on('unreadMessagingMessage', () => { - updateAccount({ hasUnreadMessagingMessage: true }); - sound.play('chatBg'); - }); + main.on('readAllMessagingMessages', () => { + updateAccount({ hasUnreadMessagingMessage: false }); + }); - main.on('readAllAntennas', () => { - updateAccount({ hasUnreadAntenna: false }); - }); + main.on('unreadMessagingMessage', () => { + updateAccount({ hasUnreadMessagingMessage: true }); + sound.play('chatBg'); + }); - main.on('unreadAntenna', () => { - updateAccount({ hasUnreadAntenna: true }); - sound.play('antenna'); - }); + main.on('readAllAntennas', () => { + updateAccount({ hasUnreadAntenna: false }); + }); - main.on('readAllAnnouncements', () => { - updateAccount({ hasUnreadAnnouncement: false }); - }); + main.on('unreadAntenna', () => { + updateAccount({ hasUnreadAntenna: true }); + sound.play('antenna'); + }); - main.on('readAllChannels', () => { - updateAccount({ hasUnreadChannel: false }); - }); + main.on('readAllAnnouncements', () => { + updateAccount({ hasUnreadAnnouncement: false }); + }); - main.on('unreadChannel', () => { - updateAccount({ hasUnreadChannel: true }); - sound.play('channel'); - }); + main.on('readAllChannels', () => { + updateAccount({ hasUnreadChannel: false }); + }); - // トークンが再生成されたとき - // このままではMisskeyが利用できないので強制的にサインアウトさせる - main.on('myTokenRegenerated', () => { - signout(); - }); -} + main.on('unreadChannel', () => { + updateAccount({ hasUnreadChannel: true }); + sound.play('channel'); + }); + + // トークンが再生成されたとき + // このままではMisskeyが利用できないので強制的にサインアウトさせる + main.on('myTokenRegenerated', () => { + signout(); + }); + } -// shortcut -document.addEventListener('keydown', makeHotkey(hotkeys)); + // shortcut + document.addEventListener('keydown', makeHotkey(hotkeys)); +})(); diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts index cd29ae87a5..4efa0345af 100644 --- a/packages/client/src/nirax.ts +++ b/packages/client/src/nirax.ts @@ -3,6 +3,7 @@ import { EventEmitter } from 'eventemitter3'; import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue'; import { pleaseLogin } from '@/scripts/please-login'; +import { safeURIDecode } from '@/scripts/safe-uri-decode'; type RouteDef = { path: string; @@ -116,7 +117,7 @@ export class Router extends EventEmitter<{ } if (p.wildcard) { if (parts.length !== 0) { - props.set(p.name, parts.join('/')); + props.set(p.name, safeURIDecode(parts.join('/'))); parts = []; } break pathMatchLoop; @@ -124,10 +125,12 @@ export class Router extends EventEmitter<{ if (p.startsWith) { if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop; - props.set(p.name, parts[0].substring(p.startsWith.length)); + props.set(p.name, safeURIDecode(parts[0].substring(p.startsWith.length))); parts.shift(); } else { - props.set(p.name, parts[0]); + if (parts[0]) { + props.set(p.name, safeURIDecode(parts[0])); + } parts.shift(); } } @@ -137,7 +140,7 @@ export class Router extends EventEmitter<{ if (parts.length !== 0) continue forEachRouteLoop; if (route.hash != null && hash != null) { - props.set(route.hash, hash); + props.set(route.hash, safeURIDecode(hash)); } if (route.query != null && queryString != null) { @@ -147,7 +150,7 @@ export class Router extends EventEmitter<{ for (const q in route.query) { const as = route.query[q]; if (queryObject[q]) { - props.set(as, queryObject[q]); + props.set(as, safeURIDecode(queryObject[q])); } } } diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue index a80041b5ce..fd7b5f936d 100644 --- a/packages/client/src/pages/about-misskey.vue +++ b/packages/client/src/pages/about-misskey.vue @@ -1,7 +1,7 @@ <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> - <div style="overflow: hidden; overflow: clip;"> + <div style="overflow: clip;"> <MkSpacer :content-max="600" :margin-min="20"> <div class="_formRoot znqjceqz"> <div id="debug"></div> diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue index 6241bbbdda..e482c4ca6d 100644 --- a/packages/client/src/pages/about.vue +++ b/packages/client/src/pages/about.vue @@ -138,7 +138,7 @@ definePageMetadata(computed(() => ({ .fwhjspax { text-align: center; border-radius: 10px; - overflow: hidden; overflow: clip; + overflow: clip; background-size: cover; background-position: center center; diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue index f0ac5b3fc9..d82880c34a 100644 --- a/packages/client/src/pages/admin/index.vue +++ b/packages/client/src/pages/admin/index.vue @@ -58,7 +58,7 @@ let view = $ref(null); let el = $ref(null); let pageProps = $ref({}); let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail); -let noBotProtection = !instance.enableHcaptcha && !instance.enableRecaptcha; +let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha; let noEmailServer = !instance.enableEmail; let thereIsUnresolvedAbuseReport = $ref(false); diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue index 7e085106b9..c186df15a7 100644 --- a/packages/client/src/pages/admin/overview.vue +++ b/packages/client/src/pages/admin/overview.vue @@ -355,7 +355,7 @@ async function renderChart() { plugins: [{ id: 'vLine', beforeDraw(chart, args, options) { - if (chart.tooltip._active && chart.tooltip._active.length) { + if (chart.tooltip?._active?.length) { const activePoint = chart.tooltip._active[0]; const ctx = chart.ctx; const x = activePoint.element.x; @@ -561,7 +561,7 @@ definePageMetadata({ > .body { background: var(--panel); border-radius: var(--radius); - overflow: hidden; overflow: clip; + overflow: clip; } } @@ -620,7 +620,7 @@ definePageMetadata({ > .body { background: var(--panel); border-radius: var(--radius); - overflow: hidden; overflow: clip; + overflow: clip; } } } diff --git a/packages/client/src/pages/antenna-timeline.vue b/packages/client/src/pages/antenna-timeline.vue index 309f94f9f5..1865781a0e 100644 --- a/packages/client/src/pages/antenna-timeline.vue +++ b/packages/client/src/pages/antenna-timeline.vue @@ -110,7 +110,7 @@ definePageMetadata(computed(() => antenna ? { > .tl { background: var(--bg); border-radius: var(--radius); - overflow: hidden; overflow: clip; + overflow: clip; } &.min-width_800px { diff --git a/packages/client/src/pages/explore.users.vue b/packages/client/src/pages/explore.users.vue index bdc96b33a3..8af37ab529 100644 --- a/packages/client/src/pages/explore.users.vue +++ b/packages/client/src/pages/explore.users.vue @@ -1,5 +1,9 @@ <template> <MkSpacer :content-max="1200"> + <MkTab v-model="origin" style="margin-bottom: var(--margin);"> + <option value="local">{{ $ts.local }}</option> + <option value="remote">{{ $ts.remote }}</option> + </MkTab> <div v-if="origin === 'local'"> <template v-if="tag == null"> <MkFolder class="_gap" persist-key="explore-pinned-users"> @@ -57,16 +61,17 @@ import { computed, watch } from 'vue'; import XUserList from '@/components/user-list.vue'; import MkFolder from '@/components/ui/folder.vue'; +import MkTab from '@/components/tab.vue'; import number from '@/filters/number'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { instance } from '@/instance'; const props = defineProps<{ - origin: 'local' | 'remote'; tag?: string; }>(); +let origin = $ref('local'); let tagsEl = $ref<InstanceType<typeof MkFolder>>(); let tagsLocal = $ref([]); let tagsRemote = $ref([]); diff --git a/packages/client/src/pages/explore.vue b/packages/client/src/pages/explore.vue index c0b9438a50..059e94d1df 100644 --- a/packages/client/src/pages/explore.vue +++ b/packages/client/src/pages/explore.vue @@ -5,11 +5,8 @@ <div v-if="tab === 'featured'"> <XFeatured/> </div> - <div v-else-if="tab === 'localUsers'"> - <XUsers origin="local"/> - </div> - <div v-else-if="tab === 'remoteUsers'"> - <XUsers origin="remote"/> + <div v-else-if="tab === 'users'"> + <XUsers/> </div> <div v-else-if="tab === 'search'"> <MkSpacer :content-max="1200"> @@ -75,14 +72,10 @@ const headerTabs = $computed(() => [{ icon: 'fas fa-bolt', title: i18n.ts.featured, }, { - key: 'localUsers', + key: 'users', icon: 'fas fa-users', title: i18n.ts.users, }, { - key: 'remoteUsers', - icon: 'fas fa-users', - title: i18n.ts.remote, -}, { key: 'search', title: i18n.ts.search, }]); diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue index 2a326fc2b6..121709f127 100644 --- a/packages/client/src/pages/settings/profile.vue +++ b/packages/client/src/pages/settings/profile.vue @@ -188,7 +188,7 @@ definePageMetadata({ background-size: cover; background-position: center; border-radius: 10px; - overflow: hidden; overflow: clip; + overflow: clip; > .avatar { display: inline-block; diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue index d330e1ba25..667155019b 100644 --- a/packages/client/src/pages/settings/theme.vue +++ b/packages/client/src/pages/settings/theme.vue @@ -181,7 +181,7 @@ definePageMetadata({ > .toggleWrapper { display: inline-block; text-align: left; - overflow: hidden; overflow: clip; + overflow: clip; padding: 0 100px; input { diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index 8554a9aebc..40eb85ff43 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -177,7 +177,7 @@ definePageMetadata(computed(() => ({ > .tl { background: var(--bg); border-radius: var(--radius); - overflow: hidden; overflow: clip; + overflow: clip; } } </style> diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue index 3fca6f1416..593db1dea7 100644 --- a/packages/client/src/pages/user-list-timeline.vue +++ b/packages/client/src/pages/user-list-timeline.vue @@ -103,7 +103,7 @@ definePageMetadata(computed(() => list ? { > .tl { background: var(--bg); border-radius: var(--radius); - overflow: hidden; overflow: clip; + overflow: clip; } &.min-width_800px { diff --git a/packages/client/src/pages/user/index.photos.vue b/packages/client/src/pages/user/index.photos.vue index cedb0e05f3..79dd1726e1 100644 --- a/packages/client/src/pages/user/index.photos.vue +++ b/packages/client/src/pages/user/index.photos.vue @@ -90,7 +90,7 @@ export default defineComponent({ > .img { height: 128px; border-radius: 6px; - overflow: hidden; overflow: clip; + overflow: clip; } } diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue index f9d5852212..240177c9d9 100644 --- a/packages/client/src/pages/welcome.entrance.a.vue +++ b/packages/client/src/pages/welcome.entrance.a.vue @@ -276,7 +276,7 @@ function showMenu(ev) { -webkit-backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px)); border-radius: 999px; - overflow: hidden; overflow: clip; + overflow: clip; width: 800px; padding: 8px 0; diff --git a/packages/client/src/scripts/emojilist.ts b/packages/client/src/scripts/emojilist.ts index 4196170d24..4ce63dc7e7 100644 --- a/packages/client/src/scripts/emojilist.ts +++ b/packages/client/src/scripts/emojilist.ts @@ -8,4 +8,6 @@ export type UnicodeEmojiDef = { } // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -export const emojilist = (await import('../emojilist.json')).default as UnicodeEmojiDef[]; +import _emojilist from '../emojilist.json'; + +export const emojilist = _emojilist as UnicodeEmojiDef[]; diff --git a/packages/client/src/scripts/hpml/expr.ts b/packages/client/src/scripts/hpml/expr.ts index 00e3ed118b..18c7c2a14b 100644 --- a/packages/client/src/scripts/hpml/expr.ts +++ b/packages/client/src/scripts/hpml/expr.ts @@ -16,7 +16,7 @@ export type TextValue = ExprBase & { value: string; }; -export type MultiLineTextValue = ExprBase & { +export type MultiLineTextValue = ExprBase & { type: 'multiLineText'; value: string; }; diff --git a/packages/client/src/scripts/idb-proxy.ts b/packages/client/src/scripts/idb-proxy.ts index d462a0d7ce..77bb84463c 100644 --- a/packages/client/src/scripts/idb-proxy.ts +++ b/packages/client/src/scripts/idb-proxy.ts @@ -11,16 +11,15 @@ const fallbackName = (key: string) => `idbfallback::${key}`; let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true; if (idbAvailable) { - try { - await iset('idb-test', 'test'); - } catch (err) { + iset('idb-test', 'test').catch(err => { console.error('idb error', err); + console.error('indexedDB is unavailable. It will use localStorage.'); idbAvailable = false; - } + }); +} else { + console.error('indexedDB is unavailable. It will use localStorage.'); } -if (!idbAvailable) console.error('indexedDB is unavailable. It will use localStorage.'); - export async function get(key: string) { if (idbAvailable) return iget(key); return JSON.parse(localStorage.getItem(fallbackName(key))); diff --git a/packages/client/src/scripts/safe-uri-decode.ts b/packages/client/src/scripts/safe-uri-decode.ts new file mode 100644 index 0000000000..301b56d7fd --- /dev/null +++ b/packages/client/src/scripts/safe-uri-decode.ts @@ -0,0 +1,7 @@ +export function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} diff --git a/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts index dec9fb355c..77d1df0826 100644 --- a/packages/client/src/scripts/theme.ts +++ b/packages/client/src/scripts/theme.ts @@ -1,6 +1,6 @@ import { ref } from 'vue'; -import { globalEvents } from '@/events'; import tinycolor from 'tinycolor2'; +import { globalEvents } from '@/events'; export type Theme = { id: string; @@ -31,11 +31,11 @@ export const getBuiltinThemes = () => Promise.all( 'd-astro', 'd-future', 'd-botanical', + 'd-green-lime', + 'd-green-orange', 'd-cherry', 'd-ice', - 'd-pumpkin', - 'd-black', - ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)) + ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)), ); export const getBuiltinThemesRef = () => { diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss index 9b30e64a31..0f892d2e19 100644 --- a/packages/client/src/style.scss +++ b/packages/client/src/style.scss @@ -252,7 +252,7 @@ hr { ._panel { background: var(--panel); border-radius: var(--radius); - overflow: hidden; overflow: clip; + overflow: clip; } ._block { diff --git a/packages/client/src/themes/d-black.json5 b/packages/client/src/themes/d-black.json5 deleted file mode 100644 index 3c18ebdaf1..0000000000 --- a/packages/client/src/themes/d-black.json5 +++ /dev/null @@ -1,17 +0,0 @@ -{ - id: '8c539dc1-0fab-4d47-9194-39c508e9bfe1', - - name: 'Mi Black', - author: 'syuilo', - - base: 'dark', - - props: { - divider: '#2d2d2d', - panel: '#131313', - panelHeaderBg: '@panel', - panelHeaderDivider: '@divider', - shadow: 'rgba(255, 255, 255, 0.05)', - modalBg: 'rgba(255, 255, 255, 0.1)', - }, -} diff --git a/packages/client/src/themes/d-green-lime.json5 b/packages/client/src/themes/d-green-lime.json5 new file mode 100644 index 0000000000..a6983b9ac2 --- /dev/null +++ b/packages/client/src/themes/d-green-lime.json5 @@ -0,0 +1,24 @@ +{ + id: '02816013-8107-440f-877e-865083ffe194', + + name: 'Mi Green+Lime Dark', + author: 'syuilo', + + base: 'dark', + + props: { + accent: '#b4e900', + bg: '#0C1210', + fg: '#dee7e4', + fgHighlighted: '#fff', + fgOnAccent: '#192320', + divider: '#e7fffb24', + panel: '#192320', + panelHeaderBg: '@panel', + panelHeaderDivider: '@divider', + popup: '#293330', + renote: '@accent', + mentionMe: '#ffaa00', + link: '#24d7ce', + }, +} diff --git a/packages/client/src/themes/d-green-orange.json5 b/packages/client/src/themes/d-green-orange.json5 new file mode 100644 index 0000000000..62adc39e29 --- /dev/null +++ b/packages/client/src/themes/d-green-orange.json5 @@ -0,0 +1,24 @@ +{ + id: 'dc489603-27b5-424a-9b25-1ff6aec9824a', + + name: 'Mi Green+Orange Dark', + author: 'syuilo', + + base: 'dark', + + props: { + accent: '#e97f00', + bg: '#0C1210', + fg: '#dee7e4', + fgHighlighted: '#fff', + fgOnAccent: '#192320', + divider: '#e7fffb24', + panel: '#192320', + panelHeaderBg: '@panel', + panelHeaderDivider: '@divider', + popup: '#293330', + renote: '@accent', + mentionMe: '#b4e900', + link: '#24d7ce', + }, +} diff --git a/packages/client/src/themes/d-pumpkin.json5 b/packages/client/src/themes/d-pumpkin.json5 deleted file mode 100644 index 064ca4577b..0000000000 --- a/packages/client/src/themes/d-pumpkin.json5 +++ /dev/null @@ -1,88 +0,0 @@ -{ - id: '0b64fef3-02c7-20b5-dd87-b3f77e2b4301', - - name: 'Mi Pumpkin Dark', - author: 'syuilo', - - base: 'dark', - - props: { - X2: ':darken<2<@panel', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - bg: 'rgb(37, 32, 47)', - fg: '#e0d5c0', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - cwBg: '#687390', - cwFg: '#393f4f', - link: 'rgb(172, 193, 68)', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: ':lighten<3<@bg', - popup: ':lighten<3<@panel', - accent: 'rgb(242, 133, 36)', - header: ':alpha<0.7<@panel', - infoBg: '#253142', - infoFg: '#fff', - renote: 'rgb(110, 179, 72)', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: 'rgba(255, 255, 255, 0.1)', - hashtag: 'rgb(188, 90, 255)', - mention: 'rgb(72, 179, 139)', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#86b300', - buttonBg: 'rgba(255, 255, 255, 0.05)', - switchBg: 'rgba(255, 255, 255, 0.15)', - acrylicBg: ':alpha<0.5<@bg', - cwHoverBg: '#707b97', - indicator: '@accent', - mentionMe: '@accent', - messageBg: '@bg', - navActive: '@accent', - accentedBg: ':alpha<0.15<@accent', - fgOnAccent: '#000', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - navHoverFg: ':lighten<17<@fg', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@indicator', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - htmlThemeColor: '@bg', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - }, -} diff --git a/packages/client/src/ui/_common_/statusbars.vue b/packages/client/src/ui/_common_/statusbars.vue index 80949665b3..e50b4e54c3 100644 --- a/packages/client/src/ui/_common_/statusbars.vue +++ b/packages/client/src/ui/_common_/statusbars.vue @@ -64,7 +64,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') width: 100%; line-height: var(--height); height: var(--height); - overflow: hidden; overflow: clip; + overflow: clip; contain: strict; > .name { diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue index a2c26f536e..70db7ed12b 100644 --- a/packages/client/src/ui/classic.vue +++ b/packages/client/src/ui/classic.vue @@ -241,7 +241,7 @@ onMounted(() => { border-left: solid 1px var(--divider); border-right: solid 1px var(--divider); border-radius: 0; - overflow: hidden; overflow: clip; + overflow: clip; --margin: 12px; } diff --git a/packages/client/src/ui/deck/list-column.vue b/packages/client/src/ui/deck/list-column.vue index 843a3bd1cb..a27e65594a 100644 --- a/packages/client/src/ui/deck/list-column.vue +++ b/packages/client/src/ui/deck/list-column.vue @@ -9,7 +9,7 @@ </template> <script lang="ts" setup> -import { } from 'vue'; +import { } from 'vue'; import XColumn from './column.vue'; import XTimeline from '@/components/timeline.vue'; import * as os from '@/os'; diff --git a/packages/client/src/widgets/activity.chart.vue b/packages/client/src/widgets/activity.chart.vue index b7db2af580..b61e419f94 100644 --- a/packages/client/src/widgets/activity.chart.vue +++ b/packages/client/src/widgets/activity.chart.vue @@ -39,15 +39,15 @@ let pointsRenote: any = $ref(null); let pointsTotal: any = $ref(null); function dragListen(fn) { - window.addEventListener('mousemove', fn); + window.addEventListener('mousemove', fn); window.addEventListener('mouseleave', dragClear.bind(null, fn)); - window.addEventListener('mouseup', dragClear.bind(null, fn)); + window.addEventListener('mouseup', dragClear.bind(null, fn)); } function dragClear(fn) { - window.removeEventListener('mousemove', fn); + window.removeEventListener('mousemove', fn); window.removeEventListener('mouseleave', dragClear); - window.removeEventListener('mouseup', dragClear); + window.removeEventListener('mouseup', dragClear); } function onMousedown(ev) { |