diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-04-18 00:07:33 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-04-18 00:07:33 +0900 |
| commit | 938fcb3e5ecc6862e40c9cb85b8010af63c69181 (patch) | |
| tree | 512fa77d5b4dca9efd38af00a805c187876029fc /src/client/components | |
| parent | Merge branch 'develop' (diff) | |
| parent | 12.77.0 (diff) | |
| download | misskey-938fcb3e5ecc6862e40c9cb85b8010af63c69181.tar.gz misskey-938fcb3e5ecc6862e40c9cb85b8010af63c69181.tar.bz2 misskey-938fcb3e5ecc6862e40c9cb85b8010af63c69181.zip | |
Merge branch 'develop'
Diffstat (limited to 'src/client/components')
23 files changed, 531 insertions, 120 deletions
diff --git a/src/client/components/avatars.vue b/src/client/components/avatars.vue index cac95e6d40..da862967dd 100644 --- a/src/client/components/avatars.vue +++ b/src/client/components/avatars.vue @@ -1,7 +1,7 @@ <template> <div> <div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;"> - <MkAvatar :user="user" style="width:32px;height:32px;"/> + <MkAvatar :user="user" style="width:32px;height:32px;" :show-indicator="true"/> </div> </div> </template> diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue index 833cdfc898..012ed42385 100644 --- a/src/client/components/date-separated-list.vue +++ b/src/client/components/date-separated-list.vue @@ -18,7 +18,12 @@ export default defineComponent({ type: Boolean, required: false, default: false - } + }, + noGap: { + type: Boolean, + required: false, + default: false + }, }, methods: { @@ -37,18 +42,16 @@ export default defineComponent({ }, render() { - const noGap = [...document.querySelectorAll('._noGap_')].some(el => el.contains(this.$parent.$el)); - if (this.items.length === 0) return; return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? { - class: 'sqadhkmv' + (noGap ? ' _block' : ''), + class: 'sqadhkmv' + (this.noGap ? ' noGap _block' : ''), name: 'list', tag: 'div', 'data-direction': this.direction, 'data-reversed': this.reversed ? 'true' : 'false', } : { - class: 'sqadhkmv', + class: 'sqadhkmv' + (this.noGap ? ' noGap _block' : ''), }, this.items.map((item, i) => { const el = this.$slots.default({ item: item @@ -154,17 +157,17 @@ export default defineComponent({ } } } -} -._noGap_ .sqadhkmv { - > * { - margin: 0 !important; - border: none; - border-radius: 0; - box-shadow: none; + &.noGap { + > * { + margin: 0 !important; + border: none; + border-radius: 0; + box-shadow: none; - &:not(:last-child) { - border-bottom: solid 0.5px var(--divider); + &:not(:last-child) { + border-bottom: solid 0.5px var(--divider); + } } } } diff --git a/src/client/components/drive-file-thumbnail.vue b/src/client/components/drive-file-thumbnail.vue index 91f57d1f49..aadf22ed77 100644 --- a/src/client/components/drive-file-thumbnail.vue +++ b/src/client/components/drive-file-thumbnail.vue @@ -26,7 +26,7 @@ import { faFileArchive, faFilm } from '@fortawesome/free-solid-svg-icons'; -import ImgWithBlurhash from './img-with-blurhash.vue'; +import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; import { ColdDeviceStorage } from '@client/store'; export default defineComponent({ diff --git a/src/client/components/form/base.vue b/src/client/components/form/base.vue index 84438a5b32..34deb39465 100644 --- a/src/client/components/form/base.vue +++ b/src/client/components/form/base.vue @@ -24,9 +24,12 @@ export default defineComponent({ --formXPadding: 32px; --formYPadding: 32px; + font-size: 95%; line-height: 1.3em; background: var(--bg); padding: var(--formYPadding) var(--formXPadding); + max-width: 750px; + margin: 0 auto; &:not(.wide).max-width_400px { --formXPadding: 0px; @@ -40,16 +43,16 @@ export default defineComponent({ } ._form_group { - > * { - &:not(:first-child) { + > *:not(._formNoConcat) { + &:not(:last-child):not(._formNoConcatPrev) { &._formPanel, ._formPanel { - border-top: none; + border-bottom: solid 0.5px var(--divider); } } - &:not(:last-child) { + &:not(:first-child):not(._formNoConcatNext) { &._formPanel, ._formPanel { - border-bottom: solid 0.5px var(--divider); + border-top: none; } } } diff --git a/src/client/components/form/group.vue b/src/client/components/form/group.vue index d07852155a..34ccaeff07 100644 --- a/src/client/components/form/group.vue +++ b/src/client/components/form/group.vue @@ -1,7 +1,7 @@ <template> -<div class="vrtktovg _formItem" v-size="{ max: [500] }"> +<div class="vrtktovg _formItem _formNoConcat" v-size="{ max: [500] }" v-sticky-container> <div class="_formLabel"><slot name="label"></slot></div> - <div class="main _form_group"> + <div class="main _form_group" ref="child"> <slot></slot> </div> <div class="_formCaption"><slot name="caption"></slot></div> @@ -9,33 +9,69 @@ </template> <script lang="ts"> -import { defineComponent } from 'vue'; +import { defineComponent, onMounted, ref } from 'vue'; export default defineComponent({ + setup(props, context) { + const child = ref<HTMLElement | null>(null); + + const scanChild = () => { + if (child.value == null) return; + const els = Array.from(child.value.children); + for (let i = 0; i < els.length; i++) { + const el = els[i]; + if (el.classList.contains('_formNoConcat')) { + if (els[i - 1]) els[i - 1].classList.add('_formNoConcatPrev'); + if (els[i + 1]) els[i + 1].classList.add('_formNoConcatNext'); + } + } + }; + + onMounted(() => { + scanChild(); + + const observer = new MutationObserver(records => { + scanChild(); + }); + + observer.observe(child.value, { + childList: true, + subtree: false, + attributes: false, + characterData: false, + }); + }); + + return { + child + }; + } }); </script> <style lang="scss" scoped> .vrtktovg { > .main { - > ::v-deep(*) { - margin: 0; - - &:not(:first-child) { - &._formPanel, ._formPanel { - border-top: none; - border-top-left-radius: 0; - border-top-right-radius: 0; - } + > ::v-deep(*):not(._formNoConcat) { + &:not(._formNoConcatNext) { + margin: 0; } - &:not(:last-child) { + &:not(:last-child):not(._formNoConcatPrev) { &._formPanel, ._formPanel { border-bottom: solid 0.5px var(--divider); border-bottom-left-radius: 0; border-bottom-right-radius: 0; } } + + &:not(:first-child):not(._formNoConcatNext) { + &._formPanel, ._formPanel { + border-top: none; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } } } } diff --git a/src/client/components/form/key-value-view.vue b/src/client/components/form/key-value-view.vue index eadc675f89..85f4febef9 100644 --- a/src/client/components/form/key-value-view.vue +++ b/src/client/components/form/key-value-view.vue @@ -22,9 +22,17 @@ export default defineComponent({ align-items: center; padding: 14px 16px; + > .key { + margin-right: 12px; + white-space: nowrap; + } + > .value { margin-left: auto; opacity: 0.7; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } } </style> diff --git a/src/client/components/form/object-view.vue b/src/client/components/form/object-view.vue new file mode 100644 index 0000000000..cbd4186e56 --- /dev/null +++ b/src/client/components/form/object-view.vue @@ -0,0 +1,102 @@ +<template> +<FormGroup class="_formItem"> + <template #label><slot></slot></template> + <div class="drooglns _formItem" :class="{ tall }"> + <div class="input _formPanel"> + <textarea class="_monospace" + v-model="v" + readonly + :spellcheck="false" + ></textarea> + </div> + </div> + <template #caption><slot name="desc"></slot></template> +</FormGroup> +</template> + +<script lang="ts"> +import { defineComponent, ref, toRefs, watch } from 'vue'; +import * as JSON5 from 'json5'; +import './form.scss'; +import FormGroup from './group.vue'; + +export default defineComponent({ + components: { + FormGroup, + }, + props: { + value: { + required: false + }, + tall: { + type: Boolean, + required: false, + default: false + }, + pre: { + type: Boolean, + required: false, + default: false + }, + manualSave: { + type: Boolean, + required: false, + default: false + }, + }, + setup(props, context) { + const { value } = toRefs(props); + const v = ref(''); + + watch(() => value, newValue => { + v.value = JSON5.stringify(newValue.value, null, '\t'); + }, { + immediate: true + }); + + return { + v, + }; + } +}); +</script> + +<style lang="scss" scoped> +.drooglns { + position: relative; + + > .input { + position: relative; + + > textarea { + display: block; + width: 100%; + min-width: 100%; + max-width: 100%; + min-height: 130px; + margin: 0; + padding: 16px; + box-sizing: border-box; + font: inherit; + font-weight: normal; + font-size: 1em; + background: transparent; + border: none; + border-radius: 0; + outline: none; + box-shadow: none; + color: var(--fg); + tab-size: 2; + white-space: pre; + } + } + + &.tall { + > .input { + > textarea { + min-height: 200px; + } + } + } +} +</style> diff --git a/src/client/components/form/suspense.vue b/src/client/components/form/suspense.vue new file mode 100644 index 0000000000..6a8282733f --- /dev/null +++ b/src/client/components/form/suspense.vue @@ -0,0 +1,92 @@ +<template> +<transition name="fade" mode="out-in"> + <div class="_formItem" v-if="pending"> + <div class="_formPanel"> + <MkLoading/> + </div> + </div> + <FormGroup v-else-if="resolved" class="_formItem"> + <slot :result="result"></slot> + </FormGroup> + <div class="_formItem" v-else> + <div class="_formPanel"> + error! + <button @click="retry">retry</button> + </div> + </div> +</transition> +</template> + +<script lang="ts"> +import { defineComponent, PropType, ref, watch } from 'vue'; +import './form.scss'; +import FormGroup from './group.vue'; + +export default defineComponent({ + components: { + FormGroup, + }, + + props: { + p: { + type: Function as PropType<() => Promise<any>>, + required: true, + } + }, + + setup(props, context) { + const pending = ref(true); + const resolved = ref(false); + const rejected = ref(false); + const result = ref(null); + + const process = () => { + if (props.p == null) { + return; + } + const promise = props.p(); + pending.value = true; + resolved.value = false; + rejected.value = false; + promise.then((_result) => { + pending.value = false; + resolved.value = true; + result.value = _result; + }); + promise.catch(() => { + pending.value = false; + rejected.value = true; + }); + }; + + watch(() => props.p, () => { + process(); + }, { + immediate: true + }); + + const retry = () => { + process(); + }; + + return { + pending, + resolved, + rejected, + result, + retry, + }; + } +}); +</script> + +<style lang="scss" scoped> +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.125s ease; +} +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} +</style> diff --git a/src/client/components/global/avatar.vue b/src/client/components/global/avatar.vue index 83675a155c..64fb2116b6 100644 --- a/src/client/components/global/avatar.vue +++ b/src/client/components/global/avatar.vue @@ -1,9 +1,11 @@ <template> <span class="eiwwqkts _noSelect" :class="{ cat }" :title="acct(user)" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick"> <img class="inner" :src="url" decoding="async"/> + <MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/> </span> <MkA class="eiwwqkts _noSelect" :class="{ cat }" :to="userPage(user)" :title="acct(user)" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id"> <img class="inner" :src="url" decoding="async"/> + <MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/> </MkA> </template> @@ -12,8 +14,12 @@ import { defineComponent } from 'vue'; import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash'; import { acct, userPage } from '@client/filters/user'; +import MkUserOnlineIndicator from '@client/components/user-online-indicator.vue'; export default defineComponent({ + components: { + MkUserOnlineIndicator + }, props: { user: { type: Object, @@ -30,6 +36,10 @@ export default defineComponent({ disablePreview: { required: false, default: false + }, + showIndicator: { + required: false, + default: false } }, emits: ['click'], @@ -93,7 +103,7 @@ export default defineComponent({ } } - .inner { + > .inner { position: absolute; bottom: 0; left: 0; @@ -106,5 +116,14 @@ export default defineComponent({ width: 100%; height: 100%; } + + > .indicator { + position: absolute; + z-index: 1; + bottom: 0; + left: 0; + width: 20%; + height: 20%; + } } </style> diff --git a/src/client/components/global/loading.vue b/src/client/components/global/loading.vue index 5d0c10c086..9b810f0a16 100644 --- a/src/client/components/global/loading.vue +++ b/src/client/components/global/loading.vue @@ -1,12 +1,11 @@ <template> -<div class="yxspomdl" :class="{ inline }"> +<div class="yxspomdl" :class="{ inline, colored }"> <div class="ring"></div> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; -import * as os from '@client/os'; export default defineComponent({ props: { @@ -14,6 +13,11 @@ export default defineComponent({ type: Boolean, required: false, default: false + }, + colored: { + type: Boolean, + required: false, + default: true } } }); @@ -32,6 +36,11 @@ export default defineComponent({ .yxspomdl { padding: 32px; text-align: center; + cursor: wait; + + &.colored { + color: var(--accent); + } &.inline { display: inline; @@ -41,24 +50,43 @@ export default defineComponent({ width: 32px; height: 32px; } + + > .ring { + &:before, + &:after { + width: 32px; + height: 32px; + } + } } > .ring { + position: relative; display: inline-block; - opacity: 0.7; vertical-align: middle; - } - > .ring:after { - content: " "; - display: block; - box-sizing: border-box; - width: 48px; - height: 48px; - border-radius: 50%; - border: solid 4px; - border-color: currentColor transparent transparent transparent; - animation: ring 0.5s linear infinite; + &:before, + &:after { + content: " "; + display: block; + box-sizing: border-box; + width: 48px; + height: 48px; + border-radius: 50%; + border: solid 4px; + } + + &:before { + border-color: currentColor; + opacity: 0.3; + } + + &:after { + position: absolute; + top: 0; + border-color: currentColor transparent transparent transparent; + animation: ring 0.5s linear infinite; + } } } </style> diff --git a/src/client/components/img-with-blurhash.vue b/src/client/components/img-with-blurhash.vue index 7606708e9b..7e80b00208 100644 --- a/src/client/components/img-with-blurhash.vue +++ b/src/client/components/img-with-blurhash.vue @@ -71,6 +71,7 @@ export default defineComponent({ <style lang="scss" scoped> .xubzgfgb { + position: relative; width: 100%; height: 100%; @@ -82,6 +83,7 @@ export default defineComponent({ } > canvas { + position: absolute; object-fit: cover; } diff --git a/src/client/components/media-image.vue b/src/client/components/media-image.vue index 4de5daa84f..5760466138 100644 --- a/src/client/components/media-image.vue +++ b/src/client/components/media-image.vue @@ -27,7 +27,7 @@ import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-i import { getStaticImageUrl } from '@client/scripts/get-static-image-url'; import { extractAvgColorFromBlurhash } from '@client/scripts/extract-avg-color-from-blurhash'; import ImageViewer from './image-viewer.vue'; -import ImgWithBlurhash from './img-with-blurhash.vue'; +import ImgWithBlurhash from '@client/components/img-with-blurhash.vue'; import * as os from '@client/os'; export default defineComponent({ diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue index b25c97543b..50e76e5299 100644 --- a/src/client/components/note-detailed.vue +++ b/src/client/components/note-detailed.vue @@ -35,7 +35,7 @@ </div> <article class="article" @contextmenu.stop="onContextmenu"> <header class="header"> - <MkAvatar class="avatar" :user="appearNote.user"/> + <MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/> <div class="body"> <div class="top"> <MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.user.id"> diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index aedf11bc40..675748d540 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -1,30 +1,34 @@ <template> -<div> - <div class="_fullinfo" v-if="empty"> +<transition name="fade" mode="out-in"> + <MkLoading v-if="fetching"/> + + <MkError v-else-if="error" @retry="init()"/> + + <div class="_fullinfo" v-else-if="empty"> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $ts.noNotes }}</div> </div> - <MkError v-if="error" @retry="init()"/> + <div v-else> + <div v-show="more && reversed" style="margin-bottom: var(--margin);"> + <MkButton style="margin: 0 auto;" @click="fetchMoreFeature" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <template v-if="!moreFetching">{{ $ts.loadMore }}</template> + <template v-if="moreFetching"><MkLoading inline/></template> + </MkButton> + </div> - <div v-show="more && reversed" style="margin-bottom: var(--margin);"> - <MkButton style="margin: 0 auto;" @click="fetchMoreFeature" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">{{ $ts.loadMore }}</template> - <template v-if="moreFetching"><MkLoading inline/></template> - </MkButton> - </div> + <XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap"> + <XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> + </XList> - <XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> - <XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> - </XList> - - <div v-show="more && !reversed" style="margin-top: var(--margin);"> - <MkButton style="margin: 0 auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">{{ $ts.loadMore }}</template> - <template v-if="moreFetching"><MkLoading inline/></template> - </MkButton> + <div v-show="more && !reversed" style="margin-top: var(--margin);"> + <MkButton style="margin: 0 auto;" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <template v-if="!moreFetching">{{ $ts.loadMore }}</template> + <template v-if="moreFetching"><MkLoading inline/></template> + </MkButton> + </div> </div> -</div> +</transition> </template> <script lang="ts"> @@ -55,11 +59,15 @@ export default defineComponent({ pagination: { required: true }, - prop: { type: String, required: false - } + }, + noGap: { + type: Boolean, + required: false, + default: false + }, }, emits: ['before', 'after'], @@ -90,3 +98,14 @@ export default defineComponent({ } }); </script> + +<style lang="scss" scoped> +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.125s ease; +} +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} +</style> diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue index 0891108d8b..1b789093ef 100644 --- a/src/client/components/notifications.vue +++ b/src/client/components/notifications.vue @@ -1,19 +1,23 @@ <template> -<div class="mfcuwfyp _noGap_ _magnetParent"> - <XList class="notifications _magnetChild" :items="items" v-slot="{ item: notification }"> - <XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/> - <XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/> - </XList> +<transition name="fade" mode="out-in"> + <MkLoading v-if="fetching"/> - <button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">{{ $ts.loadMore }}</template> - <template v-if="moreFetching"><MkLoading inline/></template> - </button> + <MkError v-else-if="error" @retry="init()"/> - <p class="empty" v-if="empty">{{ $ts.noNotifications }}</p> + <p class="mfcuwfyp" v-else-if="empty">{{ $ts.noNotifications }}</p> - <MkError v-if="error" @retry="init()"/> -</div> + <div v-else class="_magnetParent"> + <XList class="notifications _magnetChild" :items="items" v-slot="{ item: notification }" :no-gap="true"> + <XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @update:note="noteUpdated(notification.note, $event)" :key="notification.id"/> + <XNotification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/> + </XList> + + <button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" v-show="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <template v-if="!moreFetching">{{ $ts.loadMore }}</template> + <template v-if="moreFetching"><MkLoading inline/></template> + </button> + </div> +</transition> </template> <script lang="ts"> @@ -120,17 +124,19 @@ export default defineComponent({ </script> <style lang="scss" scoped> -.mfcuwfyp { - > .empty { - margin: 0; - padding: 16px; - text-align: center; - color: var(--fg); - } +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.125s ease; +} +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} - > .placeholder { - padding: 32px; - opacity: 0.3; - } +.mfcuwfyp { + margin: 0; + padding: 16px; + text-align: center; + color: var(--fg); } </style> diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue index faa3984638..753eba2ba1 100644 --- a/src/client/components/timeline.vue +++ b/src/client/components/timeline.vue @@ -1,5 +1,5 @@ <template> -<XNotes :class="{ _noGap_: !$store.state.showGapBetweenNotesInTimeline }" ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/> +<XNotes :no-gap="!$store.state.showGapBetweenNotesInTimeline" ref="tl" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/> </template> <script lang="ts"> diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue index 427421af7d..ecd48876e8 100644 --- a/src/client/components/ui/container.vue +++ b/src/client/components/ui/container.vue @@ -4,7 +4,7 @@ <div class="title"><slot name="header"></slot></div> <div class="sub"> <slot name="func"></slot> - <button class="_button" v-if="bodyTogglable" @click="() => showBody = !showBody"> + <button class="_button" v-if="foldable" @click="() => showBody = !showBody"> <template v-if="showBody"><Fa :icon="faAngleUp"/></template> <template v-else><Fa :icon="faAngleDown"/></template> </button> @@ -16,8 +16,11 @@ @leave="leave" @after-leave="afterLeave" > - <div v-show="showBody"> + <div v-show="showBody" class="content" :class="{ omitted }" ref="content"> <slot></slot> + <button v-if="omitted" class="fade _button" @click="() => { ignoreOmit = true; omitted = false; }"> + <span>{{ $ts.showMore }}</span> + </button> </div> </transition> </div> @@ -39,7 +42,7 @@ export default defineComponent({ required: false, default: false }, - bodyTogglable: { + foldable: { type: Boolean, required: false, default: false @@ -54,10 +57,17 @@ export default defineComponent({ required: false, default: false }, + maxHeight: { + type: Number, + required: false, + default: null + }, }, data() { return { showBody: this.expanded, + omitted: null, + ignoreOmit: false, faAngleUp, faAngleDown }; }, @@ -73,10 +83,23 @@ export default defineComponent({ }, { immediate: true }); + + this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px'); + + const calcOmit = () => { + if (this.omitted || this.ignoreOmit || this.maxHeight == null) return; + const height = this.$refs.content.offsetHeight; + this.omitted = height > this.maxHeight; + }; + + calcOmit(); + new ResizeObserver((entries, observer) => { + calcOmit(); + }).observe(this.$refs.content); }, methods: { toggleContent(show: boolean) { - if (!this.bodyTogglable) return; + if (!this.foldable) return; this.showBody = show; }, @@ -127,7 +150,7 @@ export default defineComponent({ display: flex; flex-direction: column; - > div { + > .content { overflow: auto; } } @@ -169,12 +192,35 @@ export default defineComponent({ } } - > div { - > ::v-deep(._content) { - padding: 24px; + > .content { + &.omitted { + position: relative; + max-height: var(--maxHeight); + overflow: hidden; + + > .fade { + display: block; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 64px; + background: linear-gradient(0deg, var(--panel), var(--X15)); - & + ._content { - border-top: solid 0.5px var(--divider); + > span { + display: inline-block; + background: var(--panel); + padding: 6px 10px; + font-size: 0.8em; + border-radius: 999px; + box-shadow: 0 2px 6px rgb(0 0 0 / 20%); + } + + &:hover { + > span { + background: var(--panelHighlight); + } + } } } } @@ -187,10 +233,7 @@ export default defineComponent({ } } - > div { - > ::v-deep(._content) { - padding: 16px; - } + > .content { } } } diff --git a/src/client/components/ui/tooltip.vue b/src/client/components/ui/tooltip.vue index b220fe5d8c..de8c02ad4a 100644 --- a/src/client/components/ui/tooltip.vue +++ b/src/client/components/ui/tooltip.vue @@ -39,7 +39,7 @@ export default defineComponent({ const contentHeight = this.$refs.content.offsetHeight; let left = rect.left + window.pageXOffset + (this.source.offsetWidth / 2); - let top = rect.top + window.pageYOffset + this.source.offsetHeight; + let top = rect.top + window.pageYOffset - contentHeight; left -= (this.$el.offsetWidth / 2); @@ -47,9 +47,9 @@ export default defineComponent({ left = window.innerWidth - contentWidth + window.pageXOffset - 1; } - if (top + contentHeight - window.pageYOffset > window.innerHeight) { - top = rect.top + window.pageYOffset - contentHeight; - this.$refs.content.style.transformOrigin = 'center bottom'; + if (top - window.pageYOffset < 0) { + top = rect.top + window.pageYOffset + this.source.offsetHeight; + this.$refs.content.style.transformOrigin = 'center top'; } this.$el.style.left = left + 'px'; @@ -81,6 +81,6 @@ export default defineComponent({ text-align: center; border-radius: 4px; pointer-events: none; - transform-origin: center top; + transform-origin: center bottom; } </style> diff --git a/src/client/components/user-info.vue b/src/client/components/user-info.vue index ac2f9a75a6..289e0f3c3f 100644 --- a/src/client/components/user-info.vue +++ b/src/client/components/user-info.vue @@ -1,7 +1,7 @@ <template> <div class="_panel vjnjpkug"> <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> - <MkAvatar class="avatar" :user="user" :disable-preview="true"/> + <MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> <div class="title"> <MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> <p class="username"><MkAcct :user="user"/></p> diff --git a/src/client/components/user-online-indicator.vue b/src/client/components/user-online-indicator.vue new file mode 100644 index 0000000000..bb98978bba --- /dev/null +++ b/src/client/components/user-online-indicator.vue @@ -0,0 +1,50 @@ +<template> +<div class="fzgwjkgc" :class="user.onlineStatus" v-tooltip="text"></div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; + +export default defineComponent({ + props: { + user: { + type: Object, + required: true + }, + }, + + computed: { + text(): string { + switch (this.user.onlineStatus) { + case 'online': return this.$ts.online; + case 'active': return this.$ts.active; + case 'offline': return this.$ts.offline; + case 'unknown': return this.$ts.unknown; + } + } + } +}); +</script> + +<style lang="scss" scoped> +.fzgwjkgc { + box-shadow: 0 0 0 3px var(--panel); + border-radius: 100%; + + &.online { + background: #58d4c9; + } + + &.active { + background: #e4bc48; + } + + &.offline { + background: #ea5353; + } + + &.unknown { + background: #888; + } +} +</style> diff --git a/src/client/components/user-preview.vue b/src/client/components/user-preview.vue index b2b3358374..2ec81a5220 100644 --- a/src/client/components/user-preview.vue +++ b/src/client/components/user-preview.vue @@ -3,7 +3,7 @@ <div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }"> <div v-if="fetched" class="info"> <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div> - <MkAvatar class="avatar" :user="user" :disable-preview="true"/> + <MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/> <div class="title"> <MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA> <p class="username"><MkAcct :user="user"/></p> diff --git a/src/client/components/user-select-dialog.vue b/src/client/components/user-select-dialog.vue index 05a43402a8..a243e182e8 100644 --- a/src/client/components/user-select-dialog.vue +++ b/src/client/components/user-select-dialog.vue @@ -17,7 +17,7 @@ <div class="tbhwbxda _section result" v-if="username != '' || host != ''" :class="{ hit: users.length > 0 }"> <div class="users" v-if="users.length > 0"> <div class="user" v-for="user in users" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()"> - <MkAvatar :user="user" class="avatar"/> + <MkAvatar :user="user" class="avatar" :show-indicator="true"/> <div class="body"> <MkUserName :user="user" class="name"/> <MkAcct :user="user" class="acct"/> @@ -31,7 +31,7 @@ <div class="tbhwbxda _section recent" v-if="username == '' && host == ''"> <div class="users"> <div class="user" v-for="user in recentUsers" :key="user.id" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()"> - <MkAvatar :user="user" class="avatar"/> + <MkAvatar :user="user" class="avatar" :show-indicator="true"/> <div class="body"> <MkUserName :user="user" class="name"/> <MkAcct :user="user" class="acct"/> diff --git a/src/client/components/users-dialog.vue b/src/client/components/users-dialog.vue index 381aa60911..ebf867f702 100644 --- a/src/client/components/users-dialog.vue +++ b/src/client/components/users-dialog.vue @@ -7,7 +7,7 @@ <div class="users"> <MkA v-for="item in items" class="user" :key="item.id" :to="userPage(extract ? extract(item) : item)"> - <MkAvatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true"/> + <MkAvatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true" :show-indicator="true"/> <div class="body"> <MkUserName :user="extract ? extract(item) : item" class="name"/> <MkAcct :user="extract ? extract(item) : item" class="acct"/> |