summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/components/note-detailed.vue8
-rw-r--r--src/client/components/note.vue8
-rw-r--r--src/client/components/notes.vue11
-rw-r--r--src/client/components/post-form.vue8
-rw-r--r--src/client/components/ui/modal.vue1
-rw-r--r--src/client/init.ts3
-rw-r--r--src/client/pages/messaging/messaging-room.form.vue10
-rw-r--r--src/client/pages/messaging/messaging-room.vue32
-rw-r--r--src/client/pages/settings/email-address.vue2
-rw-r--r--src/client/pages/settings/general.vue2
-rw-r--r--src/client/scripts/paging.ts2
-rw-r--r--src/client/store.ts4
-rw-r--r--src/client/ui/chat/date-separated-list.vue2
-rw-r--r--src/client/ui/chat/header-clock.vue24
-rw-r--r--src/client/ui/chat/index.vue54
-rw-r--r--src/client/ui/chat/note.vue11
-rw-r--r--src/client/ui/chat/notes.vue13
-rw-r--r--src/client/ui/chat/post-form.vue8
-rw-r--r--src/client/ui/chat/store.ts4
-rw-r--r--src/client/ui/chat/timeline.vue104
-rw-r--r--src/client/ui/chat/widgets.vue3
-rw-r--r--src/client/widgets/define.ts10
-rw-r--r--src/client/widgets/job-queue.vue32
23 files changed, 272 insertions, 84 deletions
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index 1108bd2c27..434dd56ba3 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -756,7 +756,13 @@ export default defineComponent({
};
if (isLink(e.target)) return;
if (window.getSelection().toString() !== '') return;
- os.contextMenu(this.getMenu(), e).then(this.focus);
+
+ if (this.$store.state.useReactionPickerForContextMenu) {
+ e.preventDefault();
+ this.react();
+ } else {
+ os.contextMenu(this.getMenu(), e).then(this.focus);
+ }
},
menu(viaKeyboard = false) {
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index d532289857..24c374869d 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -731,7 +731,13 @@ export default defineComponent({
};
if (isLink(e.target)) return;
if (window.getSelection().toString() !== '') return;
- os.contextMenu(this.getMenu(), e).then(this.focus);
+
+ if (this.$store.state.useReactionPickerForContextMenu) {
+ e.preventDefault();
+ this.react();
+ } else {
+ os.contextMenu(this.getMenu(), e).then(this.focus);
+ }
},
menu(viaKeyboard = false) {
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
index bd6d5bb4f5..332f00e5db 100644
--- a/src/client/components/notes.vue
+++ b/src/client/components/notes.vue
@@ -8,10 +8,10 @@
<MkError v-if="error" @retry="init()"/>
<div v-show="more && reversed" style="margin-bottom: var(--margin);">
- <button class="_buttonPrimary" @click="fetchMoreFeature" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+ <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>
- </button>
+ </MkButton>
</div>
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
@@ -19,10 +19,10 @@
</XList>
<div v-show="more && !reversed" style="margin-top: var(--margin);">
- <button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+ <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>
- </button>
+ </MkButton>
</div>
</div>
</template>
@@ -32,10 +32,11 @@ import { defineComponent } from 'vue';
import paging from '@/scripts/paging';
import XNote from './note.vue';
import XList from './date-separated-list.vue';
+import MkButton from '@/components/ui/button.vue';
export default defineComponent({
components: {
- XNote, XList,
+ XNote, XList, MkButton,
},
mixins: [
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index fa9aeff8af..7849095ba8 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -70,6 +70,7 @@ import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
import { notePostInterruptors, postFormActions } from '@/store';
import { isMobile } from '@/scripts/is-mobile';
+import { throttle } from 'throttle-debounce';
export default defineComponent({
components: {
@@ -144,6 +145,11 @@ export default defineComponent({
quoteId: null,
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
imeText: '',
+ typing: throttle(3000, () => {
+ if (this.channel) {
+ os.stream.send('typingOnChannel', { channel: this.channel.id });
+ }
+ }),
postFormActions,
faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug
};
@@ -434,10 +440,12 @@ export default defineComponent({
onKeydown(e: KeyboardEvent) {
if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
if (e.which === 27) this.$emit('esc');
+ this.typing();
},
onCompositionUpdate(e: CompositionEvent) {
this.imeText = e.data;
+ this.typing();
},
onCompositionEnd(e: CompositionEvent) {
diff --git a/src/client/components/ui/modal.vue b/src/client/components/ui/modal.vue
index 69a83e002c..405fa4aaa5 100644
--- a/src/client/components/ui/modal.vue
+++ b/src/client/components/ui/modal.vue
@@ -70,6 +70,7 @@ export default defineComponent({
// TODO: ResizeObserver無くしたい
new ResizeObserver((entries, observer) => {
const rect = this.src.getBoundingClientRect();
+
const width = popover.offsetWidth;
const height = popover.offsetHeight;
diff --git a/src/client/init.ts b/src/client/init.ts
index c60b25359b..ce12849770 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -63,6 +63,9 @@ import { reloadChannel } from '@/scripts/unison-reload';
console.info(`Misskey v${version}`);
+// boot.jsのやつを解除
+window.onerror = null;
+
if (_DEV_) {
console.warn('Development mode!!!');
diff --git a/src/client/pages/messaging/messaging-room.form.vue b/src/client/pages/messaging/messaging-room.form.vue
index e561cb3db5..258300dc52 100644
--- a/src/client/pages/messaging/messaging-room.form.vue
+++ b/src/client/pages/messaging/messaging-room.form.vue
@@ -7,6 +7,7 @@
v-model="text"
ref="text"
@keypress="onKeypress"
+ @compositionupdate="onCompositionUpdate"
@paste="onPaste"
:placeholder="$ts.inputMessageHere"
></textarea>
@@ -29,6 +30,7 @@ import { formatTimeString } from '../../../misc/format-time-string';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
import { Autocomplete } from '@/scripts/autocomplete';
+import { throttle } from 'throttle-debounce';
export default defineComponent({
props: {
@@ -46,6 +48,9 @@ export default defineComponent({
text: null,
file: null,
sending: false,
+ typing: throttle(3000, () => {
+ os.stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id });
+ }),
faPaperPlane, faPhotoVideo, faLaughSquint
};
},
@@ -147,11 +152,16 @@ export default defineComponent({
},
onKeypress(e) {
+ this.typing();
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) {
this.send();
}
},
+ onCompositionUpdate() {
+ this.typing();
+ },
+
chooseFile(e) {
selectFile(e.currentTarget || e.target, this.$ts.selectFile, false).then(file => {
this.file = file;
diff --git a/src/client/pages/messaging/messaging-room.vue b/src/client/pages/messaging/messaging-room.vue
index 7fdd0a201b..3921a081d1 100644
--- a/src/client/pages/messaging/messaging-room.vue
+++ b/src/client/pages/messaging/messaging-room.vue
@@ -16,6 +16,14 @@
</XList>
</div>
<footer>
+ <div class="typers" v-if="typers.length > 0">
+ <I18n :src="$ts.typingUsers" text-tag="span" class="users">
+ <template #users>
+ <b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
+ </template>
+ </I18n>
+ <MkEllipsis/>
+ </div>
<transition name="fade">
<div class="new-message" v-show="showIndicator">
<button class="_buttonPrimary" @click="onIndicatorClick"><i><Fa :icon="faArrowCircleDown"/></i>{{ $ts.newMessageExists }}</button>
@@ -86,6 +94,7 @@ const Component = defineComponent({
connection: null,
showIndicator: false,
timer: null,
+ typers: [],
ilObserver: new IntersectionObserver(
(entries) => entries.some((entry) => entry.isIntersecting)
&& !this.fetching
@@ -142,6 +151,9 @@ const Component = defineComponent({
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
this.connection.on('deleted', this.onDeleted);
+ this.connection.on('typers', typers => {
+ this.typers = typers.filter(u => u.id !== this.$i.id);
+ });
document.addEventListener('visibilitychange', this.onVisibilitychange);
@@ -397,6 +409,7 @@ export default Component;
> footer {
width: 100%;
+ position: relative;
> .new-message {
position: absolute;
@@ -422,6 +435,25 @@ export default Component;
}
}
}
+
+ > .typers {
+ position: absolute;
+ bottom: 100%;
+ padding: 0 8px 0 8px;
+ font-size: 0.9em;
+ color: var(--fgTransparentWeak);
+
+ > .users {
+ > .user + .user:before {
+ content: ", ";
+ font-weight: normal;
+ }
+
+ > .user:last-of-type:after {
+ content: " ";
+ }
+ }
+ }
}
}
diff --git a/src/client/pages/settings/email-address.vue b/src/client/pages/settings/email-address.vue
index 4aed9bf4c7..8ca0f119c5 100644
--- a/src/client/pages/settings/email-address.vue
+++ b/src/client/pages/settings/email-address.vue
@@ -60,7 +60,7 @@ export default defineComponent({
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
- os.api('i/update-email', {
+ os.apiWithDialog('i/update-email', {
password: password,
email: this.emailAddress,
});
diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue
index 0e741d474c..90ff3e2c20 100644
--- a/src/client/pages/settings/general.vue
+++ b/src/client/pages/settings/general.vue
@@ -19,6 +19,7 @@
<template #label>{{ $ts.behavior }}</template>
<FormSwitch v-model:value="imageNewTab">{{ $ts.openImageInNewTab }}</FormSwitch>
<FormSwitch v-model:value="enableInfiniteScroll">{{ $ts.enableInfiniteScroll }}</FormSwitch>
+ <FormSwitch v-model:value="useReactionPickerForContextMenu">{{ $ts.useReactionPickerForContextMenu }}</FormSwitch>
<FormSwitch v-model:value="disablePagesScript">{{ $ts.disablePagesScript }}</FormSwitch>
</FormGroup>
@@ -144,6 +145,7 @@ export default defineComponent({
chatOpenBehavior: ColdDeviceStorage.makeGetterSetter('chatOpenBehavior'),
instanceTicker: defaultStore.makeGetterSetter('instanceTicker'),
enableInfiniteScroll: defaultStore.makeGetterSetter('enableInfiniteScroll'),
+ useReactionPickerForContextMenu: defaultStore.makeGetterSetter('useReactionPickerForContextMenu'),
},
watch: {
diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts
index a8f122412c..6e3da94124 100644
--- a/src/client/scripts/paging.ts
+++ b/src/client/scripts/paging.ts
@@ -192,8 +192,6 @@ export default (opts) => ({
this.items = this.items.slice(-opts.displayLimit);
this.more = true;
}
- } else {
-
}
this.items.push(item);
// TODO
diff --git a/src/client/store.ts b/src/client/store.ts
index bf042d8ab4..528e563fdd 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -144,6 +144,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: true
},
+ useReactionPickerForContextMenu: {
+ where: 'device',
+ default: true
+ },
showGapBetweenNotesInTimeline: {
where: 'device',
default: true
diff --git a/src/client/ui/chat/date-separated-list.vue b/src/client/ui/chat/date-separated-list.vue
index b209330656..65deb9e1c2 100644
--- a/src/client/ui/chat/date-separated-list.vue
+++ b/src/client/ui/chat/date-separated-list.vue
@@ -32,7 +32,7 @@ export default defineComponent({
});
}
- return h(TransitionGroup, {
+ return h(this.reversed ? 'div' : TransitionGroup, {
class: 'hmjzthxl',
name: this.reversed ? 'list-reversed' : 'list',
tag: 'div',
diff --git a/src/client/ui/chat/header-clock.vue b/src/client/ui/chat/header-clock.vue
index 65573d460b..3488289c21 100644
--- a/src/client/ui/chat/header-clock.vue
+++ b/src/client/ui/chat/header-clock.vue
@@ -1,12 +1,15 @@
<template>
-<div class="_monospace">
- <span>
+<div class="acemodlh _monospace">
+ <div>
+ <span v-text="y"></span>/<span v-text="m"></span>/<span v-text="d"></span>
+ </div>
+ <div>
<span v-text="hh"></span>
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
<span v-text="mm"></span>
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
<span v-text="ss"></span>
- </span>
+ </div>
</div>
</template>
@@ -18,6 +21,9 @@ export default defineComponent({
data() {
return {
clock: null,
+ y: null,
+ m: null,
+ d: null,
hh: null,
mm: null,
ss: null,
@@ -34,6 +40,9 @@ export default defineComponent({
methods: {
tick() {
const now = new Date();
+ this.y = now.getFullYear().toString();
+ this.m = (now.getMonth() + 1).toString().padStart(2, '0');
+ this.d = now.getDate().toString().padStart(2, '0');
this.hh = now.getHours().toString().padStart(2, '0');
this.mm = now.getMinutes().toString().padStart(2, '0');
this.ss = now.getSeconds().toString().padStart(2, '0');
@@ -42,3 +51,12 @@ export default defineComponent({
}
});
</script>
+
+<style lang="scss" scoped>
+.acemodlh {
+ opacity: 0.7;
+ font-size: 0.85em;
+ line-height: 1em;
+ text-align: center;
+}
+</style>
diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue
index 44f47447a7..26c81a1aa9 100644
--- a/src/client/ui/chat/index.vue
+++ b/src/client/ui/chat/index.vue
@@ -99,6 +99,9 @@
<div class="right">
<div class="instance">{{ instanceName }}</div>
<XHeaderClock class="clock"/>
+ <button class="_button button timetravel" @click="timetravel" v-tooltip="$ts.jumpToSpecifiedDate">
+ <Fa :icon="faCalendarAlt"/>
+ </button>
<button class="_button button search" v-if="tl.startsWith('channel:') && currentChannel" @click="inChannelSearch" v-tooltip="$ts.inChannelSearch">
<Fa :icon="faSearch"/>
</button>
@@ -114,14 +117,9 @@
</button>
</div>
</header>
- <div class="body">
- <XTimeline v-if="tl.startsWith('channel:')" src="channel" :key="tl" :channel="tl.replace('channel:', '')"/>
- <XTimeline v-else :src="tl" :key="tl"/>
- </div>
- <footer class="footer">
- <XPostForm v-if="tl.startsWith('channel:')" :key="tl" :channel="tl.replace('channel:', '')"/>
- <XPostForm v-else/>
- </footer>
+
+ <XTimeline class="body" ref="tl" v-if="tl.startsWith('channel:')" src="channel" :key="tl" :channel="tl.replace('channel:', '')"/>
+ <XTimeline class="body" ref="tl" v-else :src="tl" :key="tl"/>
</main>
<XSide class="side" ref="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
@@ -136,20 +134,20 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { faLayerGroup, faBars, faHome, faCircle, faWindowMaximize, faColumns, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog, faSearch, faPlus, faStar, faAt, faLink, faEllipsisH, faGlobe } from '@fortawesome/free-solid-svg-icons';
-import { faBell, faStar as farStar, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons';
+import { faBell, faStar as farStar, faEnvelope, faComments, faCalendarAlt } from '@fortawesome/free-regular-svg-icons';
import { instanceName, url } from '@/config';
import XSidebar from '@/components/sidebar.vue';
import XWidgets from './widgets.vue';
import XCommon from '../_common_/common.vue';
import XSide from './side.vue';
import XTimeline from './timeline.vue';
-import XPostForm from './post-form.vue';
import XHeaderClock from './header-clock.vue';
import * as os from '@/os';
import { router } from '@/router';
import { sidebarDef } from '@/sidebar';
import { search } from '@/scripts/search';
import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { store } from './store';
export default defineComponent({
components: {
@@ -158,7 +156,6 @@ export default defineComponent({
XWidgets,
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
XTimeline,
- XPostForm,
XHeaderClock,
},
@@ -189,7 +186,7 @@ export default defineComponent({
data() {
return {
- tl: 'home',
+ tl: store.state.tl,
lists: null,
antennas: null,
followedChannels: null,
@@ -198,7 +195,7 @@ export default defineComponent({
menuDef: sidebarDef,
sideViewOpening: false,
instanceName,
- faLayerGroup, faBars, faBell, faHome, faCircle, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog, faSearch, faPlus, faStar, farStar, faAt, faLink, faEllipsisH, faGlobe, faComments, faEnvelope,
+ faLayerGroup, faBars, faBell, faHome, faCircle, faPencilAlt, faShareAlt, faSatelliteDish, faListUl, faSatellite, faCog, faSearch, faPlus, faStar, farStar, faAt, faLink, faEllipsisH, faGlobe, faComments, faEnvelope, faCalendarAlt,
};
},
@@ -222,11 +219,12 @@ export default defineComponent({
this.antennas = antennas;
});
- os.api('channels/followed').then(channels => {
+ os.api('channels/followed', { limit: 20 }).then(channels => {
this.followedChannels = channels;
});
- os.api('channels/featured').then(channels => {
+ // TODO: pagination
+ os.api('channels/featured', { limit: 20 }).then(channels => {
this.featuredChannels = channels;
});
@@ -236,6 +234,7 @@ export default defineComponent({
this.currentChannel = channel;
});
}
+ store.set('tl', this.tl);
}, { immediate: true });
},
@@ -248,6 +247,18 @@ export default defineComponent({
os.post();
},
+ async timetravel() {
+ const { canceled, result: date } = await os.dialog({
+ title: this.$ts.date,
+ input: {
+ type: 'date'
+ }
+ });
+ if (canceled) return;
+
+ this.$refs.tl.timetravel(new Date(date));
+ },
+
search() {
search();
},
@@ -470,6 +481,9 @@ export default defineComponent({
display: block;
padding: 6px 8px;
border-radius: 4px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
&:hover {
text-decoration: none;
@@ -581,16 +595,6 @@ export default defineComponent({
}
}
}
-
- > .footer {
- padding: 0 16px 16px 16px;
- }
-
- > .body {
- flex: 1;
- min-width: 0;
- overflow: auto;
- }
}
> .side {
diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue
index f4c9f063dc..9312b99d27 100644
--- a/src/client/ui/chat/note.vue
+++ b/src/client/ui/chat/note.vue
@@ -741,7 +741,13 @@ export default defineComponent({
};
if (isLink(e.target)) return;
if (window.getSelection().toString() !== '') return;
- os.contextMenu(this.getMenu(), e).then(this.focus);
+
+ if (this.$store.state.useReactionPickerForContextMenu) {
+ e.preventDefault();
+ this.react();
+ } else {
+ os.contextMenu(this.getMenu(), e).then(this.focus);
+ }
},
menu(viaKeyboard = false) {
@@ -1004,7 +1010,7 @@ export default defineComponent({
flex-shrink: 0;
display: block;
position: sticky;
- top: 12px;
+ top: 0;
margin: 0 14px 0 0;
width: 46px;
height: 46px;
@@ -1085,6 +1091,7 @@ export default defineComponent({
> .poll {
font-size: 80%;
+ max-width: 500px;
}
> .renote {
diff --git a/src/client/ui/chat/notes.vue b/src/client/ui/chat/notes.vue
index 1fa2870cee..3a169cc20a 100644
--- a/src/client/ui/chat/notes.vue
+++ b/src/client/ui/chat/notes.vue
@@ -1,5 +1,5 @@
<template>
-<div class="" :ref="mounted">
+<div class="">
<div class="_fullinfo" v-if="empty">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $ts.noNotes }}</div>
@@ -8,10 +8,10 @@
<MkError v-if="error" @retry="init()"/>
<div v-show="more && reversed" style="margin-bottom: var(--margin);">
- <button class="_buttonPrimary" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+ <MkButton style="margin: 0 auto;" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $ts.loadMore }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
- </button>
+ </MkButton>
</div>
<XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
@@ -19,10 +19,10 @@
</XList>
<div v-show="more && !reversed" style="margin-top: var(--margin);">
- <button class="_buttonPrimary" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+ <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>
- </button>
+ </MkButton>
</div>
</div>
</template>
@@ -32,10 +32,11 @@ import { defineComponent } from 'vue';
import paging from '@/scripts/paging';
import XNote from './note.vue';
import XList from './date-separated-list.vue';
+import MkButton from '@/components/ui/button.vue';
export default defineComponent({
components: {
- XNote, XList,
+ XNote, XList, MkButton,
},
mixins: [
diff --git a/src/client/ui/chat/post-form.vue b/src/client/ui/chat/post-form.vue
index 38fe48cc62..b0a31b097d 100644
--- a/src/client/ui/chat/post-form.vue
+++ b/src/client/ui/chat/post-form.vue
@@ -65,6 +65,7 @@ import * as os from '@/os';
import { selectFile } from '@/scripts/select-file';
import { notePostInterruptors, postFormActions } from '@/store';
import { isMobile } from '@/scripts/is-mobile';
+import { throttle } from 'throttle-debounce';
export default defineComponent({
components: {
@@ -131,6 +132,11 @@ export default defineComponent({
quoteId: null,
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
imeText: '',
+ typing: throttle(3000, () => {
+ if (this.channel) {
+ os.stream.send('typingOnChannel', { channel: this.channel });
+ }
+ }),
postFormActions,
faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faAt, faBiohazard, faPlug
};
@@ -421,10 +427,12 @@ export default defineComponent({
onKeydown(e: KeyboardEvent) {
if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
if (e.which === 27) this.$emit('esc');
+ this.typing();
},
onCompositionUpdate(e: CompositionEvent) {
this.imeText = e.data;
+ this.typing();
},
onCompositionEnd(e: CompositionEvent) {
diff --git a/src/client/ui/chat/store.ts b/src/client/ui/chat/store.ts
index a869debd61..389d56afb6 100644
--- a/src/client/ui/chat/store.ts
+++ b/src/client/ui/chat/store.ts
@@ -10,4 +10,8 @@ export const store = markRaw(new Storage('chatUi', {
data: Record<string, any>;
}[]
},
+ tl: {
+ where: 'deviceAccount',
+ default: 'home'
+ },
}));
diff --git a/src/client/ui/chat/timeline.vue b/src/client/ui/chat/timeline.vue
index f96a48a776..232e749c1c 100644
--- a/src/client/ui/chat/timeline.vue
+++ b/src/client/ui/chat/timeline.vue
@@ -1,8 +1,25 @@
<template>
-<div class="dbiokgaf">
+<div class="dbiokgaf info" v-if="date">
+ <MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
+</div>
+<div class="dbiokgaf top" v-if="['home', 'local', 'social', 'global'].includes(src)">
+ <XPostForm/>
+</div>
+<div class="dbiokgaf tl" ref="body">
<div class="new" v-if="queue > 0" :style="{ width: width + 'px', [pagination.reversed ? 'bottom' : 'top']: pagination.reversed ? bottom + 'px' : top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated" v-follow="pagination.reversed"/>
</div>
+<div class="dbiokgaf bottom" v-if="src === 'channel'">
+ <div class="typers" v-if="typers.length > 0">
+ <I18n :src="$ts.typingUsers" text-tag="span" class="users">
+ <template #users>
+ <b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
+ </template>
+ </I18n>
+ <MkEllipsis/>
+ </div>
+ <XPostForm :channel="channel"/>
+</div>
</template>
<script lang="ts">
@@ -12,10 +29,14 @@ import * as os from '@/os';
import * as sound from '@/scripts/sound';
import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
import follow from '@/directives/follow-append';
+import XPostForm from './post-form.vue';
+import MkInfo from '@/components/ui/info.vue';
export default defineComponent({
components: {
- XNotes
+ XNotes,
+ XPostForm,
+ MkInfo,
},
directives: {
@@ -45,11 +66,6 @@ export default defineComponent({
type: String,
required: false
},
- sound: {
- type: Boolean,
- required: false,
- default: false,
- }
},
emits: ['note', 'queue', 'before', 'after'],
@@ -69,6 +85,8 @@ export default defineComponent({
width: 0,
top: 0,
bottom: 0,
+ typers: [],
+ date: null
};
},
@@ -78,9 +96,7 @@ export default defineComponent({
this.$emit('note');
- if (this.sound) {
- sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
- }
+ sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
};
const onUserAdded = () => {
@@ -166,6 +182,9 @@ export default defineComponent({
channelId: this.channel
});
this.connection.on('note', prepend);
+ this.connection.on('typers', typers => {
+ this.typers = this.$i ? typers.filter(u => u.id !== this.$i.id) : typers;
+ });
}
this.pagination = {
@@ -173,7 +192,7 @@ export default defineComponent({
reversed,
limit: 10,
params: init => ({
- untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
+ untilDate: this.date?.getTime(),
...this.baseQuery, ...this.query
})
};
@@ -190,34 +209,73 @@ export default defineComponent({
methods: {
focus() {
- this.$refs.tl.focus();
+ this.$refs.body.focus();
},
goTop() {
- const container = getScrollContainer(this.$el);
+ const container = getScrollContainer(this.$refs.body);
container.scrollTop = 0;
},
queueUpdated(q) {
- if (this.$el.offsetWidth !== 0) {
- const rect = this.$el.getBoundingClientRect();
- const scrollTop = getScrollPosition(this.$el);
- this.width = this.$el.offsetWidth;
- this.top = rect.top + scrollTop;
- this.bottom = this.$el.offsetHeight;
+ if (this.$refs.body.offsetWidth !== 0) {
+ const rect = this.$refs.body.getBoundingClientRect();
+ this.width = this.$refs.body.offsetWidth;
+ this.top = rect.top;
+ this.bottom = this.$refs.body.offsetHeight;
}
this.queue = q;
},
+
+ timetravel(date?: Date) {
+ this.date = date;
+ this.$refs.tl.reload();
+ }
}
});
</script>
<style lang="scss" scoped>
-.dbiokgaf {
- padding: 16px 0;
+.dbiokgaf.info{
+ padding: 16px 16px 0 16px;
+}
+
+.dbiokgaf.top {
+ padding: 16px 16px 0 16px;
+}
+
+.dbiokgaf.bottom {
+ padding: 0 16px 16px 16px;
+ position: relative;
+
+ > .typers {
+ position: absolute;
+ bottom: 100%;
+ padding: 0 8px 0 8px;
+ font-size: 0.9em;
+ background: var(--panel);
+ border-radius: 0 8px 0 0;
+ color: var(--fgTransparentWeak);
- // TODO: これはノート追加アニメーションによるスクロール発生を抑えるために必要だが、position stickyが効かなくなるので、両者を両立させる良い方法を考える
- overflow: hidden;
+ > .users {
+ > .user + .user:before {
+ content: ", ";
+ font-weight: normal;
+ }
+
+ > .user:last-of-type:after {
+ content: " ";
+ }
+ }
+ }
+}
+
+.dbiokgaf.tl {
+ position: relative;
+ padding: 16px 0;
+ flex: 1;
+ min-width: 0;
+ overflow: auto;
> .new {
position: fixed;
diff --git a/src/client/ui/chat/widgets.vue b/src/client/ui/chat/widgets.vue
index 6becaa22e3..6b12f9dac9 100644
--- a/src/client/ui/chat/widgets.vue
+++ b/src/client/ui/chat/widgets.vue
@@ -10,7 +10,7 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import XWidgets from '@/components/widgets.vue';
-import { store } from './store.ts';
+import { store } from './store';
export default defineComponent({
components: {
@@ -34,6 +34,7 @@ export default defineComponent({
},
updateWidget({ id, data }) {
+ // TODO: throttleしたい
store.set('widgets', store.state.widgets.map(w => w.id === id ? {
...w,
data: data
diff --git a/src/client/widgets/define.ts b/src/client/widgets/define.ts
index b5498204b3..08a346d97c 100644
--- a/src/client/widgets/define.ts
+++ b/src/client/widgets/define.ts
@@ -1,4 +1,5 @@
import { defineComponent } from 'vue';
+import { throttle } from 'throttle-debounce';
import { Form } from '@/scripts/form';
import * as os from '@/os';
@@ -21,7 +22,10 @@ export default function <T extends Form>(data: {
data() {
return {
- props: this.widget ? JSON.parse(JSON.stringify(this.widget.data)) : {}
+ props: this.widget ? JSON.parse(JSON.stringify(this.widget.data)) : {},
+ save: throttle(3000, () => {
+ this.$emit('updateProps', this.props);
+ }),
};
},
@@ -66,10 +70,6 @@ export default function <T extends Form>(data: {
this.save();
},
-
- save() {
- this.$emit('updateProps', this.props);
- }
}
});
}
diff --git a/src/client/widgets/job-queue.vue b/src/client/widgets/job-queue.vue
index 11bb20979b..b7bfb6de27 100644
--- a/src/client/widgets/job-queue.vue
+++ b/src/client/widgets/job-queue.vue
@@ -5,19 +5,19 @@
<div class="values">
<div>
<div>Process</div>
- <div>{{ number(inbox.activeSincePrevTick) }}</div>
+ <div :class="{ inc: inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(inbox.activeSincePrevTick) }}</div>
</div>
<div>
<div>Active</div>
- <div>{{ number(inbox.active) }}</div>
+ <div :class="{ inc: inbox.active > prev.inbox.active, dec: inbox.active < prev.inbox.active }">{{ number(inbox.active) }}</div>
</div>
<div>
<div>Delayed</div>
- <div>{{ number(inbox.delayed) }}</div>
+ <div :class="{ inc: inbox.delayed > prev.inbox.delayed, dec: inbox.delayed < prev.inbox.delayed }">{{ number(inbox.delayed) }}</div>
</div>
<div>
<div>Waiting</div>
- <div>{{ number(inbox.waiting) }}</div>
+ <div :class="{ inc: inbox.waiting > prev.inbox.waiting, dec: inbox.waiting < prev.inbox.waiting }">{{ number(inbox.waiting) }}</div>
</div>
</div>
</div>
@@ -26,19 +26,19 @@
<div class="values">
<div>
<div>Process</div>
- <div>{{ number(deliver.activeSincePrevTick) }}</div>
+ <div :class="{ inc: deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(deliver.activeSincePrevTick) }}</div>
</div>
<div>
<div>Active</div>
- <div>{{ number(deliver.active) }}</div>
+ <div :class="{ inc: deliver.active > prev.deliver.active, dec: deliver.active < prev.deliver.active }">{{ number(deliver.active) }}</div>
</div>
<div>
<div>Delayed</div>
- <div>{{ number(deliver.delayed) }}</div>
+ <div :class="{ inc: deliver.delayed > prev.deliver.delayed, dec: deliver.delayed < prev.deliver.delayed }">{{ number(deliver.delayed) }}</div>
</div>
<div>
<div>Waiting</div>
- <div>{{ number(deliver.waiting) }}</div>
+ <div :class="{ inc: deliver.waiting > prev.deliver.waiting, dec: deliver.waiting < prev.deliver.waiting }">{{ number(deliver.waiting) }}</div>
</div>
</div>
</div>
@@ -79,10 +79,15 @@ export default defineComponent({
waiting: 0,
delayed: 0,
},
+ prev: {},
faExclamationTriangle,
};
},
created() {
+ for (const domain of ['inbox', 'deliver']) {
+ this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
+ }
+
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
@@ -99,6 +104,7 @@ export default defineComponent({
methods: {
onStats(stats) {
for (const domain of ['inbox', 'deliver']) {
+ this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
this[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
this[domain].active = stats[domain].active;
this[domain].waiting = stats[domain].waiting;
@@ -152,6 +158,16 @@ export default defineComponent({
> div:first-child {
opacity: 0.7;
}
+
+ > div:last-child {
+ &.inc {
+ color: var(--warn);
+ }
+
+ &.dec {
+ color: var(--success);
+ }
+ }
}
}
}