refactoring

Resolve #7779
This commit is contained in:
syuilo
2021-11-12 02:02:25 +09:00
parent 037837b551
commit 0e4a111f81
1714 changed files with 20803 additions and 11751 deletions

View File

@@ -0,0 +1,307 @@
<template>
<MkSpacer :content-max="800">
<div class="yweeujhr" v-size="{ max: [400] }">
<MkButton @click="start" primary class="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton>
<div class="history" v-if="messages.length > 0">
<MkA v-for="(message, i) in messages"
class="message _block"
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($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"
v-anim="i"
>
<div>
<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/>
<header v-if="message.groupId">
<span class="name">{{ message.group.name }}</span>
<MkTime :time="message.createdAt" class="time"/>
</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" class="time"/>
</header>
<div class="body">
<p class="text"><span class="me" v-if="isMe(message)">{{ $ts.you }}:</span>{{ message.text }}</p>
</div>
</div>
</MkA>
</div>
<div class="_fullinfo" v-if="!fetching && messages.length == 0">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $ts.noHistory }}</div>
</div>
<MkLoading v-if="fetching"/>
</div>
</MkSpacer>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
import * as Acct from 'misskey-js/built/acct';
import MkButton from '@/components/ui/button.vue';
import { acct } from '@/filters/user';
import * as os from '@/os';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
MkButton
},
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.messaging,
icon: 'fas fa-comments',
bg: 'var(--bg)',
},
fetching: true,
moreFetching: false,
messages: [],
connection: null,
};
},
mounted() {
this.connection = markRaw(os.stream.useChannel('messagingIndex'));
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
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;
this.fetching = false;
});
});
},
beforeUnmount() {
this.connection.dispose();
},
methods: {
getAcct: Acct.toString,
isMe(message) {
return message.userId == this.$i.id;
},
onMessage(message) {
if (message.recipientId) {
this.messages = this.messages.filter(m => !(
(m.recipientId == message.recipientId && m.userId == message.userId) ||
(m.recipientId == message.userId && m.userId == message.recipientId)));
this.messages.unshift(message);
} else if (message.groupId) {
this.messages = this.messages.filter(m => m.groupId !== message.groupId);
this.messages.unshift(message);
}
},
onRead(ids) {
for (const id of ids) {
const found = this.messages.find(m => m.id == id);
if (found) {
if (found.recipientId) {
found.isRead = true;
} else if (found.groupId) {
found.reads.push(this.$i.id);
}
}
}
},
start(ev) {
os.popupMenu([{
text: this.$ts.messagingWithUser,
icon: 'fas fa-user',
action: () => { this.startUser() }
}, {
text: this.$ts.messagingWithGroup,
icon: 'fas fa-users',
action: () => { this.startGroup() }
}], ev.currentTarget || ev.target);
},
async startUser() {
os.selectUser().then(user => {
this.$router.push(`/my/messaging/${Acct.toString(user)}`);
});
},
async startGroup() {
const groups1 = await os.api('users/groups/owned');
const groups2 = await os.api('users/groups/joined');
if (groups1.length === 0 && groups2.length === 0) {
os.dialog({
type: 'warning',
title: this.$ts.youHaveNoGroups,
text: this.$ts.joinOrCreateGroup,
});
return;
}
const { canceled, result: group } = await os.dialog({
type: null,
title: this.$ts.group,
select: {
items: groups1.concat(groups2).map(group => ({
value: group, text: group.name
}))
},
showCancelButton: true
});
if (canceled) return;
this.$router.push(`/my/messaging/group/${group.id}`);
},
acct
}
});
</script>
<style lang="scss" scoped>
.yweeujhr {
> .start {
margin: 0 auto var(--margin) auto;
}
> .history {
> .message {
display: block;
text-decoration: none;
margin-bottom: var(--margin);
* {
pointer-events: none;
user-select: none;
}
&:hover {
.avatar {
filter: saturate(200%);
}
}
&:active {
}
&.isRead,
&.isMe {
opacity: 0.8;
}
&:not(.isMe):not(.isRead) {
> div {
background-image: url("/client-assets/unread.svg");
background-repeat: no-repeat;
background-position: 0 center;
}
}
&:after {
content: "";
display: block;
clear: both;
}
> div {
padding: 20px 30px;
&:after {
content: "";
display: block;
clear: both;
}
> header {
display: flex;
align-items: center;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
> .name {
margin: 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1em;
font-weight: bold;
transition: all 0.1s ease;
}
> .username {
margin: 0 8px;
}
> .time {
margin: 0 0 0 auto;
}
}
> .avatar {
float: left;
width: 54px;
height: 54px;
margin: 0 16px 0 0;
border-radius: 8px;
transition: all 0.1s ease;
}
> .body {
> .text {
display: block;
margin: 0 0 0 0;
padding: 0;
overflow: hidden;
overflow-wrap: break-word;
font-size: 1.1em;
color: var(--faceText);
.me {
opacity: 0.7;
}
}
> .image {
display: block;
max-width: 100%;
max-height: 512px;
}
}
}
}
}
&.max-width_400px {
> .history {
> .message {
&:not(.isMe):not(.isRead) {
> div {
background-image: none;
border-left: solid 4px #3aa2dc;
}
}
> div {
padding: 16px;
font-size: 0.9em;
> .avatar {
margin: 0 12px 0 0;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,348 @@
<template>
<div class="pemppnzi _block"
@dragover.stop="onDragover"
@drop.stop="onDrop"
>
<textarea
v-model="text"
ref="text"
@keypress="onKeypress"
@compositionupdate="onCompositionUpdate"
@paste="onPaste"
:placeholder="$ts.inputMessageHere"
></textarea>
<div class="file" @click="file = null" v-if="file">{{ file.name }}</div>
<button class="send _button" @click="send" :disabled="!canSend || sending" :title="$ts.send">
<template v-if="!sending"><i class="fas fa-paper-plane"></i></template><template v-if="sending"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>
</button>
<button class="_button" @click="chooseFile"><i class="fas fa-photo-video"></i></button>
<button class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
<input ref="file" type="file" @change="onChangeFile"/>
</div>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as autosize from 'autosize';
import { formatTimeString } from '@/scripts/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: {
user: {
type: Object,
requird: false,
},
group: {
type: Object,
requird: false,
},
},
data() {
return {
text: null,
file: null,
sending: false,
typing: throttle(3000, () => {
os.stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id });
}),
};
},
computed: {
draftKey(): string {
return this.user ? 'user:' + this.user.id : 'group:' + this.group.id;
},
canSend(): boolean {
return (this.text != null && this.text != '') || this.file != null;
},
room(): any {
return this.$parent;
}
},
watch: {
text() {
this.saveDraft();
},
file() {
this.saveDraft();
}
},
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) {
this.text = draft.data.text;
this.file = draft.data.file;
}
},
methods: {
async onPaste(e: ClipboardEvent) {
const data = e.clipboardData;
const items = data.items;
if (items.length == 1) {
if (items[0].kind == 'file') {
const file = items[0].getAsFile();
const lio = file.name.lastIndexOf('.');
const ext = lio >= 0 ? file.name.slice(lio) : '';
const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
const name = this.$store.state.pasteDialog
? await os.dialog({
title: this.$ts.enterFileName,
input: {
default: formatted
},
allowEmpty: false
}).then(({ canceled, result }) => canceled ? false : result)
: formatted;
if (name) this.upload(file, name);
}
} else {
if (items[0].kind == 'file') {
os.dialog({
type: 'error',
text: this.$ts.onlyOneFileCanBeAttached
});
}
}
},
onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == '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';
}
},
onDrop(e): void {
// ファイルだったら
if (e.dataTransfer.files.length == 1) {
e.preventDefault();
this.upload(e.dataTransfer.files[0]);
return;
} else if (e.dataTransfer.files.length > 1) {
e.preventDefault();
os.dialog({
type: 'error',
text: this.$ts.onlyOneFileCanBeAttached
});
return;
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
this.file = JSON.parse(driveFile);
e.preventDefault();
}
//#endregion
},
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;
});
},
onChangeFile() {
this.upload((this.$refs.file as any).files[0]);
},
upload(file: File, name?: string) {
os.upload(file, this.$store.state.uploadFolder, name).then(res => {
this.file = res;
});
},
send() {
this.sending = true;
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,
fileId: this.file ? this.file.id : undefined
}).then(message => {
this.clear();
}).catch(err => {
console.error(err);
}).then(() => {
this.sending = false;
});
},
clear() {
this.text = '';
this.file = null;
this.deleteDraft();
},
saveDraft() {
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
data[this.draftKey] = {
updatedAt: new Date(),
data: {
text: this.text,
file: this.file
}
}
localStorage.setItem('message_drafts', JSON.stringify(data));
},
deleteDraft() {
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
delete data[this.draftKey];
localStorage.setItem('message_drafts', JSON.stringify(data));
},
async insertEmoji(ev) {
os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
}
}
});
</script>
<style lang="scss" scoped>
.pemppnzi {
position: relative;
> textarea {
cursor: auto;
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
height: 80px;
margin: 0;
padding: 16px 16px 0 16px;
resize: none;
font-size: 1em;
font-family: inherit;
outline: none;
border: none;
border-radius: 0;
box-shadow: none;
background: transparent;
box-sizing: border-box;
color: var(--fg);
}
> .file {
padding: 8px;
color: #444;
background: #eee;
cursor: pointer;
}
> .send {
position: absolute;
bottom: 0;
right: 0;
margin: 0;
padding: 16px;
font-size: 1em;
transition: color 0.1s ease;
color: var(--accent);
&:active {
color: var(--accentDarken);
transition: color 0s ease;
}
}
.files {
display: block;
margin: 0;
padding: 0 8px;
list-style: none;
&:after {
content: '';
display: block;
clear: both;
}
> li {
display: block;
float: left;
margin: 4px;
padding: 0;
width: 64px;
height: 64px;
background-color: #eee;
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
cursor: move;
&:hover {
> .remove {
display: block;
}
}
> .remove {
display: none;
position: absolute;
right: -6px;
top: -6px;
margin: 0;
padding: 0;
background: transparent;
outline: none;
border: none;
border-radius: 0;
box-shadow: none;
cursor: pointer;
}
}
}
._button {
margin: 0;
padding: 16px;
font-size: 1em;
font-weight: normal;
text-decoration: none;
transition: color 0.1s ease;
&:hover {
color: var(--accent);
}
&:active {
color: var(--accentDarken);
transition: color 0s ease;
}
}
input[type=file] {
display: none;
}
}
</style>

View File

@@ -0,0 +1,350 @@
<template>
<div class="thvuemwp" :class="{ isMe }" v-size="{ max: [400, 500] }">
<MkAvatar class="avatar" :user="message.user" :show-indicator="true"/>
<div class="content">
<div class="balloon" :class="{ noText: message.text == null }" >
<button class="delete-button" v-if="isMe" :title="$ts.delete" @click="del">
<img src="/client-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="$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"/>
<p v-else>{{ message.file.name }}</p>
</a>
</div>
</div>
<div class="content" v-else>
<p class="is-deleted">{{ $ts.deleted }}</p>
</div>
</div>
<div></div>
<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">{{ $ts.messageRead }} {{ message.reads.length }}</span>
</template>
<template v-else>
<span class="read" v-if="isMe && message.isRead">{{ $ts.messageRead }}</span>
</template>
<MkTime :time="message.createdAt"/>
<template v-if="message.is_edited"><i class="fas fa-pencil-alt"></i></template>
</footer>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as mfm from 'mfm-js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import MkUrlPreview from '@/components/url-preview.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkUrlPreview
},
props: {
message: {
required: true
},
isGroup: {
required: false
}
},
computed: {
isMe(): boolean {
return this.message.userId === this.$i.id;
},
urls(): string[] {
if (this.message.text) {
return extractUrlFromMfm(mfm.parse(this.message.text));
} else {
return [];
}
}
},
methods: {
del() {
os.api('messaging/messages/delete', {
messageId: this.message.id
});
}
}
});
</script>
<style lang="scss" scoped>
.thvuemwp {
$me-balloon-color: var(--accent);
position: relative;
background-color: transparent;
display: flex;
> .avatar {
position: sticky;
top: calc(var(--stickyTop, 0px) + 16px);
display: block;
width: 54px;
height: 54px;
transition: all 0.1s ease;
}
> .content {
min-width: 0;
> .balloon {
position: relative;
display: inline-flex;
align-items: center;
padding: 0;
min-height: 38px;
border-radius: 16px;
max-width: 100%;
&:before {
content: "";
pointer-events: none;
display: block;
position: absolute;
top: 12px;
}
& + * {
clear: both;
}
&:hover {
> .delete-button {
display: block;
}
}
> .delete-button {
display: none;
position: absolute;
z-index: 1;
top: -4px;
right: -4px;
margin: 0;
padding: 0;
cursor: pointer;
outline: none;
border: none;
border-radius: 0;
box-shadow: none;
background: transparent;
> img {
vertical-align: bottom;
width: 16px;
height: 16px;
cursor: pointer;
}
}
> .content {
max-width: 100%;
> .is-deleted {
display: block;
margin: 0;
padding: 0;
overflow: hidden;
overflow-wrap: break-word;
font-size: 1em;
color: rgba(#000, 0.5);
}
> .text {
display: block;
margin: 0;
padding: 12px 18px;
overflow: hidden;
overflow-wrap: break-word;
word-break: break-word;
font-size: 1em;
color: rgba(#000, 0.8);
& + .file {
> a {
border-radius: 0 0 16px 16px;
}
}
}
> .file {
> a {
display: block;
max-width: 100%;
border-radius: 16px;
overflow: hidden;
text-decoration: none;
&:hover {
text-decoration: none;
> p {
background: #ccc;
}
}
> * {
display: block;
margin: 0;
width: 100%;
max-height: 512px;
object-fit: contain;
box-sizing: border-box;
}
> p {
padding: 30px;
text-align: center;
color: #555;
background: #ddd;
}
}
}
}
}
> footer {
display: block;
margin: 2px 0 0 0;
font-size: 0.65em;
> .read {
margin: 0 8px;
}
> i {
margin-left: 4px;
}
}
}
&:not(.isMe) {
padding-left: var(--margin);
> .content {
padding-left: 16px;
padding-right: 32px;
> .balloon {
$color: var(--messageBg);
background: $color;
&.noText {
background: transparent;
}
&:not(.noText):before {
left: -14px;
border-top: solid 8px transparent;
border-right: solid 8px $color;
border-bottom: solid 8px transparent;
border-left: solid 8px transparent;
}
> .content {
> .text {
color: var(--fg);
}
}
}
> footer {
text-align: left;
}
}
}
&.isMe {
flex-direction: row-reverse;
padding-right: var(--margin);
> .content {
padding-right: 16px;
padding-left: 32px;
text-align: right;
> .balloon {
background: $me-balloon-color;
text-align: left;
::selection {
color: var(--accent);
background-color: #fff;
}
&.noText {
background: transparent;
}
&:not(.noText):before {
right: -14px;
left: auto;
border-top: solid 8px transparent;
border-right: solid 8px transparent;
border-bottom: solid 8px transparent;
border-left: solid 8px $me-balloon-color;
}
> .content {
> p.is-deleted {
color: rgba(#fff, 0.5);
}
> .text {
&, ::v-deep(*) {
color: var(--fgOnAccent) !important;
}
}
}
}
> footer {
text-align: right;
> .read {
user-select: none;
}
}
}
}
&.max-width_400px {
> .avatar {
width: 48px;
height: 48px;
}
> .content {
> .balloon {
> .content {
> .text {
font-size: 0.9em;
}
}
}
}
}
&.max-width_500px {
> .content {
> .balloon {
> .content {
> .text {
padding: 8px 16px;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,470 @@
<template>
<div class="_section"
@dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop"
>
<div class="_content mk-messaging-room">
<div class="body">
<MkLoading v-if="fetching"/>
<p class="empty" v-if="!fetching && messages.length == 0"><i class="fas fa-info-circle"></i>{{ $ts.noMessagesYet }}</p>
<p class="no-history" v-if="!fetching && messages.length > 0 && !existMoreMessages"><i class="fas fa-flag"></i>{{ $ts.noMoreHistory }}</p>
<button class="more _button" ref="loadMore" :class="{ fetching: fetchingMoreMessages }" v-show="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
<template v-if="fetchingMoreMessages"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ fetchingMoreMessages ? $ts.loading : $ts.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>
<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 class="fas fa-arrow-circle-down"></i>{{ $ts.newMessageExists }}</button>
</div>
</transition>
<XForm v-if="!fetching" :user="user" :group="group" ref="form" class="form"/>
</footer>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, markRaw } from 'vue';
import XList from '@/components/date-separated-list.vue';
import XMessage from './messaging-room.message.vue';
import XForm from './messaging-room.form.vue';
import * as Acct from 'misskey-js/built/acct';
import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll';
import * as os from '@/os';
import { popout } from '@/scripts/popout';
import * as sound from '@/scripts/sound';
import * as symbols from '@/symbols';
const Component = defineComponent({
components: {
XMessage,
XForm,
XList,
},
inject: ['inWindow'],
props: {
userAcct: {
type: String,
required: false,
},
groupId: {
type: String,
required: false,
},
},
data() {
return {
[symbols.PAGE_INFO]: computed(() => !this.fetching ? this.user ? {
userName: this.user,
avatar: this.user,
action: {
icon: 'fas fa-ellipsis-h',
handler: this.menu,
},
} : {
title: this.group.name,
icon: 'fas fa-users',
action: {
icon: 'fas fa-ellipsis-h',
handler: this.menu,
},
} : null),
fetching: true,
user: null,
group: null,
fetchingMoreMessages: false,
messages: [],
existMoreMessages: false,
connection: null,
showIndicator: false,
timer: null,
typers: [],
ilObserver: new IntersectionObserver(
(entries) => entries.some((entry) => entry.isIntersecting)
&& !this.fetching
&& !this.fetchingMoreMessages
&& this.existMoreMessages
&& this.fetchMoreMessages()
),
};
},
computed: {
form(): any {
return this.$refs.form;
}
},
watch: {
userAcct: 'fetch',
groupId: 'fetch',
},
mounted() {
this.fetch();
if (this.$store.state.enableInfiniteScroll) {
this.$nextTick(() => this.ilObserver.observe(this.$refs.loadMore as Element));
}
},
beforeUnmount() {
this.connection.dispose();
document.removeEventListener('visibilitychange', this.onVisibilitychange);
this.ilObserver.disconnect();
},
methods: {
async fetch() {
this.fetching = true;
if (this.userAcct) {
const user = await os.api('users/show', Acct.parse(this.userAcct));
this.user = user;
} else {
const group = await os.api('users/groups/show', { groupId: this.groupId });
this.group = group;
}
this.connection = markRaw(os.stream.useChannel('messaging', {
otherparty: this.user ? this.user.id : undefined,
group: this.group ? this.group.id : undefined,
}));
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);
this.fetchMessages().then(() => {
this.scrollToBottom();
// もっと見るの交差検知を発火させないためにfetchは
// スクロールが終わるまでfalseにしておく
// scrollendのようなイベントはないのでsetTimeoutで
setTimeout(() => this.fetching = false, 300);
});
},
onDragover(e) {
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
} else {
e.dataTransfer.dropEffect = 'none';
}
},
onDrop(e): void {
// ファイルだったら
if (e.dataTransfer.files.length == 1) {
this.form.upload(e.dataTransfer.files[0]);
return;
} else if (e.dataTransfer.files.length > 1) {
os.dialog({
type: 'error',
text: this.$ts.onlyOneFileCanBeAttached
});
return;
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
this.form.file = file;
}
//#endregion
},
fetchMessages() {
return new Promise((resolve, reject) => {
const max = this.existMoreMessages ? 20 : 10;
os.api('messaging/messages', {
userId: this.user ? this.user.id : undefined,
groupId: this.group ? this.group.id : undefined,
limit: max + 1,
untilId: this.existMoreMessages ? this.messages[0].id : undefined
}).then(messages => {
if (messages.length == max + 1) {
this.existMoreMessages = true;
messages.pop();
} else {
this.existMoreMessages = false;
}
this.messages.unshift.apply(this.messages, messages.reverse());
resolve();
});
});
},
fetchMoreMessages() {
this.fetchingMoreMessages = true;
this.fetchMessages().then(() => {
this.fetchingMoreMessages = false;
});
},
onMessage(message) {
sound.play('chat');
const _isBottom = isBottom(this.$el, 64);
this.messages.push(message);
if (message.userId != this.$i.id && !document.hidden) {
this.connection.send('read', {
id: message.id
});
}
if (_isBottom) {
// Scroll to bottom
this.$nextTick(() => {
this.scrollToBottom();
});
} else if (message.userId != this.$i.id) {
// Notify
this.notifyNewMessage();
}
},
onRead(x) {
if (this.user) {
if (!Array.isArray(x)) x = [x];
for (const id of x) {
if (this.messages.some(x => x.id == id)) {
const exist = this.messages.map(x => x.id).indexOf(id);
this.messages[exist] = {
...this.messages[exist],
isRead: true,
};
}
}
} else if (this.group) {
for (const id of x.ids) {
if (this.messages.some(x => x.id == id)) {
const exist = this.messages.map(x => x.id).indexOf(id);
this.messages[exist] = {
...this.messages[exist],
reads: [...this.messages[exist].reads, x.userId]
};
}
}
}
},
onDeleted(id) {
const msg = this.messages.find(m => m.id === id);
if (msg) {
this.messages = this.messages.filter(m => m.id !== msg.id);
}
},
scrollToBottom() {
scroll(this.$el, { top: this.$el.offsetHeight });
},
onIndicatorClick() {
this.showIndicator = false;
this.scrollToBottom();
},
notifyNewMessage() {
this.showIndicator = true;
onScrollBottom(this.$el, () => {
this.showIndicator = false;
});
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.showIndicator = false;
}, 4000);
},
onVisibilitychange() {
if (document.hidden) return;
for (const message of this.messages) {
if (message.userId !== this.$i.id && !message.isRead) {
this.connection.send('read', {
id: message.id
});
}
}
},
menu(ev) {
const path = this.groupId ? `/my/messaging/group/${this.groupId}` : `/my/messaging/${this.userAcct}`;
os.popupMenu([this.inWindow ? undefined : {
text: this.$ts.openInWindow,
icon: 'fas fa-window-maximize',
action: () => {
os.pageWindow(path);
this.$router.back();
},
}, this.inWindow ? undefined : {
text: this.$ts.popout,
icon: 'fas fa-external-link-alt',
action: () => {
popout(path);
this.$router.back();
},
}], ev.currentTarget || ev.target);
}
}
});
export default Component;
</script>
<style lang="scss" scoped>
.mk-messaging-room {
> .body {
> .empty {
width: 100%;
margin: 0;
padding: 16px 8px 8px 8px;
text-align: center;
font-size: 0.8em;
opacity: 0.5;
i {
margin-right: 4px;
}
}
> .no-history {
display: block;
margin: 0;
padding: 16px;
text-align: center;
font-size: 0.8em;
color: var(--messagingRoomInfo);
opacity: 0.5;
i {
margin-right: 4px;
}
}
> .more {
display: block;
margin: 16px auto;
padding: 0 12px;
line-height: 24px;
color: #fff;
background: rgba(#000, 0.3);
border-radius: 12px;
&:hover {
background: rgba(#000, 0.4);
}
&:active {
background: rgba(#000, 0.5);
}
&.fetching {
cursor: wait;
}
> i {
margin-right: 4px;
}
}
> .messages {
> ::v-deep(*) {
margin-bottom: 16px;
}
}
}
> footer {
width: 100%;
position: relative;
> .new-message {
position: absolute;
top: -48px;
width: 100%;
padding: 8px 0;
text-align: center;
> button {
display: inline-block;
margin: 0;
padding: 0 12px 0 30px;
line-height: 32px;
font-size: 12px;
border-radius: 16px;
> i {
position: absolute;
top: 0;
left: 10px;
line-height: 32px;
font-size: 16px;
}
}
}
> .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: " ";
}
}
}
> .form {
border-top: solid 0.5px var(--divider);
}
}
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.1s;
}
.fade-enter-from, .fade-leave-to {
transition: opacity 0.5s;
opacity: 0;
}
</style>