Migrate to Vue3 (#6587)

* Update reaction.vue

* fix  bug

* wip

* wip

* wjio

* wip

* Revert "wip"

This reverts commit e427f2160a.

* wip

* wip

* wip

* Update init.ts

* Update drive-window.vue

* wip

* wip

* Use PascalCase for components

* Use PascalCase for components

* update dep

* wip

* wip

* wip

* Update init.ts

* wip

* Update paging.ts

* Update test.vue

* watch deep

* wip

* lint

* wip

* wip

* wip

* wip

* wiop

* wip

* Update webpack.config.ts

* alllow null poll

* wip

* wip

* wip

* wiop

* UI redesign & refactor (#6714)

* wip

* wip

* wip

* wip

* wip

* Update drive.vue

* Update word-mute.vue

* wip

* wip

* wip

* clean up

* wip

* Update default.vue

* wip

* Update notes.vue

* Update mfm.ts

* Update index.home.vue

* Update post-form.vue

* Update post-form-attaches.vue

* wip

* Update post-form.vue

* Update sidebar.vue

* wip

* wip

* Update index.vue

* wip

* Update default.vue

* Update index.vue

* Update index.vue

* wip

* Update post-form-attaches.vue

* Update note.vue

* wip

* clean up

* Update notes.vue

* wip

* wip

* Update ja-JP.yml

* wip

* wip

* Update index.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update default.vue

* wip

* Update _dark.json5

* wip

* wip

* wip

* clean up

* wip

* wip

* Update index.vue

* Update test.vue

* wip

* wip

* fix

* wip

* wip

* wip

* wip

* clena yop

* wip

* wip

* Update store.ts

* Update messaging-room.vue

* Update default.widgets.vue

* fix

* wip

* wip

* Update modal.vue

* wip

* Update os.ts

* Update os.ts

* Update deck.vue

* Update init.ts

* wip

* Update ja-JP.yml

* v-sizeは単にwindowのresizeを監視するだけで良いかもしれない

* Update modal.vue

* wip

* Update tooltip.ts

* wip

* wip

* wip

* wip

* wip

* Update image-viewer.vue

* wip

* wip

* Update style.scss

* Update style.scss

* Update visitor.vue

* wip

* Update init.ts

* Update init.ts

* wip

* wip

* Update visitor.vue

* Update visitor.vue

* Update visitor.vue

* Update visitor.vue

* wip

* wip

* Update modal.vue

* Update header.vue

* Update menu.vue

* Update about.vue

* Update about-misskey.vue

* wip

* wip

* Update visitor.vue

* Update tooltip.ts

* wip

* Update drive.vue

* wip

* Update style.scss

* Update header.vue

* wip

* wip

* Update users.user.vue

* Update announcements.vue

* wip

* wip

* wip

* Update emojis.vue

* wip

* Update emojis.vue

* Update style.scss

* Update users.vue

* wip

* Update style.scss

* wip

* Update welcome.entrance.vue

* Update radio.vue

* Update size.ts

* Update emoji-edit-dialog.vue

* wip

* Update emojis.vue

* wip

* Update emojis.vue

* Update emojis.vue

* Update emojis.vue

* wip

* wip

* wip

* wip

* Update file-dialog.vue

* wip

* wip

* Update token-generate-window.vue

* Update notification-setting-window.vue

* wip

* wip

* Update _error_.vue

* Update ja-JP.yml

* wip

* wip

* Update store.ts

* Update emojis.vue

* Update emojis.vue

* Update emojis.vue

* Update announcements.vue

* Update store.ts

* wip

* Update page-editor.vue

* wip

* wip

* Update modal.vue

* wip

* Update select-file.ts

* Update timeline.vue

* Update emojis.vue

* Update os.ts

* wip

* Update user-select.vue

* Update mfm.ts

* Update get-file-info.ts

* Update drive.vue

* Update init.ts

* Update mfm.ts

* wip

* wip

* Update window.vue

* Update note.vue

* wip

* wip

* Update user-info.vue

* wip

* wip

* wip

* wip

* wip

* Update header.vue

* Update header.vue

* wip

* Update explore.vue

* wip

* wip

* wip

* Update webpack.config.ts

* wip

* wip

* wip

* wip

* wip

* wip

* Update autocomplete.ts

* wip

* wip

* wip

* Update toast.vue

* wip

* Update post-form-dialog.vue

* wip

* wip

* wip

* wip

* wip

* Update users.vue

* wip

* Update explore.vue

* wip

* wip

* wip

* Update package.json

* wip

* Update icon-dialog.vue

* wip

* wip

* Update user-preview.ts

* wip

* wip

* wip

* wip

* wip

* Update instance.vue

* Update user-name.vue

* Update federation.vue

* Update instance.vue

* wip

* wip

* Update tag.vue

* wip

* wip

* wip

* wip

* wip

* Update instance.vue

* wip

* Update os.ts

* Update os.ts

* wip

* wip

* wip

* Update router.ts

* wip

* Update init.ts

* Update note.vue

* Update messages.vue

* wip

* wip

* wip

* wip

* wip

* google

* wip

* wip

* wip

* wip

* Update theme-editor.vue

* wip

* wip

* Update room.vue

* Update channel-editor.vue

* wip

* Update window.vue

* Update window.vue

* wip

* Update window.vue

* Update window.vue

* wip

* Update menu.vue

* wip

* wip

* wip

* wip

* Update messaging-room.vue

* wip

* Update post-form.vue

* Update default.widgets.vue

* Update window.vue

* wip
This commit is contained in:
syuilo
2020-10-17 20:12:00 +09:00
committed by GitHub
parent a40f38b2b5
commit 7199e6f4e0
357 changed files with 15053 additions and 12496 deletions

View File

@@ -1,58 +1,66 @@
<template>
<div class="mk-messaging" v-size="{ max: [400] }">
<portal to="icon"><fa :icon="faComments"/></portal>
<portal to="title">{{ $t('messaging') }}</portal>
<div class="_section">
<div class="mk-messaging _content" v-size="{ max: [400] }">
<MkButton @click="start" primary class="start"><Fa :icon="faPlus"/> {{ $t('startMessaging') }}</MkButton>
<mk-button @click="start" primary class="start"><fa :icon="faPlus"/> {{ $t('startMessaging') }}</mk-button>
<div class="history" v-if="messages.length > 0">
<router-link v-for="(message, i) in messages"
class="message _panel"
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
:data-is-me="isMe(message)"
:data-is-read="message.groupId ? message.reads.includes($store.state.i.id) : message.isRead"
:data-index="i"
:key="message.id"
>
<div>
<mk-avatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>
<header v-if="message.groupId">
<span class="name">{{ message.group.name }}</span>
<mk-time :time="message.createdAt"/>
</header>
<header v-else>
<span class="name"><mk-user-name :user="isMe(message) ? message.recipient : message.user"/></span>
<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
<mk-time :time="message.createdAt"/>
</header>
<div class="body">
<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
<div class="history" v-if="messages.length > 0">
<router-link v-for="(message, i) in messages"
class="message _panel"
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($store.state.i.id) : message.isRead }"
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
:data-index="i"
:key="message.id"
@click.prevent="go(message)"
>
<div>
<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user"/>
<header v-if="message.groupId">
<span class="name">{{ message.group.name }}</span>
<MkTime :time="message.createdAt"/>
</header>
<header v-else>
<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
<MkTime :time="message.createdAt"/>
</header>
<div class="body">
<p class="text"><span class="me" v-if="isMe(message)">{{ $t('you') }}:</span>{{ message.text }}</p>
</div>
</div>
</div>
</router-link>
</router-link>
</div>
<div class="_fullinfo" v-if="!fetching && messages.length == 0">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $t('noHistory') }}</div>
</div>
<MkLoading v-if="fetching"/>
</div>
<div class="_fullinfo" v-if="!fetching && messages.length == 0">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $t('noHistory') }}</div>
</div>
<mk-loading v-if="fetching"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineAsyncComponent, defineComponent } from 'vue';
import { faUser, faUsers, faComments, faPlus } from '@fortawesome/free-solid-svg-icons';
import getAcct from '../../../misc/acct/render';
import MkButton from '../../components/ui/button.vue';
import MkUserSelect from '../../components/user-select.vue';
import MkButton from '@/components/ui/button.vue';
import { acct } from '../../filters/user';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
MkButton
},
inject: ['navHook'],
data() {
return {
INFO: {
header: [{
title: this.$t('messaging'),
icon: faComments
}]
},
fetching: true,
moreFetching: false,
messages: [],
@@ -62,13 +70,13 @@ export default Vue.extend({
},
mounted() {
this.connection = this.$root.stream.useSharedConnection('messagingIndex');
this.connection = os.stream.useSharedConnection('messagingIndex');
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
this.$root.api('messaging/history', { group: false }).then(userMessages => {
this.$root.api('messaging/history', { group: true }).then(groupMessages => {
os.api('messaging/history', { group: false }).then(userMessages => {
os.api('messaging/history', { group: true }).then(groupMessages => {
const messages = userMessages.concat(groupMessages);
messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
this.messages = messages;
@@ -77,11 +85,23 @@ export default Vue.extend({
});
},
beforeDestroy() {
beforeUnmount() {
this.connection.dispose();
},
methods: {
go(message) {
const url = message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(this.isMe(message) ? message.recipient : message.user)}`;
if (this.navHook) {
this.navHook(url, defineAsyncComponent(() => import('@/pages/messaging/messaging-room.vue')), {
userAcct: message.groupId ? null : getAcct(this.isMe(message) ? message.recipient : message.user),
groupId: message.groupId
});
} else {
this.$router.push(url);
}
},
getAcct,
isMe(message) {
@@ -115,39 +135,35 @@ export default Vue.extend({
},
start(ev) {
this.$root.menu({
items: [{
text: this.$t('messagingWithUser'),
icon: faUser,
action: () => { this.startUser() }
}, {
text: this.$t('messagingWithGroup'),
icon: faUsers,
action: () => { this.startGroup() }
}],
noCenter: true,
source: ev.currentTarget || ev.target,
});
os.modalMenu([{
text: this.$t('messagingWithUser'),
icon: faUser,
action: () => { this.startUser() }
}, {
text: this.$t('messagingWithGroup'),
icon: faUsers,
action: () => { this.startGroup() }
}], ev.currentTarget || ev.target);
},
async startUser() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
os.selectUser().then(user => {
this.$router.push(`/my/messaging/${getAcct(user)}`);
});
},
async startGroup() {
const groups1 = await this.$root.api('users/groups/owned');
const groups2 = await this.$root.api('users/groups/joined');
const groups1 = await os.api('users/groups/owned');
const groups2 = await os.api('users/groups/joined');
if (groups1.length === 0 && groups2.length === 0) {
this.$root.dialog({
os.dialog({
type: 'warning',
title: this.$t('youHaveNoGroups'),
text: this.$t('joinOrCreateGroup'),
});
return;
}
const { canceled, result: group } = await this.$root.dialog({
const { canceled, result: group } = await os.dialog({
type: null,
title: this.$t('group'),
select: {
@@ -159,7 +175,9 @@ export default Vue.extend({
});
if (canceled) return;
this.$router.push(`/my/messaging/group/${group.id}`);
}
},
acct
}
});
</script>
@@ -191,12 +209,12 @@ export default Vue.extend({
&:active {
}
&[data-is-read],
&[data-is-me] {
&.isRead,
&.isMe {
opacity: 0.8;
}
&:not([data-is-me]):not([data-is-read]) {
&:not(.isMe):not(.isRead) {
> div {
background-image: url("/assets/unread.svg");
background-repeat: no-repeat;
@@ -283,7 +301,7 @@ export default Vue.extend({
&.max-width_400px {
> .history {
> .message {
&:not([data-is-me]):not([data-is-read]) {
&:not(.isMe):not(.isRead) {
> div {
background-image: none;
border-left: solid 4px #3aa2dc;

View File

@@ -9,31 +9,28 @@
@keypress="onKeypress"
@paste="onPaste"
:placeholder="$t('inputMessageHere')"
v-autocomplete="{ model: 'text' }"
></textarea>
<div class="file" @click="file = null" v-if="file">{{ file.name }}</div>
<x-uploader ref="uploader" @uploaded="onUploaded"/>
<button class="send _button" @click="send" :disabled="!canSend || sending" :title="$t('send')">
<template v-if="!sending"><fa :icon="faPaperPlane"/></template><template v-if="sending"><fa icon="spinner .spin"/></template>
<template v-if="!sending"><Fa :icon="faPaperPlane"/></template><template v-if="sending"><Fa icon="spinner .spin"/></template>
</button>
<button class="_button" @click="chooseFile"><fa :icon="faPhotoVideo"/></button>
<button class="_button" @click="insertEmoji"><fa :icon="faLaughSquint"/></button>
<button class="_button" @click="chooseFile"><Fa :icon="faPhotoVideo"/></button>
<button class="_button" @click="insertEmoji"><Fa :icon="faLaughSquint"/></button>
<input ref="file" type="file" @change="onChangeFile"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent, defineAsyncComponent } from 'vue';
import { faPaperPlane, faPhotoVideo, faLaughSquint } from '@fortawesome/free-solid-svg-icons';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as autosize from 'autosize';
import { formatTimeString } from '../../../misc/format-time-string';
import { selectFile } from '../../scripts/select-file';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
import { Autocomplete } from '@/scripts/autocomplete';
export default Vue.extend({
components: {
XUploader: () => import('../../components/uploader.vue').then(m => m.default),
},
export default defineComponent({
props: {
user: {
type: Object,
@@ -69,15 +66,14 @@ export default Vue.extend({
},
file() {
this.saveDraft();
if (this.room.isBottom()) {
this.room.scrollToBottom();
}
}
},
mounted() {
autosize(this.$refs.text);
// TODO: detach when unmount
new Autocomplete(this.$refs.text, this, { model: 'text' });
// 書きかけの投稿を復元
const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftKey];
if (draft) {
@@ -97,7 +93,7 @@ export default Vue.extend({
const ext = lio >= 0 ? file.name.slice(lio) : '';
const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
const name = this.$store.state.settings.pasteDialog
? await this.$root.dialog({
? await os.dialog({
title: this.$t('enterFileName'),
input: {
default: formatted
@@ -109,7 +105,7 @@ export default Vue.extend({
}
} else {
if (items[0].kind == 'file') {
this.$root.dialog({
os.dialog({
type: 'error',
text: this.$t('onlyOneFileCanBeAttached')
});
@@ -119,7 +115,7 @@ export default Vue.extend({
onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
e.preventDefault();
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
@@ -134,7 +130,7 @@ export default Vue.extend({
return;
} else if (e.dataTransfer.files.length > 1) {
e.preventDefault();
this.$root.dialog({
os.dialog({
type: 'error',
text: this.$t('onlyOneFileCanBeAttached')
});
@@ -142,7 +138,7 @@ export default Vue.extend({
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData('mk_drive_file');
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
this.file = JSON.parse(driveFile);
e.preventDefault();
@@ -157,7 +153,7 @@ export default Vue.extend({
},
chooseFile(e) {
selectFile(this, e.currentTarget || e.target, this.$t('selectFile'), false).then(file => {
selectFile(e.currentTarget || e.target, this.$t('selectFile'), false).then(file => {
this.file = file;
});
},
@@ -167,16 +163,14 @@ export default Vue.extend({
},
upload(file: File, name?: string) {
(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
},
onUploaded(file) {
this.file = file;
os.upload(file, this.$store.state.settings.uploadFolder, name).then(res => {
this.file = res;
});
},
send() {
this.sending = true;
this.$root.api('messaging/messages/create', {
os.api('messaging/messages/create', {
userId: this.user ? this.user.id : undefined,
groupId: this.group ? this.group.id : undefined,
text: this.text ? this.text : undefined,
@@ -219,11 +213,8 @@ export default Vue.extend({
},
async insertEmoji(ev) {
const vm = this.$root.new(await import('../../components/emoji-picker.vue').then(m => m.default), {
source: ev.currentTarget || ev.target
}).$once('chosen', emoji => {
os.pickEmoji(ev.currentTarget || ev.target).then(emoji => {
insertTextAtCursor(this.$refs.text, emoji);
vm.close();
});
}
}

View File

@@ -1,13 +1,13 @@
<template>
<div class="thvuemwp" :data-is-me="isMe">
<mk-avatar class="avatar" :user="message.user"/>
<div class="thvuemwp" :class="{ isMe }">
<MkAvatar class="avatar" :user="message.user"/>
<div class="content">
<div class="balloon" :data-no-text="message.text == null">
<div class="balloon" :class="{ noText: message.text == null }" >
<button class="delete-button" v-if="isMe" :title="$t('delete')" @click="del">
<img src="/assets/remove.png" alt="Delete"/>
</button>
<div class="content" v-if="!message.isDeleted">
<mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
<Mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
<div class="file" v-if="message.file">
<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
@@ -20,7 +20,7 @@
</div>
</div>
<div></div>
<mk-url-preview v-for="url in urls" :url="url" :key="url" style="margin: 8px 0;"/>
<MkUrlPreview v-for="url in urls" :url="url" :key="url" style="margin: 8px 0;"/>
<footer>
<template v-if="isGroup">
<span class="read" v-if="message.reads.length > 0">{{ $t('messageRead') }} {{ message.reads.length }}</span>
@@ -28,20 +28,21 @@
<template v-else>
<span class="read" v-if="isMe && message.isRead">{{ $t('messageRead') }}</span>
</template>
<mk-time :time="message.createdAt"/>
<template v-if="message.is_edited"><fa icon="pencil-alt"/></template>
<MkTime :time="message.createdAt"/>
<template v-if="message.is_edited"><Fa icon="pencil-alt"/></template>
</footer>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import { parse } from '../../../mfm/parse';
import { unique } from '../../../prelude/array';
import MkUrlPreview from '../../components/url-preview.vue';
import MkUrlPreview from '@/components/url-preview.vue';
import * as os from '@/os';
export default Vue.extend({
export default defineComponent({
components: {
MkUrlPreview
},
@@ -70,7 +71,7 @@ export default Vue.extend({
},
methods: {
del() {
this.$root.api('messaging/messages/delete', {
os.api('messaging/messages/delete', {
messageId: this.message.id
});
}
@@ -240,7 +241,7 @@ export default Vue.extend({
}
}
&:not([data-is-me]) {
&:not(.isMe) {
padding-left: var(--margin);
> .content {
@@ -251,11 +252,11 @@ export default Vue.extend({
$color: var(--messageBg);
background: $color;
&[data-no-text] {
&.noText {
background: transparent;
}
&:not([data-no-text]):before {
&:not(.noText):before {
left: -14px;
border-top: solid 8px transparent;
border-right: solid 8px $color;
@@ -276,7 +277,7 @@ export default Vue.extend({
}
}
&[data-is-me] {
&.isMe {
flex-direction: row-reverse;
padding-right: var(--margin);
@@ -289,11 +290,11 @@ export default Vue.extend({
background: $me-balloon-color;
text-align: left;
&[data-no-text] {
&.noText {
background: transparent;
}
&:not([data-no-text]):before {
&:not(.noText):before {
right: -14px;
left: auto;
border-top: solid 8px transparent;
@@ -309,7 +310,7 @@ export default Vue.extend({
}
> .text {
&, ::v-deep * {
&, ::v-deep(*) {
color: #fff !important;
}
}
@@ -325,11 +326,5 @@ export default Vue.extend({
}
}
}
&[data-is-deleted] {
> .balloon {
opacity: 0.5;
}
}
}
</style>

View File

@@ -1,57 +1,85 @@
<template>
<div class="mk-messaging-room"
<div class="_section"
@dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop"
>
<template v-if="!fetching && user">
<portal to="title"><mk-user-name :user="user" :nowrap="false" class="name"/></portal>
<portal to="avatar"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
</template>
<template v-if="!fetching && group">
<portal to="icon"><fa :icon="faUsers"/></portal>
<portal to="title">{{ group.name }}</portal>
</template>
<div class="body">
<mk-loading v-if="fetching"/>
<p class="empty" v-if="!fetching && messages.length == 0"><fa :icon="faInfoCircle"/>{{ $t('noMessagesYet') }}</p>
<p class="no-history" v-if="!fetching && messages.length > 0 && !existMoreMessages"><fa :icon="faFlag"/>{{ $t('noMoreHistory') }}</p>
<button class="more _button" ref="loadMore" :class="{ fetching: fetchingMoreMessages }" v-show="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('loading') : $t('loadMore') }}
</button>
<x-list class="messages" :items="messages" v-slot="{ item: message }" direction="up" reversed>
<x-message :message="message" :is-group="group != null" :key="message.id"/>
</x-list>
<div class="_content mk-messaging-room">
<div class="body">
<MkLoading v-if="fetching"/>
<p class="empty" v-if="!fetching && messages.length == 0"><Fa :icon="faInfoCircle"/>{{ $t('noMessagesYet') }}</p>
<p class="no-history" v-if="!fetching && messages.length > 0 && !existMoreMessages"><Fa :icon="faFlag"/>{{ $t('noMoreHistory') }}</p>
<button class="more _button" ref="loadMore" :class="{ fetching: fetchingMoreMessages }" v-show="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
<template v-if="fetchingMoreMessages"><Fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('loading') : $t('loadMore') }}
</button>
<XList class="messages" :items="messages" v-slot="{ item: message }" direction="up" reversed>
<XMessage :message="message" :is-group="group != null" :key="message.id"/>
</XList>
</div>
<footer>
<transition name="fade">
<div class="new-message" v-show="showIndicator">
<button class="_buttonPrimary" @click="onIndicatorClick"><i><Fa :icon="faArrowCircleDown"/></i>{{ $t('newMessageExists') }}</button>
</div>
</transition>
<XForm v-if="!fetching" :user="user" :group="group" ref="form"/>
</footer>
</div>
<footer>
<transition name="fade">
<div class="new-message" v-show="showIndicator">
<button class="_buttonPrimary" @click="onIndicatorClick"><i><fa :icon="faArrowCircleDown"/></i>{{ $t('newMessageExists') }}</button>
</div>
</transition>
<x-form v-if="!fetching" :user="user" :group="group" ref="form"/>
</footer>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faArrowCircleDown, faFlag, faUsers, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import XList from '../../components/date-separated-list.vue';
import { computed, defineComponent } from 'vue';
import { faArrowCircleDown, faFlag, faUsers, faInfoCircle, faEllipsisH, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
import { faWindowMaximize } from '@fortawesome/free-regular-svg-icons';
import XList from '@/components/date-separated-list.vue';
import XMessage from './messaging-room.message.vue';
import XForm from './messaging-room.form.vue';
import parseAcct from '../../../misc/acct/parse';
import { isBottom, onScrollBottom } from '../../scripts/scroll';
import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll';
import * as os from '@/os';
import { popout } from '@/scripts/popout';
export default Vue.extend({
const Component = defineComponent({
components: {
XMessage,
XForm,
XList,
},
inject: ['inWindow'],
props: {
userAcct: {
type: String,
required: false,
},
groupId: {
type: String,
required: false,
},
},
data() {
return {
INFO: computed(() => !this.fetching ? this.user ? {
header: [{
userName: this.user,
avatar: this.user,
}],
action: {
icon: faEllipsisH,
handler: this.menu,
},
} : {
header: [{
title: this.group.name,
icon: faUsers
}],
action: {
icon: faEllipsisH,
handler: this.menu,
},
} : null),
fetching: true,
user: null,
group: null,
@@ -68,7 +96,7 @@ export default Vue.extend({
&& this.existMoreMessages
&& this.fetchMoreMessages()
),
faArrowCircleDown, faFlag, faUsers, faInfoCircle
faArrowCircleDown, faFlag, faInfoCircle
};
},
@@ -79,7 +107,8 @@ export default Vue.extend({
},
watch: {
$route: 'fetch'
userAcct: 'fetch',
groupId: 'fetch',
},
mounted() {
@@ -89,7 +118,7 @@ export default Vue.extend({
}
},
beforeDestroy() {
beforeUnmount() {
this.connection.dispose();
document.removeEventListener('visibilitychange', this.onVisibilitychange);
@@ -100,15 +129,15 @@ export default Vue.extend({
methods: {
async fetch() {
this.fetching = true;
if (this.$route.params.user) {
const user = await this.$root.api('users/show', parseAcct(this.$route.params.user));
if (this.userAcct) {
const user = await os.api('users/show', parseAcct(this.userAcct));
this.user = user;
} else {
const group = await this.$root.api('users/groups/show', { groupId: this.$route.params.group });
const group = await os.api('users/groups/show', { groupId: this.groupId });
this.group = group;
}
this.connection = this.$root.stream.connectToChannel('messaging', {
this.connection = os.stream.connectToChannel('messaging', {
otherparty: this.user ? this.user.id : undefined,
group: this.group ? this.group.id : undefined,
});
@@ -131,7 +160,7 @@ export default Vue.extend({
onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == 'mk_drive_file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
@@ -146,7 +175,7 @@ export default Vue.extend({
this.form.upload(e.dataTransfer.files[0]);
return;
} else if (e.dataTransfer.files.length > 1) {
this.$root.dialog({
os.dialog({
type: 'error',
text: this.$t('onlyOneFileCanBeAttached')
});
@@ -154,7 +183,7 @@ export default Vue.extend({
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData('mk_drive_file');
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
this.form.file = file;
@@ -166,7 +195,7 @@ export default Vue.extend({
return new Promise((resolve, reject) => {
const max = this.existMoreMessages ? 20 : 10;
this.$root.api('messaging/messages', {
os.api('messaging/messages', {
userId: this.user ? this.user.id : undefined,
groupId: this.group ? this.group.id : undefined,
limit: max + 1,
@@ -193,7 +222,7 @@ export default Vue.extend({
},
onMessage(message) {
this.$root.sound('chat');
os.sound('chat');
const _isBottom = isBottom(this.$el, 64);
@@ -248,7 +277,7 @@ export default Vue.extend({
},
scrollToBottom() {
window.scroll(0, document.body.offsetHeight);
scroll(this.$el, this.$el.offsetHeight);
},
onIndicatorClick() {
@@ -279,17 +308,36 @@ export default Vue.extend({
});
}
}
},
menu(ev) {
const url = this.groupId ? `/my/messaging/group/${this.groupId}` : `/my/messaging/${this.userAcct}`;
os.modalMenu([this.inWindow ? undefined : {
text: this.$t('openInWindow'),
icon: faWindowMaximize,
action: () => {
os.pageWindow(url, Component, this.$props);
this.$router.back();
},
}, this.inWindow ? undefined : {
text: this.$t('popout'),
icon: faExternalLinkAlt,
action: () => {
popout(url);
this.$router.back();
},
}], ev.currentTarget || ev.target);
}
}
});
export default Component;
</script>
<style lang="scss" scoped>
.mk-messaging-room {
> .body {
width: 100%;
> .empty {
width: 100%;
margin: 0;
@@ -344,7 +392,7 @@ export default Vue.extend({
}
> .messages {
> ::v-deep * {
> ::v-deep(*) {
margin-bottom: 16px;
}
}
@@ -384,7 +432,7 @@ export default Vue.extend({
transition: opacity 0.1s;
}
.fade-enter, .fade-leave-to {
.fade-enter-from, .fade-leave-to {
transition: opacity 0.5s;
opacity: 0;
}