summaryrefslogtreecommitdiff
path: root/src/client/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-04-18 00:07:33 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-04-18 00:07:33 +0900
commit938fcb3e5ecc6862e40c9cb85b8010af63c69181 (patch)
tree512fa77d5b4dca9efd38af00a805c187876029fc /src/client/components
parentMerge branch 'develop' (diff)
parent12.77.0 (diff)
downloadmisskey-938fcb3e5ecc6862e40c9cb85b8010af63c69181.tar.gz
misskey-938fcb3e5ecc6862e40c9cb85b8010af63c69181.tar.bz2
misskey-938fcb3e5ecc6862e40c9cb85b8010af63c69181.zip
Merge branch 'develop'
Diffstat (limited to 'src/client/components')
-rw-r--r--src/client/components/avatars.vue2
-rw-r--r--src/client/components/date-separated-list.vue31
-rw-r--r--src/client/components/drive-file-thumbnail.vue2
-rw-r--r--src/client/components/form/base.vue13
-rw-r--r--src/client/components/form/group.vue62
-rw-r--r--src/client/components/form/key-value-view.vue8
-rw-r--r--src/client/components/form/object-view.vue102
-rw-r--r--src/client/components/form/suspense.vue92
-rw-r--r--src/client/components/global/avatar.vue21
-rw-r--r--src/client/components/global/loading.vue56
-rw-r--r--src/client/components/img-with-blurhash.vue2
-rw-r--r--src/client/components/media-image.vue2
-rw-r--r--src/client/components/note-detailed.vue2
-rw-r--r--src/client/components/notes.vue61
-rw-r--r--src/client/components/notifications.vue52
-rw-r--r--src/client/components/timeline.vue2
-rw-r--r--src/client/components/ui/container.vue71
-rw-r--r--src/client/components/ui/tooltip.vue10
-rw-r--r--src/client/components/user-info.vue2
-rw-r--r--src/client/components/user-online-indicator.vue50
-rw-r--r--src/client/components/user-preview.vue2
-rw-r--r--src/client/components/user-select-dialog.vue4
-rw-r--r--src/client/components/users-dialog.vue2
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"/>