Merge branch 'develop' into sw-notification-action
This commit is contained in:
@@ -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',
|
||||
|
@@ -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>
|
||||
|
@@ -99,7 +99,13 @@
|
||||
<div class="right">
|
||||
<div class="instance">{{ instanceName }}</div>
|
||||
<XHeaderClock class="clock"/>
|
||||
<button class="_button button search" @click="search" v-tooltip="$ts.search">
|
||||
<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>
|
||||
<button class="_button button search" v-else @click="search" v-tooltip="$ts.search">
|
||||
<Fa :icon="faSearch"/>
|
||||
</button>
|
||||
<button class="_button button follow" v-if="tl.startsWith('channel:') && currentChannel" :class="{ followed: currentChannel.isFollowing }" @click="toggleChannelFollow" v-tooltip="currentChannel.isFollowing ? $ts.unfollow : $ts.follow">
|
||||
@@ -111,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"/>
|
||||
@@ -133,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: {
|
||||
@@ -155,7 +156,6 @@ export default defineComponent({
|
||||
XWidgets,
|
||||
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
|
||||
XTimeline,
|
||||
XPostForm,
|
||||
XHeaderClock,
|
||||
},
|
||||
|
||||
@@ -186,7 +186,7 @@ export default defineComponent({
|
||||
|
||||
data() {
|
||||
return {
|
||||
tl: 'home',
|
||||
tl: store.state.tl,
|
||||
lists: null,
|
||||
antennas: null,
|
||||
followedChannels: null,
|
||||
@@ -195,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,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -219,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;
|
||||
});
|
||||
|
||||
@@ -233,6 +234,7 @@ export default defineComponent({
|
||||
this.currentChannel = channel;
|
||||
});
|
||||
}
|
||||
store.set('tl', this.tl);
|
||||
}, { immediate: true });
|
||||
},
|
||||
|
||||
@@ -245,10 +247,31 @@ 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();
|
||||
},
|
||||
|
||||
async inChannelSearch() {
|
||||
const { canceled, result: query } = await os.dialog({
|
||||
title: this.$ts.inChannelSearch,
|
||||
input: true
|
||||
});
|
||||
if (canceled || query == null || query === '') return;
|
||||
router.push(`/search?q=${encodeURIComponent(query)}&channel=${this.currentChannel.id}`);
|
||||
},
|
||||
|
||||
top() {
|
||||
window.scroll({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
@@ -458,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;
|
||||
@@ -569,16 +595,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .footer {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> .side {
|
||||
|
@@ -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 {
|
||||
|
@@ -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: [
|
||||
|
@@ -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) {
|
||||
|
@@ -10,4 +10,8 @@ export const store = markRaw(new Storage('chatUi', {
|
||||
data: Record<string, any>;
|
||||
}[]
|
||||
},
|
||||
tl: {
|
||||
where: 'deviceAccount',
|
||||
default: 'home'
|
||||
},
|
||||
}));
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
// TODO: これはノート追加アニメーションによるスクロール発生を抑えるために必要だが、position stickyが効かなくなるので、両者を両立させる良い方法を考える
|
||||
overflow: hidden;
|
||||
.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);
|
||||
|
||||
> .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;
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user